diff options
Diffstat (limited to 'src')
42 files changed, 754 insertions, 100 deletions
diff --git a/src/boot/after_store.js b/src/boot/after_store.js index f61e9252..86874e47 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -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) } diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 08283fff..45fb2bf6 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -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 diff --git a/src/components/domain_mute_card/domain_mute_card.js b/src/components/domain_mute_card/domain_mute_card.js new file mode 100644 index 00000000..c8e838ba --- /dev/null +++ b/src/components/domain_mute_card/domain_mute_card.js @@ -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 diff --git a/src/components/domain_mute_card/domain_mute_card.vue b/src/components/domain_mute_card/domain_mute_card.vue new file mode 100644 index 00000000..567d81c5 --- /dev/null +++ b/src/components/domain_mute_card/domain_mute_card.vue @@ -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> diff --git a/src/components/emoji_reactions/emoji_reactions.js b/src/components/emoji_reactions/emoji_reactions.js new file mode 100644 index 00000000..95d52cb6 --- /dev/null +++ b/src/components/emoji_reactions/emoji_reactions.js @@ -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 diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue new file mode 100644 index 00000000..00d6d2b7 --- /dev/null +++ b/src/components/emoji_reactions/emoji_reactions.vue @@ -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> diff --git a/src/components/interactions/interactions.js b/src/components/interactions/interactions.js index 1f8a9de9..cc31ff20 100644 --- a/src/components/interactions/interactions.js +++ b/src/components/interactions/interactions.js @@ -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 = { diff --git a/src/components/interactions/interactions.vue b/src/components/interactions/interactions.vue index 08cee343..a2e252ab 100644 --- a/src/components/interactions/interactions.vue +++ b/src/components/interactions/interactions.vue @@ -21,6 +21,10 @@ key="follows" :label="$t('interactions.follows')" /> + <span + key="moves" + :label="$t('interactions.moves')" + /> </tab-switcher> <Notifications ref="notifications" diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index d9268585..8f7edb7f 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -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({ diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index a934a411..8cd04dc7 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -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> diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index 7d46eb5a..e7bd769e 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -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 } diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 1f192c77..16124e50 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -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" diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index a89c0cdc..26ffbab6 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -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 }) } } diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss index ca762432..64ded936 100644 --- a/src/components/notifications/notifications.scss +++ b/src/components/notifications/notifications.scss @@ -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; diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue index c42c35e6..d477a41b 100644 --- a/src/components/notifications/notifications.vue +++ b/src/components/notifications/notifications.vue @@ -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}" diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js new file mode 100644 index 00000000..6fb2a780 --- /dev/null +++ b/src/components/react_button/react_button.js @@ -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 diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue new file mode 100644 index 00000000..c925dd71 --- /dev/null +++ b/src/components/react_button/react_button.vue @@ -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> diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js index 57f3caf0..ace8cc7c 100644 --- a/src/components/registration/registration.js +++ b/src/components/registration/registration.js @@ -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() } } }, diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue index 222b67a8..fdbda007 100644 --- a/src/components/registration/registration.vue +++ b/src/components/registration/registration.vue @@ -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 diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index e118dbcc..22e14a89 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -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> diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index 2534eb8f..2181ecc7 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -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 }, diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index 25e4de01..df1d22a4 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -88,7 +88,7 @@ </router-link> </li> <li - v-if="federating && !privateMode" + v-if="federating && (currentUser || !privateMode)" @click="toggleDrawer" > <router-link to="/main/all"> diff --git a/src/components/staff_panel/staff_panel.js b/src/components/staff_panel/staff_panel.js index 93e950ad..4f98fff6 100644 --- a/src/components/staff_panel/staff_panel.js +++ b/src/components/staff_panel/staff_panel.js @@ -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(_ => _) } } } diff --git a/src/components/status/status.js b/src/components/status/status.js index c49e729c..81b57667 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -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) { diff --git a/src/components/status/status.vue b/src/components/status/status.vue index b1048832..a326874e 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -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" diff --git a/src/components/user_settings/mfa.js b/src/components/user_settings/mfa.js index 3090138a..abf37062 100644 --- a/src/components/user_settings/mfa.js +++ b/src/components/user_settings/mfa.js @@ -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 diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js index d5d671e4..38373056 100644 --- a/src/components/user_settings/user_settings.js +++ b/src/components/user_settings/user_settings.js @@ -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 } diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue index 3f1982a6..2222c293 100644 --- a/src/components/user_settings/user_settings.vue +++ b/src/components/user_settings/user_settings.vue @@ -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')" + > + <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)" + > + {{ $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" > - {{ $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)" + :click="muteDomain" > - {{ $t('user_card.unmute') }} + {{ $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; } diff --git a/src/i18n/en.json b/src/i18n/en.json index 06cc12f0..4efa41c3 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -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":{ diff --git a/src/lib/event_target_polyfill.js b/src/lib/event_target_polyfill.js new file mode 100644 index 00000000..2042c770 --- /dev/null +++ b/src/lib/event_target_polyfill.js @@ -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 +} diff --git a/src/main.js b/src/main.js index a9db1cff..baf73ac8 100644 --- a/src/main.js +++ b/src/main.js @@ -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' diff --git a/src/modules/api.js b/src/modules/api.js index 9c296275..748570e5 100644 --- a/src/modules/api.js +++ b/src/modules/api.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) { diff --git a/src/modules/config.js b/src/modules/config.js index ee474b16..eb763c10 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -31,7 +31,8 @@ export const defaultState = { follows: true, mentions: true, likes: true, - repeats: true + repeats: true, + moves: true }, webPushNotifications: false, muteWords: [], diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 7d88761c..ea0c1749 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -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 })) diff --git a/src/modules/users.js b/src/modules/users.js index e54588df..ce3e595d 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -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]) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index ef0267aa..11aa0675 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -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 diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index b7372ed0..e1c32860 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -16,7 +16,7 @@ const backendInteractorService = credentials => ({ return notificationsFetcher.fetchAndUpdate({ store, credentials }) }, - startFetchingFollowRequest ({ store }) { + startFetchingFollowRequests ({ store }) { return followRequestFetcher.startFetching({ store, credentials }) }, diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index ca79df6f..a3d0b782 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -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) diff --git a/src/services/errors/errors.js b/src/services/errors/errors.js index 590552da..d4cf9132 100644 --- a/src/services/errors/errors.js +++ b/src/services/errors/errors.js @@ -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 } diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js index 7021adbd..860620fc 100644 --- a/src/services/notification_utils/notification_utils.js +++ b/src/services/notification_utils/notification_utils.js @@ -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) diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index 47008026..64499a1b 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -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 }) } diff --git a/src/services/push/push.js b/src/services/push/push.js index 1b189a29..5836fc26 100644 --- a/src/services/push/push.js +++ b/src/services/push/push.js @@ -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 } } }) |
