forked from AkkomaGang/akkoma-fe
Merge remote-tracking branch 'upstream/develop' into themes-accent
* upstream/develop: (33 commits) add emoji reactions to changelog fix emoji reaction classes broken in develop review changes Fix password and email update remove unnecessary anonymous function Apply suggestion to src/services/api/api.service.js remove logs/commented code remove favs count from react button remove mock data change emoji reactions to use new format Added polyfills for EventTarget (needed for Safari) and CustomEvent (needed for IE) Fix missing TWKN when logged in, but server is set to private mode. Fix follower request fetching Add domain mutes to changelog Implement domain mutes v2 change changelog to reflect actual release history of frontend Fix #750 , fix error messages and captcha resetting Optimize Notifications Rendering update CHANGELOG Use last seen notif instead of first unseen notif for sinceId ...
This commit is contained in:
commit
f0c4bb1193
49 changed files with 840 additions and 104 deletions
18
CHANGELOG.md
18
CHANGELOG.md
|
@ -5,12 +5,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- Icons in nav panel
|
||||
- Private mode support
|
||||
- Support for 'Move' type notifications
|
||||
- Pleroma AMOLED dark theme
|
||||
- User level domain mutes, under User Settings -> Mutes
|
||||
- Emoji reactions for statuses
|
||||
### Changed
|
||||
- Captcha now resets on failed registrations
|
||||
- Notifications column now cleans itself up to optimize performance when tab is left open for a long time
|
||||
- 403 messaging
|
||||
### Fixed
|
||||
- Single notifications left unread when hitting read on another device/tab
|
||||
- Registration fixed
|
||||
- Deactivation of remote accounts from frontend
|
||||
|
||||
## [1.1.7 and earlier] - 2019-12-14
|
||||
### Added
|
||||
- Ability to hide/show repeats from user
|
||||
- User profile button clutter organized into a menu
|
||||
- Emoji picker
|
||||
- Started changelog anew
|
||||
- Ability to change user's email
|
||||
- About page
|
||||
- Added remote user redirect
|
||||
### Changed
|
||||
- theme engine update to 3
|
||||
- theme doesn't get saved to local storage when opening FE anonymously
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.7.6",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@ungap/event-target": "^0.1.0",
|
||||
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
|
||||
"@vue/babel-plugin-transform-vue-jsx": "^1.1.2",
|
||||
"@vue/test-utils": "^1.0.0-beta.26",
|
||||
|
@ -56,6 +57,7 @@
|
|||
"connect-history-api-fallback": "^1.1.0",
|
||||
"cross-spawn": "^4.0.2",
|
||||
"css-loader": "^0.28.0",
|
||||
"custom-event-polyfill": "^1.0.7",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-friendly-formatter": "^2.0.5",
|
||||
|
|
|
@ -187,12 +187,9 @@ const getAppSecret = async ({ store }) => {
|
|||
})
|
||||
}
|
||||
|
||||
const resolveStaffAccounts = async ({ store, accounts }) => {
|
||||
const backendInteractor = store.state.api.backendInteractor
|
||||
let nicknames = accounts.map(uri => uri.split('/').pop())
|
||||
.map(id => backendInteractor.fetchUser({ id }))
|
||||
nicknames = await Promise.all(nicknames)
|
||||
|
||||
const resolveStaffAccounts = ({ store, accounts }) => {
|
||||
const nicknames = accounts.map(uri => uri.split('/').pop())
|
||||
nicknames.map(nickname => store.dispatch('fetchUser', nickname))
|
||||
store.dispatch('setInstanceOption', { name: 'staffAccounts', value: nicknames })
|
||||
}
|
||||
|
||||
|
@ -238,7 +235,7 @@ const getNodeInfo = async ({ store }) => {
|
|||
})
|
||||
|
||||
const accounts = metadata.staffAccounts
|
||||
await resolveStaffAccounts({ store, accounts })
|
||||
resolveStaffAccounts({ store, accounts })
|
||||
} else {
|
||||
throw (res)
|
||||
}
|
||||
|
|
|
@ -150,6 +150,7 @@ const conversation = {
|
|||
if (!id) return
|
||||
this.highlight = id
|
||||
this.$store.dispatch('fetchFavsAndRepeats', id)
|
||||
this.$store.dispatch('fetchEmojiReactionsBy', id)
|
||||
},
|
||||
getHighlight () {
|
||||
return this.isExpanded ? this.highlight : null
|
||||
|
|
15
src/components/domain_mute_card/domain_mute_card.js
Normal file
15
src/components/domain_mute_card/domain_mute_card.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import ProgressButton from '../progress_button/progress_button.vue'
|
||||
|
||||
const DomainMuteCard = {
|
||||
props: ['domain'],
|
||||
components: {
|
||||
ProgressButton
|
||||
},
|
||||
methods: {
|
||||
unmuteDomain () {
|
||||
return this.$store.dispatch('unmuteDomain', this.domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default DomainMuteCard
|
38
src/components/domain_mute_card/domain_mute_card.vue
Normal file
38
src/components/domain_mute_card/domain_mute_card.vue
Normal file
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<div class="domain-mute-card">
|
||||
<div class="domain-mute-card-domain">
|
||||
{{ domain }}
|
||||
</div>
|
||||
<ProgressButton
|
||||
:click="unmuteDomain"
|
||||
class="btn btn-default"
|
||||
>
|
||||
{{ $t('domain_mute_card.unmute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('domain_mute_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./domain_mute_card.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.domain-mute-card {
|
||||
flex: 1 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.6em 1em 0.6em 0;
|
||||
|
||||
&-domain {
|
||||
margin-right: 1em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 10em;
|
||||
}
|
||||
}
|
||||
</style>
|
32
src/components/emoji_reactions/emoji_reactions.js
Normal file
32
src/components/emoji_reactions/emoji_reactions.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
|
||||
const EmojiReactions = {
|
||||
name: 'EmojiReactions',
|
||||
props: ['status'],
|
||||
computed: {
|
||||
emojiReactions () {
|
||||
return this.status.emoji_reactions
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
reactedWith (emoji) {
|
||||
const user = this.$store.state.users.currentUser
|
||||
const reaction = this.status.emoji_reactions.find(r => r.emoji === emoji)
|
||||
return reaction.accounts && reaction.accounts.find(u => u.id === user.id)
|
||||
},
|
||||
reactWith (emoji) {
|
||||
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
|
||||
},
|
||||
unreact (emoji) {
|
||||
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
|
||||
},
|
||||
emojiOnClick (emoji, event) {
|
||||
if (this.reactedWith(emoji)) {
|
||||
this.unreact(emoji)
|
||||
} else {
|
||||
this.reactWith(emoji)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default EmojiReactions
|
49
src/components/emoji_reactions/emoji_reactions.vue
Normal file
49
src/components/emoji_reactions/emoji_reactions.vue
Normal file
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<div class="emoji-reactions">
|
||||
<button
|
||||
v-for="(reaction) in emojiReactions"
|
||||
:key="reaction.emoji"
|
||||
class="emoji-reaction btn btn-default"
|
||||
:class="{ 'picked-reaction': reactedWith(reaction.emoji) }"
|
||||
@click="emojiOnClick(reaction.emoji, $event)"
|
||||
>
|
||||
<span class="reaction-emoji">{{ reaction.emoji }}</span>
|
||||
<span>{{ reaction.count }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./emoji_reactions.js" ></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.emoji-reactions {
|
||||
display: flex;
|
||||
margin-top: 0.25em;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.emoji-reaction {
|
||||
padding: 0 0.5em;
|
||||
margin-right: 0.5em;
|
||||
margin-top: 0.5em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
.reaction-emoji {
|
||||
width: 1.25em;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.picked-reaction {
|
||||
border: 1px solid var(--link, $fallback--link);
|
||||
margin-left: -1px; // offset the border, can't use inset shadows either
|
||||
margin-right: calc(0.5em - 1px);
|
||||
}
|
||||
|
||||
</style>
|
|
@ -3,7 +3,8 @@ import Notifications from '../notifications/notifications.vue'
|
|||
const tabModeDict = {
|
||||
mentions: ['mention'],
|
||||
'likes+repeats': ['repeat', 'like'],
|
||||
follows: ['follow']
|
||||
follows: ['follow'],
|
||||
moves: ['move']
|
||||
}
|
||||
|
||||
const Interactions = {
|
||||
|
|
|
@ -21,6 +21,10 @@
|
|||
key="follows"
|
||||
:label="$t('interactions.follows')"
|
||||
/>
|
||||
<span
|
||||
key="moves"
|
||||
:label="$t('interactions.moves')"
|
||||
/>
|
||||
</tab-switcher>
|
||||
<Notifications
|
||||
ref="notifications"
|
||||
|
|
|
@ -3,7 +3,7 @@ import { mapState } from 'vuex'
|
|||
const NavPanel = {
|
||||
created () {
|
||||
if (this.currentUser && this.currentUser.locked) {
|
||||
this.$store.dispatch('startFetchingFollowRequest')
|
||||
this.$store.dispatch('startFetchingFollowRequests')
|
||||
}
|
||||
},
|
||||
computed: mapState({
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<i class="button-icon icon-users" /> {{ $t("nav.public_tl") }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li v-if="federating && !privateMode">
|
||||
<li v-if="federating && (currentUser || !privateMode)">
|
||||
<router-link :to="{ name: 'public-external-timeline' }">
|
||||
<i class="button-icon icon-globe" /> {{ $t("nav.twkn") }}
|
||||
</router-link>
|
||||
|
|
|
@ -43,18 +43,18 @@ const Notification = {
|
|||
const user = this.notification.from_profile
|
||||
return highlightStyle(highlight[user.screen_name])
|
||||
},
|
||||
userInStore () {
|
||||
return this.$store.getters.findUser(this.notification.from_profile.id)
|
||||
},
|
||||
user () {
|
||||
if (this.userInStore) {
|
||||
return this.userInStore
|
||||
}
|
||||
return this.notification.from_profile
|
||||
return this.$store.getters.findUser(this.notification.from_profile.id)
|
||||
},
|
||||
userProfileLink () {
|
||||
return this.generateUserProfileLink(this.user)
|
||||
},
|
||||
targetUser () {
|
||||
return this.$store.getters.findUser(this.notification.target.id)
|
||||
},
|
||||
targetUserProfileLink () {
|
||||
return this.generateUserProfileLink(this.targetUser)
|
||||
},
|
||||
needMute () {
|
||||
return this.user.muted
|
||||
}
|
||||
|
|
|
@ -74,9 +74,13 @@
|
|||
<i class="fa icon-user-plus lit" />
|
||||
<small>{{ $t('notifications.followed_you') }}</small>
|
||||
</span>
|
||||
<span v-if="notification.type === 'move'">
|
||||
<i class="fa icon-arrow-curved lit" />
|
||||
<small>{{ $t('notifications.migrated_to') }}</small>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="notification.type === 'follow'"
|
||||
v-if="notification.type === 'follow' || notification.type === 'move'"
|
||||
class="timeago"
|
||||
>
|
||||
<span class="faint">
|
||||
|
@ -115,6 +119,14 @@
|
|||
@{{ notification.from_profile.screen_name }}
|
||||
</router-link>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="notification.type === 'move'"
|
||||
class="move-text"
|
||||
>
|
||||
<router-link :to="targetUserProfileLink">
|
||||
@{{ notification.target.screen_name }}
|
||||
</router-link>
|
||||
</div>
|
||||
<template v-else>
|
||||
<status
|
||||
class="faint"
|
||||
|
|
|
@ -2,10 +2,12 @@ import Notification from '../notification/notification.vue'
|
|||
import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
|
||||
import {
|
||||
notificationsFromStore,
|
||||
visibleNotificationsFromStore,
|
||||
filteredNotificationsFromStore,
|
||||
unseenNotificationsFromStore
|
||||
} from '../../services/notification_utils/notification_utils.js'
|
||||
|
||||
const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30
|
||||
|
||||
const Notifications = {
|
||||
props: {
|
||||
// Disables display of panel header
|
||||
|
@ -18,7 +20,11 @@ const Notifications = {
|
|||
},
|
||||
data () {
|
||||
return {
|
||||
bottomedOut: false
|
||||
bottomedOut: false,
|
||||
// How many seen notifications to display in the list. The more there are,
|
||||
// the heavier the page becomes. This count is increased when loading
|
||||
// older notifications, and cut back to default whenever hitting "Read!".
|
||||
seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -34,14 +40,17 @@ const Notifications = {
|
|||
unseenNotifications () {
|
||||
return unseenNotificationsFromStore(this.$store)
|
||||
},
|
||||
visibleNotifications () {
|
||||
return visibleNotificationsFromStore(this.$store, this.filterMode)
|
||||
filteredNotifications () {
|
||||
return filteredNotificationsFromStore(this.$store, this.filterMode)
|
||||
},
|
||||
unseenCount () {
|
||||
return this.unseenNotifications.length
|
||||
},
|
||||
loading () {
|
||||
return this.$store.state.statuses.notifications.loading
|
||||
},
|
||||
notificationsToDisplay () {
|
||||
return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
@ -64,12 +73,21 @@ const Notifications = {
|
|||
methods: {
|
||||
markAsSeen () {
|
||||
this.$store.dispatch('markNotificationsAsSeen')
|
||||
this.seenToDisplayCount = DEFAULT_SEEN_TO_DISPLAY_COUNT
|
||||
},
|
||||
fetchOlderNotifications () {
|
||||
if (this.loading) {
|
||||
return
|
||||
}
|
||||
|
||||
const seenCount = this.filteredNotifications.length - this.unseenCount
|
||||
if (this.seenToDisplayCount < seenCount) {
|
||||
this.seenToDisplayCount = Math.min(this.seenToDisplayCount + 20, seenCount)
|
||||
return
|
||||
} else if (this.seenToDisplayCount > seenCount) {
|
||||
this.seenToDisplayCount = seenCount
|
||||
}
|
||||
|
||||
const store = this.$store
|
||||
const credentials = store.state.users.currentUser.credentials
|
||||
store.commit('setNotificationsLoading', { value: true })
|
||||
|
@ -82,6 +100,7 @@ const Notifications = {
|
|||
if (notifs.length === 0) {
|
||||
this.bottomedOut = true
|
||||
}
|
||||
this.seenToDisplayCount += notifs.length
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.follow-text {
|
||||
.follow-text, .move-text {
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
|
||||
|
@ -154,6 +154,11 @@
|
|||
color: var(--cOrange, $fallback--cOrange);
|
||||
}
|
||||
|
||||
.icon-arrow-curved.lit {
|
||||
color: $fallback--cBlue;
|
||||
color: var(--cBlue, $fallback--cBlue);
|
||||
}
|
||||
|
||||
.status-content {
|
||||
margin: 0;
|
||||
max-height: 300px;
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
</div>
|
||||
<div class="panel-body">
|
||||
<div
|
||||
v-for="notification in visibleNotifications"
|
||||
v-for="notification in notificationsToDisplay"
|
||||
:key="notification.id"
|
||||
class="notification"
|
||||
:class="{"unseen": !minimalMode && !notification.seen}"
|
||||
|
|
43
src/components/react_button/react_button.js
Normal file
43
src/components/react_button/react_button.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { mapGetters } from 'vuex'
|
||||
|
||||
const ReactButton = {
|
||||
props: ['status', 'loggedIn'],
|
||||
data () {
|
||||
return {
|
||||
showTooltip: false,
|
||||
filterWord: '',
|
||||
popperOptions: {
|
||||
modifiers: {
|
||||
preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openReactionSelect () {
|
||||
this.showTooltip = true
|
||||
this.filterWord = ''
|
||||
},
|
||||
closeReactionSelect () {
|
||||
this.showTooltip = false
|
||||
},
|
||||
addReaction (event, emoji) {
|
||||
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
|
||||
this.closeReactionSelect()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
commonEmojis () {
|
||||
return ['❤️', '😠', '👀', '😂', '🔥']
|
||||
},
|
||||
emojis () {
|
||||
if (this.filterWord !== '') {
|
||||
return this.$store.state.instance.emoji.filter(emoji => emoji.displayText.includes(this.filterWord))
|
||||
}
|
||||
return this.$store.state.instance.emoji || []
|
||||
},
|
||||
...mapGetters(['mergedConfig'])
|
||||
}
|
||||
}
|
||||
|
||||
export default ReactButton
|
109
src/components/react_button/react_button.vue
Normal file
109
src/components/react_button/react_button.vue
Normal file
|
@ -0,0 +1,109 @@
|
|||
<template>
|
||||
<v-popover
|
||||
:popper-options="popperOptions"
|
||||
:open="showTooltip"
|
||||
trigger="manual"
|
||||
placement="top"
|
||||
class="react-button-popover"
|
||||
@hide="closeReactionSelect"
|
||||
>
|
||||
<div slot="popover">
|
||||
<div class="reaction-picker-filter">
|
||||
<input
|
||||
v-model="filterWord"
|
||||
:placeholder="$t('emoji.search_emoji')"
|
||||
>
|
||||
</div>
|
||||
<div class="reaction-picker">
|
||||
<span
|
||||
v-for="emoji in commonEmojis"
|
||||
:key="emoji"
|
||||
class="emoji-button"
|
||||
@click="addReaction($event, emoji)"
|
||||
>
|
||||
{{ emoji }}
|
||||
</span>
|
||||
<div class="reaction-picker-divider" />
|
||||
<span
|
||||
v-for="(emoji, key) in emojis"
|
||||
:key="key"
|
||||
class="emoji-button"
|
||||
@click="addReaction($event, emoji.replacement)"
|
||||
>
|
||||
{{ emoji.replacement }}
|
||||
</span>
|
||||
<div class="reaction-bottom-fader" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="loggedIn"
|
||||
@click.prevent="openReactionSelect"
|
||||
>
|
||||
<i
|
||||
class="icon-smile button-icon add-reaction-button"
|
||||
:title="$t('tool_tip.add_reaction')"
|
||||
/>
|
||||
</div>
|
||||
</v-popover>
|
||||
</template>
|
||||
|
||||
<script src="./react_button.js" ></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.reaction-picker-filter {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.reaction-picker-divider {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
margin: 0.5em;
|
||||
background-color: var(--border, $fallback--border);
|
||||
}
|
||||
|
||||
.reaction-picker {
|
||||
width: 10em;
|
||||
height: 9em;
|
||||
font-size: 1.5em;
|
||||
overflow-y: scroll;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 0.5em;
|
||||
text-align: center;
|
||||
align-content: flex-start;
|
||||
user-select: none;
|
||||
|
||||
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
|
||||
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
|
||||
linear-gradient(to top, white, white);
|
||||
transition: mask-size 150ms;
|
||||
mask-size: 100% 20px, 100% 20px, auto;
|
||||
// Autoprefixed seem to ignore this one, and also syntax is different
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
|
||||
.emoji-button {
|
||||
cursor: pointer;
|
||||
|
||||
flex-basis: 20%;
|
||||
line-height: 1.5em;
|
||||
align-content: center;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.25);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-reaction-button {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: $fallback--text;
|
||||
color: var(--text, $fallback--text);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -63,7 +63,8 @@ const registration = {
|
|||
await this.signUp(this.user)
|
||||
this.$router.push({ name: 'friends' })
|
||||
} catch (error) {
|
||||
console.warn('Registration failed: ' + error)
|
||||
console.warn('Registration failed: ', error)
|
||||
this.setCaptcha()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -170,7 +170,7 @@
|
|||
<label
|
||||
class="form--label"
|
||||
for="captcha-label"
|
||||
>{{ $t('captcha') }}</label>
|
||||
>{{ $t('registration.captcha') }}</label>
|
||||
|
||||
<template v-if="['kocaptcha', 'native'].includes(captcha.type)">
|
||||
<img
|
||||
|
|
|
@ -323,6 +323,11 @@
|
|||
{{ $t('settings.notification_visibility_mentions') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
<li>
|
||||
<Checkbox v-model="notificationVisibility.moves">
|
||||
{{ $t('settings.notification_visibility_moves') }}
|
||||
</Checkbox>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
@ -12,7 +12,7 @@ const SideDrawer = {
|
|||
this.closeGesture = GestureService.swipeGesture(GestureService.DIRECTION_LEFT, this.toggleDrawer)
|
||||
|
||||
if (this.currentUser && this.currentUser.locked) {
|
||||
this.$store.dispatch('startFetchingFollowRequest')
|
||||
this.$store.dispatch('startFetchingFollowRequests')
|
||||
}
|
||||
},
|
||||
components: { UserCard },
|
||||
|
|
|
@ -88,7 +88,7 @@
|
|||
</router-link>
|
||||
</li>
|
||||
<li
|
||||
v-if="federating && !privateMode"
|
||||
v-if="federating && (currentUser || !privateMode)"
|
||||
@click="toggleDrawer"
|
||||
>
|
||||
<router-link to="/main/all">
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import map from 'lodash/map'
|
||||
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
|
||||
|
||||
const StaffPanel = {
|
||||
|
@ -6,7 +7,7 @@ const StaffPanel = {
|
|||
},
|
||||
computed: {
|
||||
staffAccounts () {
|
||||
return this.$store.state.instance.staffAccounts
|
||||
return map(this.$store.state.instance.staffAccounts, nickname => this.$store.getters.findUser(nickname)).filter(_ => _)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Attachment from '../attachment/attachment.vue'
|
||||
import FavoriteButton from '../favorite_button/favorite_button.vue'
|
||||
import ReactButton from '../react_button/react_button.vue'
|
||||
import RetweetButton from '../retweet_button/retweet_button.vue'
|
||||
import Poll from '../poll/poll.vue'
|
||||
import ExtraButtons from '../extra_buttons/extra_buttons.vue'
|
||||
|
@ -11,6 +12,7 @@ import LinkPreview from '../link-preview/link-preview.vue'
|
|||
import AvatarList from '../avatar_list/avatar_list.vue'
|
||||
import Timeago from '../timeago/timeago.vue'
|
||||
import StatusPopover from '../status_popover/status_popover.vue'
|
||||
import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import fileType from 'src/services/file_type/file_type.service'
|
||||
import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
|
||||
|
@ -319,6 +321,7 @@ const Status = {
|
|||
components: {
|
||||
Attachment,
|
||||
FavoriteButton,
|
||||
ReactButton,
|
||||
RetweetButton,
|
||||
ExtraButtons,
|
||||
PostStatusForm,
|
||||
|
@ -329,7 +332,8 @@ const Status = {
|
|||
LinkPreview,
|
||||
AvatarList,
|
||||
Timeago,
|
||||
StatusPopover
|
||||
StatusPopover,
|
||||
EmojiReactions
|
||||
},
|
||||
methods: {
|
||||
visibilityIcon (visibility) {
|
||||
|
|
|
@ -354,6 +354,10 @@
|
|||
</div>
|
||||
</transition>
|
||||
|
||||
<EmojiReactions
|
||||
:status="status"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="!noHeading && !isPreview"
|
||||
class="status-actions media-body"
|
||||
|
@ -382,6 +386,10 @@
|
|||
:logged-in="loggedIn"
|
||||
:status="status"
|
||||
/>
|
||||
<ReactButton
|
||||
:logged-in="loggedIn"
|
||||
:status="status"
|
||||
/>
|
||||
<extra-buttons
|
||||
:status="status"
|
||||
@onError="showError"
|
||||
|
|
|
@ -139,7 +139,7 @@ const Mfa = {
|
|||
|
||||
// fetch settings from server
|
||||
async fetchSettings () {
|
||||
let result = await this.backendInteractor.fetchSettingsMFA()
|
||||
let result = await this.backendInteractor.settingsMFA()
|
||||
if (result.error) return
|
||||
this.settings = result.settings
|
||||
this.settings.available = true
|
||||
|
|
|
@ -9,6 +9,7 @@ import ScopeSelector from '../scope_selector/scope_selector.vue'
|
|||
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
||||
import BlockCard from '../block_card/block_card.vue'
|
||||
import MuteCard from '../mute_card/mute_card.vue'
|
||||
import DomainMuteCard from '../domain_mute_card/domain_mute_card.vue'
|
||||
import SelectableList from '../selectable_list/selectable_list.vue'
|
||||
import ProgressButton from '../progress_button/progress_button.vue'
|
||||
import EmojiInput from '../emoji_input/emoji_input.vue'
|
||||
|
@ -32,6 +33,12 @@ const MuteList = withSubscription({
|
|||
childPropName: 'items'
|
||||
})(SelectableList)
|
||||
|
||||
const DomainMuteList = withSubscription({
|
||||
fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
|
||||
select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
|
||||
childPropName: 'items'
|
||||
})(SelectableList)
|
||||
|
||||
const UserSettings = {
|
||||
data () {
|
||||
return {
|
||||
|
@ -67,7 +74,8 @@ const UserSettings = {
|
|||
changedPassword: false,
|
||||
changePasswordError: false,
|
||||
activeTab: 'profile',
|
||||
notificationSettings: this.$store.state.users.currentUser.notification_settings
|
||||
notificationSettings: this.$store.state.users.currentUser.notification_settings,
|
||||
newDomainToMute: ''
|
||||
}
|
||||
},
|
||||
created () {
|
||||
|
@ -80,10 +88,12 @@ const UserSettings = {
|
|||
ImageCropper,
|
||||
BlockList,
|
||||
MuteList,
|
||||
DomainMuteList,
|
||||
EmojiInput,
|
||||
Autosuggest,
|
||||
BlockCard,
|
||||
MuteCard,
|
||||
DomainMuteCard,
|
||||
ProgressButton,
|
||||
Importer,
|
||||
Exporter,
|
||||
|
@ -297,7 +307,7 @@ const UserSettings = {
|
|||
newPassword: this.changePasswordInputs[1],
|
||||
newPasswordConfirmation: this.changePasswordInputs[2]
|
||||
}
|
||||
this.$store.state.api.backendInteractor.changePassword({ params })
|
||||
this.$store.state.api.backendInteractor.changePassword(params)
|
||||
.then((res) => {
|
||||
if (res.status === 'success') {
|
||||
this.changedPassword = true
|
||||
|
@ -314,7 +324,7 @@ const UserSettings = {
|
|||
email: this.newEmail,
|
||||
password: this.changeEmailPassword
|
||||
}
|
||||
this.$store.state.api.backendInteractor.changeEmail({ params })
|
||||
this.$store.state.api.backendInteractor.changeEmail(params)
|
||||
.then((res) => {
|
||||
if (res.status === 'success') {
|
||||
this.changedEmail = true
|
||||
|
@ -365,6 +375,13 @@ const UserSettings = {
|
|||
unmuteUsers (ids) {
|
||||
return this.$store.dispatch('unmuteUsers', ids)
|
||||
},
|
||||
unmuteDomains (domains) {
|
||||
return this.$store.dispatch('unmuteDomains', domains)
|
||||
},
|
||||
muteDomain () {
|
||||
return this.$store.dispatch('muteDomain', this.newDomainToMute)
|
||||
.then(() => { this.newDomainToMute = '' })
|
||||
},
|
||||
identity (value) {
|
||||
return value
|
||||
}
|
||||
|
|
|
@ -509,59 +509,114 @@
|
|||
</div>
|
||||
|
||||
<div :label="$t('settings.mutes_tab')">
|
||||
<div class="profile-edit-usersearch-wrapper">
|
||||
<Autosuggest
|
||||
:filter="filterUnMutedUsers"
|
||||
:query="queryUserIds"
|
||||
:placeholder="$t('settings.search_user_to_mute')"
|
||||
>
|
||||
<MuteCard
|
||||
slot-scope="row"
|
||||
:user-id="row.item"
|
||||
/>
|
||||
</Autosuggest>
|
||||
</div>
|
||||
<MuteList
|
||||
:refresh="true"
|
||||
:get-key="identity"
|
||||
>
|
||||
<template
|
||||
slot="header"
|
||||
slot-scope="{selected}"
|
||||
>
|
||||
<div class="profile-edit-bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => muteUsers(selected)"
|
||||
<tab-switcher>
|
||||
<div label="Users">
|
||||
<div class="profile-edit-usersearch-wrapper">
|
||||
<Autosuggest
|
||||
:filter="filterUnMutedUsers"
|
||||
:query="queryUserIds"
|
||||
:placeholder="$t('settings.search_user_to_mute')"
|
||||
>
|
||||
{{ $t('user_card.mute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('user_card.mute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => unmuteUsers(selected)"
|
||||
<MuteCard
|
||||
slot-scope="row"
|
||||
:user-id="row.item"
|
||||
/>
|
||||
</Autosuggest>
|
||||
</div>
|
||||
<MuteList
|
||||
:refresh="true"
|
||||
:get-key="identity"
|
||||
>
|
||||
<template
|
||||
slot="header"
|
||||
slot-scope="{selected}"
|
||||
>
|
||||
{{ $t('user_card.unmute') }}
|
||||
<div class="profile-edit-bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => muteUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.mute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('user_card.mute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => unmuteUsers(selected)"
|
||||
>
|
||||
{{ $t('user_card.unmute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('user_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
slot="item"
|
||||
slot-scope="{item}"
|
||||
>
|
||||
<MuteCard :user-id="item" />
|
||||
</template>
|
||||
<template slot="empty">
|
||||
{{ $t('settings.no_mutes') }}
|
||||
</template>
|
||||
</MuteList>
|
||||
</div>
|
||||
|
||||
<div :label="$t('settings.domain_mutes')">
|
||||
<div class="profile-edit-domain-mute-form">
|
||||
<input
|
||||
v-model="newDomainToMute"
|
||||
:placeholder="$t('settings.type_domains_to_mute')"
|
||||
type="text"
|
||||
@keyup.enter="muteDomain"
|
||||
>
|
||||
<ProgressButton
|
||||
class="btn btn-default"
|
||||
:click="muteDomain"
|
||||
>
|
||||
{{ $t('domain_mute_card.mute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('user_card.unmute_progress') }}
|
||||
{{ $t('domain_mute_card.mute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
slot="item"
|
||||
slot-scope="{item}"
|
||||
>
|
||||
<MuteCard :user-id="item" />
|
||||
</template>
|
||||
<template slot="empty">
|
||||
{{ $t('settings.no_mutes') }}
|
||||
</template>
|
||||
</MuteList>
|
||||
<DomainMuteList
|
||||
:refresh="true"
|
||||
:get-key="identity"
|
||||
>
|
||||
<template
|
||||
slot="header"
|
||||
slot-scope="{selected}"
|
||||
>
|
||||
<div class="profile-edit-bulk-actions">
|
||||
<ProgressButton
|
||||
v-if="selected.length > 0"
|
||||
class="btn btn-default"
|
||||
:click="() => unmuteDomains(selected)"
|
||||
>
|
||||
{{ $t('domain_mute_card.unmute') }}
|
||||
<template slot="progress">
|
||||
{{ $t('domain_mute_card.unmute_progress') }}
|
||||
</template>
|
||||
</ProgressButton>
|
||||
</div>
|
||||
</template>
|
||||
<template
|
||||
slot="item"
|
||||
slot-scope="{item}"
|
||||
>
|
||||
<DomainMuteCard :domain="item" />
|
||||
</template>
|
||||
<template slot="empty">
|
||||
{{ $t('settings.no_mutes') }}
|
||||
</template>
|
||||
</DomainMuteList>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</div>
|
||||
</tab-switcher>
|
||||
</div>
|
||||
|
@ -639,6 +694,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
&-domain-mute-form {
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
button {
|
||||
align-self: flex-end;
|
||||
margin-top: 1em;
|
||||
width: 10em;
|
||||
}
|
||||
}
|
||||
|
||||
.setting-subitem {
|
||||
margin-left: 1.75em;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,12 @@
|
|||
"chat": {
|
||||
"title": "Chat"
|
||||
},
|
||||
"domain_mute_card": {
|
||||
"mute": "Mute",
|
||||
"mute_progress": "Muting...",
|
||||
"unmute": "Unmute",
|
||||
"unmute_progress": "Unmuting..."
|
||||
},
|
||||
"exporter": {
|
||||
"export": "Export",
|
||||
"processing": "Processing, you'll soon be asked to download your file"
|
||||
|
@ -111,7 +117,8 @@
|
|||
"notifications": "Notifications",
|
||||
"read": "Read!",
|
||||
"repeated_you": "repeated your status",
|
||||
"no_more_notifications": "No more notifications"
|
||||
"no_more_notifications": "No more notifications",
|
||||
"migrated_to": "migrated to"
|
||||
},
|
||||
"polls": {
|
||||
"add_poll": "Add Poll",
|
||||
|
@ -141,6 +148,7 @@
|
|||
"interactions": {
|
||||
"favs_repeats": "Repeats and Favorites",
|
||||
"follows": "New follows",
|
||||
"moves": "User migrates",
|
||||
"load_older": "Load older interactions"
|
||||
},
|
||||
"post_status": {
|
||||
|
@ -263,6 +271,7 @@
|
|||
"delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.",
|
||||
"delete_account_instructions": "Type your password in the input below to confirm account deletion.",
|
||||
"discoverable": "Allow discovery of this account in search results and other services",
|
||||
"domain_mutes": "Domains",
|
||||
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
|
||||
"pad_emoji": "Pad emoji with spaces when adding from picker",
|
||||
"export_theme": "Save preset",
|
||||
|
@ -313,6 +322,7 @@
|
|||
"notification_visibility_likes": "Likes",
|
||||
"notification_visibility_mentions": "Mentions",
|
||||
"notification_visibility_repeats": "Repeats",
|
||||
"notification_visibility_moves": "User Migrates",
|
||||
"no_rich_text_description": "Strip rich text formatting from all posts",
|
||||
"no_blocks": "No blocks",
|
||||
"no_mutes": "No mutes",
|
||||
|
@ -360,6 +370,7 @@
|
|||
"post_status_content_type": "Post status content type",
|
||||
"stop_gifs": "Play-on-hover GIFs",
|
||||
"streaming": "Enable automatic streaming of new posts when scrolled to the top",
|
||||
"user_mutes": "Users",
|
||||
"useStreamingApi": "Receive posts and notifications real-time",
|
||||
"useStreamingApiWarning": "(Not recommended, experimental, known to skip posts)",
|
||||
"text": "Text",
|
||||
|
@ -368,6 +379,7 @@
|
|||
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
|
||||
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
|
||||
"tooltipRadius": "Tooltips/alerts",
|
||||
"type_domains_to_mute": "Type in domains to mute",
|
||||
"upload_a_photo": "Upload a photo",
|
||||
"user_settings": "User Settings",
|
||||
"values": {
|
||||
|
@ -667,6 +679,7 @@
|
|||
"repeat": "Repeat",
|
||||
"reply": "Reply",
|
||||
"favorite": "Favorite",
|
||||
"add_reaction": "Add Reaction",
|
||||
"user_settings": "User Settings"
|
||||
},
|
||||
"upload":{
|
||||
|
|
9
src/lib/event_target_polyfill.js
Normal file
9
src/lib/event_target_polyfill.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
import EventTargetPolyfill from '@ungap/event-target'
|
||||
|
||||
try {
|
||||
/* eslint-disable no-new */
|
||||
new EventTarget()
|
||||
/* eslint-enable no-new */
|
||||
} catch (e) {
|
||||
window.EventTarget = EventTargetPolyfill
|
||||
}
|
|
@ -2,6 +2,9 @@ import Vue from 'vue'
|
|||
import VueRouter from 'vue-router'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
import 'custom-event-polyfill'
|
||||
import './lib/event_target_polyfill.js'
|
||||
|
||||
import interfaceModule from './modules/interface.js'
|
||||
import instanceModule from './modules/instance.js'
|
||||
import statusesModule from './modules/statuses.js'
|
||||
|
|
|
@ -146,6 +146,7 @@ const api = {
|
|||
startFetchingFollowRequests (store) {
|
||||
if (store.state.fetchers['followRequests']) return
|
||||
const fetcher = store.state.backendInteractor.startFetchingFollowRequests({ store })
|
||||
|
||||
store.commit('addFetcher', { fetcherName: 'followRequests', fetcher })
|
||||
},
|
||||
stopFetchingFollowRequests (store) {
|
||||
|
|
|
@ -31,7 +31,8 @@ export const defaultState = {
|
|||
follows: true,
|
||||
mentions: true,
|
||||
likes: true,
|
||||
repeats: true
|
||||
repeats: true,
|
||||
moves: true
|
||||
},
|
||||
webPushNotifications: false,
|
||||
muteWords: [],
|
||||
|
|
|
@ -1,4 +1,17 @@
|
|||
import { remove, slice, each, findIndex, find, maxBy, minBy, merge, first, last, isArray, omitBy } from 'lodash'
|
||||
import {
|
||||
remove,
|
||||
slice,
|
||||
each,
|
||||
findIndex,
|
||||
find,
|
||||
maxBy,
|
||||
minBy,
|
||||
merge,
|
||||
first,
|
||||
last,
|
||||
isArray,
|
||||
omitBy
|
||||
} from 'lodash'
|
||||
import { set } from 'vue'
|
||||
import apiService from '../services/api/api.service.js'
|
||||
// import parse from '../services/status_parser/status_parser.js'
|
||||
|
@ -67,7 +80,8 @@ const visibleNotificationTypes = (rootState) => {
|
|||
rootState.config.notificationVisibility.likes && 'like',
|
||||
rootState.config.notificationVisibility.mentions && 'mention',
|
||||
rootState.config.notificationVisibility.repeats && 'repeat',
|
||||
rootState.config.notificationVisibility.follows && 'follow'
|
||||
rootState.config.notificationVisibility.follows && 'follow',
|
||||
rootState.config.notificationVisibility.moves && 'move'
|
||||
].filter(_ => _)
|
||||
}
|
||||
|
||||
|
@ -306,7 +320,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
|||
|
||||
const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => {
|
||||
each(notifications, (notification) => {
|
||||
if (notification.type !== 'follow') {
|
||||
if (notification.type !== 'follow' && notification.type !== 'move') {
|
||||
notification.action = addStatusToGlobalStorage(state, notification.action).item
|
||||
notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
|
||||
}
|
||||
|
@ -339,6 +353,9 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
|
|||
case 'follow':
|
||||
i18nString = 'followed_you'
|
||||
break
|
||||
case 'move':
|
||||
i18nString = 'migrated_to'
|
||||
break
|
||||
}
|
||||
|
||||
if (i18nString) {
|
||||
|
@ -514,6 +531,50 @@ export const mutations = {
|
|||
newStatus.fave_num = newStatus.favoritedBy.length
|
||||
newStatus.favorited = !!newStatus.favoritedBy.find(({ id }) => currentUser.id === id)
|
||||
},
|
||||
addEmojiReactionsBy (state, { id, emojiReactions, currentUser }) {
|
||||
const status = state.allStatusesObject[id]
|
||||
set(status, 'emoji_reactions', emojiReactions)
|
||||
},
|
||||
addOwnReaction (state, { id, emoji, currentUser }) {
|
||||
const status = state.allStatusesObject[id]
|
||||
const reactionIndex = findIndex(status.emoji_reactions, { emoji })
|
||||
const reaction = status.emoji_reactions[reactionIndex] || { emoji, count: 0, accounts: [] }
|
||||
|
||||
const newReaction = {
|
||||
...reaction,
|
||||
count: reaction.count + 1,
|
||||
accounts: [
|
||||
...reaction.accounts,
|
||||
currentUser
|
||||
]
|
||||
}
|
||||
|
||||
// Update count of existing reaction if it exists, otherwise append at the end
|
||||
if (reactionIndex >= 0) {
|
||||
set(status.emoji_reactions, reactionIndex, newReaction)
|
||||
} else {
|
||||
set(status, 'emoji_reactions', [...status.emoji_reactions, newReaction])
|
||||
}
|
||||
},
|
||||
removeOwnReaction (state, { id, emoji, currentUser }) {
|
||||
const status = state.allStatusesObject[id]
|
||||
const reactionIndex = findIndex(status.emoji_reactions, { emoji })
|
||||
if (reactionIndex < 0) return
|
||||
|
||||
const reaction = status.emoji_reactions[reactionIndex]
|
||||
|
||||
const newReaction = {
|
||||
...reaction,
|
||||
count: reaction.count - 1,
|
||||
accounts: reaction.accounts.filter(acc => acc.id === currentUser.id)
|
||||
}
|
||||
|
||||
if (newReaction.count > 0) {
|
||||
set(status.emoji_reactions, reactionIndex, newReaction)
|
||||
} else {
|
||||
set(status, 'emoji_reactions', status.emoji_reactions.filter(r => r.emoji !== emoji))
|
||||
}
|
||||
},
|
||||
updateStatusWithPoll (state, { id, poll }) {
|
||||
const status = state.allStatusesObject[id]
|
||||
status.poll = poll
|
||||
|
@ -618,6 +679,31 @@ const statuses = {
|
|||
commit('addRepeats', { id, rebloggedByUsers, currentUser: rootState.users.currentUser })
|
||||
})
|
||||
},
|
||||
reactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) {
|
||||
const currentUser = rootState.users.currentUser
|
||||
commit('addOwnReaction', { id, emoji, currentUser })
|
||||
rootState.api.backendInteractor.reactWithEmoji({ id, emoji }).then(
|
||||
status => {
|
||||
dispatch('fetchEmojiReactionsBy', id)
|
||||
}
|
||||
)
|
||||
},
|
||||
unreactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) {
|
||||
const currentUser = rootState.users.currentUser
|
||||
commit('removeOwnReaction', { id, emoji, currentUser })
|
||||
rootState.api.backendInteractor.unreactWithEmoji({ id, emoji }).then(
|
||||
status => {
|
||||
dispatch('fetchEmojiReactionsBy', id)
|
||||
}
|
||||
)
|
||||
},
|
||||
fetchEmojiReactionsBy ({ rootState, commit }, id) {
|
||||
rootState.api.backendInteractor.fetchEmojiReactions({ id }).then(
|
||||
emojiReactions => {
|
||||
commit('addEmojiReactionsBy', { id, emojiReactions, currentUser: rootState.users.currentUser })
|
||||
}
|
||||
)
|
||||
},
|
||||
fetchFavs ({ rootState, commit }, id) {
|
||||
rootState.api.backendInteractor.fetchFavoritedByUsers({ id })
|
||||
.then(favoritedByUsers => commit('addFavs', { id, favoritedByUsers, currentUser: rootState.users.currentUser }))
|
||||
|
|
|
@ -72,6 +72,16 @@ const showReblogs = (store, userId) => {
|
|||
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
|
||||
}
|
||||
|
||||
const muteDomain = (store, domain) => {
|
||||
return store.rootState.api.backendInteractor.muteDomain({ domain })
|
||||
.then(() => store.commit('addDomainMute', domain))
|
||||
}
|
||||
|
||||
const unmuteDomain = (store, domain) => {
|
||||
return store.rootState.api.backendInteractor.unmuteDomain({ domain })
|
||||
.then(() => store.commit('removeDomainMute', domain))
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
setMuted (state, { user: { id }, muted }) {
|
||||
const user = state.usersObject[id]
|
||||
|
@ -177,6 +187,20 @@ export const mutations = {
|
|||
state.currentUser.muteIds.push(muteId)
|
||||
}
|
||||
},
|
||||
saveDomainMutes (state, domainMutes) {
|
||||
state.currentUser.domainMutes = domainMutes
|
||||
},
|
||||
addDomainMute (state, domain) {
|
||||
if (state.currentUser.domainMutes.indexOf(domain) === -1) {
|
||||
state.currentUser.domainMutes.push(domain)
|
||||
}
|
||||
},
|
||||
removeDomainMute (state, domain) {
|
||||
const index = state.currentUser.domainMutes.indexOf(domain)
|
||||
if (index !== -1) {
|
||||
state.currentUser.domainMutes.splice(index, 1)
|
||||
}
|
||||
},
|
||||
setPinnedToUser (state, status) {
|
||||
const user = state.usersObject[status.user.id]
|
||||
const index = user.pinnedStatusIds.indexOf(status.id)
|
||||
|
@ -297,6 +321,25 @@ const users = {
|
|||
unmuteUsers (store, ids = []) {
|
||||
return Promise.all(ids.map(id => unmuteUser(store, id)))
|
||||
},
|
||||
fetchDomainMutes (store) {
|
||||
return store.rootState.api.backendInteractor.fetchDomainMutes()
|
||||
.then((domainMutes) => {
|
||||
store.commit('saveDomainMutes', domainMutes)
|
||||
return domainMutes
|
||||
})
|
||||
},
|
||||
muteDomain (store, domain) {
|
||||
return muteDomain(store, domain)
|
||||
},
|
||||
unmuteDomain (store, domain) {
|
||||
return unmuteDomain(store, domain)
|
||||
},
|
||||
muteDomains (store, domains = []) {
|
||||
return Promise.all(domains.map(domain => muteDomain(store, domain)))
|
||||
},
|
||||
unmuteDomains (store, domain = []) {
|
||||
return Promise.all(domain.map(domain => unmuteDomain(store, domain)))
|
||||
},
|
||||
fetchFriends ({ rootState, commit }, id) {
|
||||
const user = rootState.users.usersObject[id]
|
||||
const maxId = last(user.friendIds)
|
||||
|
@ -373,8 +416,10 @@ const users = {
|
|||
},
|
||||
addNewNotifications (store, { notifications }) {
|
||||
const users = map(notifications, 'from_profile')
|
||||
const targetUsers = map(notifications, 'target')
|
||||
const notificationIds = notifications.map(_ => _.id)
|
||||
store.commit('addNewUsers', users)
|
||||
store.commit('addNewUsers', targetUsers)
|
||||
|
||||
const notificationsObject = store.rootState.statuses.notifications.idStore
|
||||
const relevantNotifications = Object.entries(notificationsObject)
|
||||
|
@ -399,7 +444,9 @@ const users = {
|
|||
let rootState = store.rootState
|
||||
|
||||
try {
|
||||
let data = await rootState.api.backendInteractor.register({ ...userInfo })
|
||||
let data = await rootState.api.backendInteractor.register(
|
||||
{ params: { ...userInfo } }
|
||||
)
|
||||
store.commit('signUpSuccess')
|
||||
store.commit('setToken', data.access_token)
|
||||
store.dispatch('loginUser', data.access_token)
|
||||
|
@ -456,6 +503,7 @@ const users = {
|
|||
user.credentials = accessToken
|
||||
user.blockIds = []
|
||||
user.muteIds = []
|
||||
user.domainMutes = []
|
||||
commit('setCurrentUser', user)
|
||||
commit('addNewUsers', [user])
|
||||
|
||||
|
|
|
@ -72,7 +72,11 @@ const MASTODON_MUTE_CONVERSATION = id => `/api/v1/statuses/${id}/mute`
|
|||
const MASTODON_UNMUTE_CONVERSATION = id => `/api/v1/statuses/${id}/unmute`
|
||||
const MASTODON_SEARCH_2 = `/api/v2/search`
|
||||
const MASTODON_USER_SEARCH_URL = '/api/v1/accounts/search'
|
||||
const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
|
||||
const MASTODON_STREAMING = '/api/v1/streaming'
|
||||
const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/emoji_reactions_by`
|
||||
const PLEROMA_EMOJI_REACT_URL = id => `/api/v1/pleroma/statuses/${id}/react_with_emoji`
|
||||
const PLEROMA_EMOJI_UNREACT_URL = id => `/api/v1/pleroma/statuses/${id}/unreact_with_emoji`
|
||||
|
||||
const oldfetch = window.fetch
|
||||
|
||||
|
@ -880,6 +884,28 @@ const fetchRebloggedByUsers = ({ id }) => {
|
|||
return promisedRequest({ url: MASTODON_STATUS_REBLOGGEDBY_URL(id) }).then((users) => users.map(parseUser))
|
||||
}
|
||||
|
||||
const fetchEmojiReactions = ({ id }) => {
|
||||
return promisedRequest({ url: PLEROMA_EMOJI_REACTIONS_URL(id) })
|
||||
}
|
||||
|
||||
const reactWithEmoji = ({ id, emoji, credentials }) => {
|
||||
return promisedRequest({
|
||||
url: PLEROMA_EMOJI_REACT_URL(id),
|
||||
method: 'POST',
|
||||
credentials,
|
||||
payload: { emoji }
|
||||
}).then(parseStatus)
|
||||
}
|
||||
|
||||
const unreactWithEmoji = ({ id, emoji, credentials }) => {
|
||||
return promisedRequest({
|
||||
url: PLEROMA_EMOJI_UNREACT_URL(id),
|
||||
method: 'POST',
|
||||
credentials,
|
||||
payload: { emoji }
|
||||
}).then(parseStatus)
|
||||
}
|
||||
|
||||
const reportUser = ({ credentials, userId, statusIds, comment, forward }) => {
|
||||
return promisedRequest({
|
||||
url: MASTODON_REPORT_USER_URL,
|
||||
|
@ -948,6 +974,28 @@ const search2 = ({ credentials, q, resolve, limit, offset, following }) => {
|
|||
})
|
||||
}
|
||||
|
||||
const fetchDomainMutes = ({ credentials }) => {
|
||||
return promisedRequest({ url: MASTODON_DOMAIN_BLOCKS_URL, credentials })
|
||||
}
|
||||
|
||||
const muteDomain = ({ domain, credentials }) => {
|
||||
return promisedRequest({
|
||||
url: MASTODON_DOMAIN_BLOCKS_URL,
|
||||
method: 'POST',
|
||||
payload: { domain },
|
||||
credentials
|
||||
})
|
||||
}
|
||||
|
||||
const unmuteDomain = ({ domain, credentials }) => {
|
||||
return promisedRequest({
|
||||
url: MASTODON_DOMAIN_BLOCKS_URL,
|
||||
method: 'DELETE',
|
||||
payload: { domain },
|
||||
credentials
|
||||
})
|
||||
}
|
||||
|
||||
export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
|
||||
return Object.entries({
|
||||
...(credentials
|
||||
|
@ -1107,10 +1155,16 @@ const apiService = {
|
|||
fetchPoll,
|
||||
fetchFavoritedByUsers,
|
||||
fetchRebloggedByUsers,
|
||||
fetchEmojiReactions,
|
||||
reactWithEmoji,
|
||||
unreactWithEmoji,
|
||||
reportUser,
|
||||
updateNotificationSettings,
|
||||
search2,
|
||||
searchUsers
|
||||
searchUsers,
|
||||
fetchDomainMutes,
|
||||
muteDomain,
|
||||
unmuteDomain
|
||||
}
|
||||
|
||||
export default apiService
|
||||
|
|
|
@ -16,7 +16,7 @@ const backendInteractorService = credentials => ({
|
|||
return notificationsFetcher.fetchAndUpdate({ store, credentials })
|
||||
},
|
||||
|
||||
startFetchingFollowRequest ({ store }) {
|
||||
startFetchingFollowRequests ({ store }) {
|
||||
return followRequestFetcher.startFetching({ store, credentials })
|
||||
},
|
||||
|
||||
|
|
|
@ -242,6 +242,7 @@ export const parseStatus = (data) => {
|
|||
output.is_local = pleroma.local
|
||||
output.in_reply_to_screen_name = data.pleroma.in_reply_to_account_acct
|
||||
output.thread_muted = pleroma.thread_muted
|
||||
output.emoji_reactions = pleroma.emoji_reactions
|
||||
} else {
|
||||
output.text = data.content
|
||||
output.summary = data.spoiler_text
|
||||
|
@ -341,10 +342,13 @@ export const parseNotification = (data) => {
|
|||
if (masto) {
|
||||
output.type = mastoDict[data.type] || data.type
|
||||
output.seen = data.pleroma.is_seen
|
||||
output.status = output.type === 'follow'
|
||||
output.status = output.type === 'follow' || output.type === 'move'
|
||||
? null
|
||||
: parseStatus(data.status)
|
||||
output.action = output.status // TODO: Refactor, this is unneeded
|
||||
output.target = output.type !== 'move'
|
||||
? null
|
||||
: parseUser(data.target)
|
||||
output.from_profile = parseUser(data.account)
|
||||
} else {
|
||||
const parsedNotice = parseStatus(data.notice)
|
||||
|
|
|
@ -32,12 +32,18 @@ export class RegistrationError extends Error {
|
|||
}
|
||||
|
||||
if (typeof error === 'object') {
|
||||
const errorContents = JSON.parse(error.error)
|
||||
// keys will have the property that has the error, for example 'ap_id',
|
||||
// 'email' or 'captcha', the value will be an array of its error
|
||||
// like "ap_id": ["has been taken"] or "captcha": ["Invalid CAPTCHA"]
|
||||
|
||||
// replace ap_id with username
|
||||
if (error.ap_id) {
|
||||
error.username = error.ap_id
|
||||
delete error.ap_id
|
||||
if (errorContents.ap_id) {
|
||||
errorContents.username = errorContents.ap_id
|
||||
delete errorContents.ap_id
|
||||
}
|
||||
this.message = humanizeErrors(error)
|
||||
|
||||
this.message = humanizeErrors(errorContents)
|
||||
} else {
|
||||
this.message = error
|
||||
}
|
||||
|
|
|
@ -6,7 +6,8 @@ export const visibleTypes = store => ([
|
|||
store.state.config.notificationVisibility.likes && 'like',
|
||||
store.state.config.notificationVisibility.mentions && 'mention',
|
||||
store.state.config.notificationVisibility.repeats && 'repeat',
|
||||
store.state.config.notificationVisibility.follows && 'follow'
|
||||
store.state.config.notificationVisibility.follows && 'follow',
|
||||
store.state.config.notificationVisibility.moves && 'move'
|
||||
].filter(_ => _))
|
||||
|
||||
const sortById = (a, b) => {
|
||||
|
@ -25,7 +26,7 @@ const sortById = (a, b) => {
|
|||
}
|
||||
}
|
||||
|
||||
export const visibleNotificationsFromStore = (store, types) => {
|
||||
export const filteredNotificationsFromStore = (store, types) => {
|
||||
// map is just to clone the array since sort mutates it and it causes some issues
|
||||
let sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById)
|
||||
sortedNotifications = sortBy(sortedNotifications, 'seen')
|
||||
|
@ -35,4 +36,4 @@ export const visibleNotificationsFromStore = (store, types) => {
|
|||
}
|
||||
|
||||
export const unseenNotificationsFromStore = store =>
|
||||
filter(visibleNotificationsFromStore(store), ({ seen }) => !seen)
|
||||
filter(filteredNotificationsFromStore(store), ({ seen }) => !seen)
|
||||
|
|
|
@ -2,7 +2,6 @@ import apiService from '../api/api.service.js'
|
|||
|
||||
const update = ({ store, notifications, older }) => {
|
||||
store.dispatch('setNotificationsError', { value: false })
|
||||
|
||||
store.dispatch('addNewNotifications', { notifications, older })
|
||||
}
|
||||
|
||||
|
@ -30,9 +29,9 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
|
|||
|
||||
// load unread notifications repeatedly to provide consistency between browser tabs
|
||||
const notifications = timelineData.data
|
||||
const unread = notifications.filter(n => !n.seen).map(n => n.id)
|
||||
if (unread.length) {
|
||||
args['since'] = Math.min(...unread)
|
||||
const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id)
|
||||
if (readNotifsIds.length) {
|
||||
args['since'] = Math.max(...readNotifsIds)
|
||||
fetchNotifications({ store, args, older })
|
||||
}
|
||||
|
||||
|
|
|
@ -65,7 +65,8 @@ function sendSubscriptionToBackEnd (subscription, token, notificationVisibility)
|
|||
follow: notificationVisibility.follows,
|
||||
favourite: notificationVisibility.likes,
|
||||
mention: notificationVisibility.mentions,
|
||||
reblog: notificationVisibility.repeats
|
||||
reblog: notificationVisibility.repeats,
|
||||
move: notificationVisibility.moves
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -333,6 +333,12 @@
|
|||
"css": "login",
|
||||
"code": 59424,
|
||||
"src": "fontawesome"
|
||||
},
|
||||
{
|
||||
"uid": "f3ebd6751c15a280af5cc5f4a764187d",
|
||||
"css": "arrow-curved",
|
||||
"code": 59426,
|
||||
"src": "iconic"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"pleroma-dark": [ "Pleroma Dark", "#121a24", "#182230", "#b9b9ba", "#d8a070", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
|
||||
"pleroma-light": [ "Pleroma Light", "#f2f4f6", "#dbe0e8", "#304055", "#f86f0f", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
|
||||
"pleroma-amoled": [ "Pleroma Dark AMOLED", "#000000", "#111111", "#b0b0b1", "#d8a070", "#aa0000", "#0fa00f", "#0095ff", "#d59500"],
|
||||
"classic-dark": [ "Classic Dark", "#161c20", "#282e32", "#b9b9b9", "#baaa9c", "#d31014", "#0fa00f", "#0095ff", "#ffa500" ],
|
||||
"bird": [ "Bird", "#f8fafd", "#e6ecf0", "#14171a", "#0084b8", "#e0245e", "#17bf63", "#1b95e0", "#fab81e"],
|
||||
"ir-black": [ "Ir Black", "#000000", "#242422", "#b5b3aa", "#ff6c60", "#FF6C60", "#A8FF60", "#96CBFE", "#FFFFB6" ],
|
||||
|
|
|
@ -241,6 +241,51 @@ describe('Statuses module', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('emojiReactions', () => {
|
||||
it('increments count in existing reaction', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({ id: '1' })
|
||||
status.emoji_reactions = [ { emoji: '😂', count: 1, accounts: [] } ]
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||
mutations.addOwnReaction(state, { id: '1', emoji: '😂', currentUser: { id: 'me' } })
|
||||
expect(state.allStatusesObject['1'].emoji_reactions[0].count).to.eql(2)
|
||||
expect(state.allStatusesObject['1'].emoji_reactions[0].accounts[0].id).to.eql('me')
|
||||
})
|
||||
|
||||
it('adds a new reaction', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({ id: '1' })
|
||||
status.emoji_reactions = []
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||
mutations.addOwnReaction(state, { id: '1', emoji: '😂', currentUser: { id: 'me' } })
|
||||
expect(state.allStatusesObject['1'].emoji_reactions[0].count).to.eql(1)
|
||||
expect(state.allStatusesObject['1'].emoji_reactions[0].accounts[0].id).to.eql('me')
|
||||
})
|
||||
|
||||
it('decreases count in existing reaction', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({ id: '1' })
|
||||
status.emoji_reactions = [ { emoji: '😂', count: 2, accounts: [{ id: 'me' }] } ]
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||
mutations.removeOwnReaction(state, { id: '1', emoji: '😂', currentUser: {} })
|
||||
expect(state.allStatusesObject['1'].emoji_reactions[0].count).to.eql(1)
|
||||
expect(state.allStatusesObject['1'].emoji_reactions[0].accounts).to.eql([])
|
||||
})
|
||||
|
||||
it('removes a reaction', () => {
|
||||
const state = defaultState()
|
||||
const status = makeMockStatus({ id: '1' })
|
||||
status.emoji_reactions = [{ emoji: '😂', count: 1, accounts: [{ id: 'me' }] }]
|
||||
|
||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||
mutations.removeOwnReaction(state, { id: '1', emoji: '😂', currentUser: {} })
|
||||
expect(state.allStatusesObject['1'].emoji_reactions.length).to.eql(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('showNewStatuses', () => {
|
||||
it('resets the minId to the min of the visible statuses when adding new to visible statuses', () => {
|
||||
const state = defaultState()
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as NotificationUtils from 'src/services/notification_utils/notification_utils.js'
|
||||
|
||||
describe('NotificationUtils', () => {
|
||||
describe('visibleNotificationsFromStore', () => {
|
||||
describe('filteredNotificationsFromStore', () => {
|
||||
it('should return sorted notifications with configured types', () => {
|
||||
const store = {
|
||||
state: {
|
||||
|
@ -47,7 +47,7 @@ describe('NotificationUtils', () => {
|
|||
type: 'like'
|
||||
}
|
||||
]
|
||||
expect(NotificationUtils.visibleNotificationsFromStore(store)).to.eql(expected)
|
||||
expect(NotificationUtils.filteredNotificationsFromStore(store)).to.eql(expected)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -710,6 +710,11 @@
|
|||
dependencies:
|
||||
qrcode "^1.3.0"
|
||||
|
||||
"@ungap/event-target@^0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@ungap/event-target/-/event-target-0.1.0.tgz#88d527d40de86c4b0c99a060ca241d755999915b"
|
||||
integrity sha512-W2oyj0Fe1w/XhPZjkI3oUcDUAmu5P4qsdT2/2S8aMhtAWM/CE/jYWtji0pKNPDfxLI75fa5gWSEmnynKMNP/oA==
|
||||
|
||||
"@vue/babel-helper-vue-jsx-merge-props@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0.tgz#048fe579958da408fb7a8b2a3ec050b50a661040"
|
||||
|
@ -2281,6 +2286,11 @@ currently-unhandled@^0.4.1:
|
|||
dependencies:
|
||||
array-find-index "^1.0.1"
|
||||
|
||||
custom-event-polyfill@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/custom-event-polyfill/-/custom-event-polyfill-1.0.7.tgz#9bc993ddda937c1a30ccd335614c6c58c4f87aee"
|
||||
integrity sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==
|
||||
|
||||
custom-event@~1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
|
||||
|
|
Loading…
Reference in a new issue