From 6af870cd9069a8fc45b7684d264656dad0cf4a70 Mon Sep 17 00:00:00 2001 From: kPherox Date: Wed, 11 Dec 2019 00:00:10 +0900 Subject: Add view for moves notifications --- src/services/notification_utils/notification_utils.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src/services/notification_utils/notification_utils.js') diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js index 7021adbd..b08514da 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) => { -- cgit v1.2.3-70-g09d2 From 86380f042976557d5260a3f5c2de0a9b0bcdbac6 Mon Sep 17 00:00:00 2001 From: Shpuld Shpludson Date: Tue, 14 Jan 2020 13:28:57 +0000 Subject: Optimize Notifications Rendering --- CHANGELOG.md | 2 ++ src/components/notifications/notifications.js | 27 ++++++++++++++++++---- src/components/notifications/notifications.vue | 2 +- .../notification_utils/notification_utils.js | 4 ++-- .../notification_utils/notification_utils.spec.js | 4 ++-- 5 files changed, 30 insertions(+), 9 deletions(-) (limited to 'src/services/notification_utils/notification_utils.js') diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e17dc64..9f5a9305 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Changed +- Notifications column now cleans itself up to optimize performance when tab is left open for a long time ### Fixed - Single notifications left unread when hitting read on another device/tab 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.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 @@
{ } } -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') @@ -36,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/test/unit/specs/services/notification_utils/notification_utils.spec.js b/test/unit/specs/services/notification_utils/notification_utils.spec.js index 1baa5fc9..00628d83 100644 --- a/test/unit/specs/services/notification_utils/notification_utils.spec.js +++ b/test/unit/specs/services/notification_utils/notification_utils.spec.js @@ -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) }) }) -- cgit v1.2.3-70-g09d2 From f6b482be515ea4f0281050f71296ffe0ec2ab305 Mon Sep 17 00:00:00 2001 From: Shpuld Shpludson Date: Tue, 11 Feb 2020 12:24:51 +0000 Subject: Emoji Reactions - fixes and improvements --- src/App.scss | 2 +- src/_variables.scss | 2 + src/components/emoji_reactions/emoji_reactions.js | 48 +++++++++- src/components/emoji_reactions/emoji_reactions.vue | 103 +++++++++++++++++++-- src/components/notification/notification.vue | 7 ++ src/components/notifications/notifications.scss | 4 + src/components/react_button/react_button.js | 7 +- src/components/react_button/react_button.vue | 4 + src/components/settings/settings.vue | 10 ++ src/components/status/status.vue | 1 + src/i18n/en.json | 5 +- src/i18n/fi.json | 5 +- src/modules/config.js | 4 +- src/modules/statuses.js | 36 ++++--- src/services/api/api.service.js | 28 +++--- .../entity_normalizer/entity_normalizer.service.js | 1 + .../notification_utils/notification_utils.js | 3 +- test/unit/specs/modules/statuses.spec.js | 13 ++- 18 files changed, 236 insertions(+), 47 deletions(-) (limited to 'src/services/notification_utils/notification_utils.js') diff --git a/src/App.scss b/src/App.scss index 754ca62e..922e39b6 100644 --- a/src/App.scss +++ b/src/App.scss @@ -75,7 +75,7 @@ button { border-radius: $fallback--btnRadius; border-radius: var(--btnRadius, $fallback--btnRadius); cursor: pointer; - box-shadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset; + box-shadow: $fallback--buttonShadow; box-shadow: var(--buttonShadow); font-size: 14px; font-family: sans-serif; diff --git a/src/_variables.scss b/src/_variables.scss index e18101f0..30dc3e42 100644 --- a/src/_variables.scss +++ b/src/_variables.scss @@ -27,3 +27,5 @@ $fallback--tooltipRadius: 5px; $fallback--avatarRadius: 4px; $fallback--avatarAltRadius: 10px; $fallback--attachmentRadius: 10px; + +$fallback--buttonShadow: 0px 0px 2px 0px rgba(0, 0, 0, 1), 0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset; diff --git a/src/components/emoji_reactions/emoji_reactions.js b/src/components/emoji_reactions/emoji_reactions.js index 95d52cb6..b799ac9a 100644 --- a/src/components/emoji_reactions/emoji_reactions.js +++ b/src/components/emoji_reactions/emoji_reactions.js @@ -1,17 +1,55 @@ +import UserAvatar from '../user_avatar/user_avatar.vue' + +const EMOJI_REACTION_COUNT_CUTOFF = 12 const EmojiReactions = { name: 'EmojiReactions', + components: { + UserAvatar + }, props: ['status'], + data: () => ({ + showAll: false, + popperOptions: { + modifiers: { + preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' } + } + } + }), computed: { + tooManyReactions () { + return this.status.emoji_reactions.length > EMOJI_REACTION_COUNT_CUTOFF + }, emojiReactions () { - return this.status.emoji_reactions + return this.showAll + ? this.status.emoji_reactions + : this.status.emoji_reactions.slice(0, EMOJI_REACTION_COUNT_CUTOFF) + }, + showMoreString () { + return `+${this.status.emoji_reactions.length - EMOJI_REACTION_COUNT_CUTOFF}` + }, + accountsForEmoji () { + return this.status.emoji_reactions.reduce((acc, reaction) => { + acc[reaction.name] = reaction.accounts || [] + return acc + }, {}) + }, + loggedIn () { + return !!this.$store.state.users.currentUser } }, methods: { + toggleShowAll () { + this.showAll = !this.showAll + }, 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) + return this.status.emoji_reactions.find(r => r.name === emoji).me + }, + fetchEmojiReactionsByIfMissing () { + const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts) + if (hasNoAccounts) { + this.$store.dispatch('fetchEmojiReactionsBy', this.status.id) + } }, reactWith (emoji) { this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji }) @@ -20,6 +58,8 @@ const EmojiReactions = { this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji }) }, emojiOnClick (emoji, event) { + if (!this.loggedIn) return + if (this.reactedWith(emoji)) { this.unreact(emoji) } else { diff --git a/src/components/emoji_reactions/emoji_reactions.vue b/src/components/emoji_reactions/emoji_reactions.vue index 00d6d2b7..e5b6d9f5 100644 --- a/src/components/emoji_reactions/emoji_reactions.vue +++ b/src/components/emoji_reactions/emoji_reactions.vue @@ -1,16 +1,58 @@ @@ -23,6 +65,31 @@ flex-wrap: wrap; } +.reacted-users { + padding: 0.5em; +} + +.reacted-user { + padding: 0.25em; + display: flex; + flex-direction: row; + + .reacted-user-names { + display: flex; + flex-direction: column; + margin-left: 0.5em; + + img { + width: 1em; + height: 1em; + } + } + + .reacted-user-screen-name { + font-size: 9px; + } +} + .emoji-reaction { padding: 0 0.5em; margin-right: 0.5em; @@ -38,6 +105,26 @@ &:focus { outline: none; } + + &.not-clickable { + cursor: default; + &:hover { + box-shadow: $fallback--buttonShadow; + box-shadow: var(--buttonShadow); + } + } +} + +.emoji-reaction-expand { + padding: 0 0.5em; + margin-right: 0.5em; + margin-top: 0.5em; + display: flex; + align-items: center; + justify-content: center; + &:hover { + text-decoration: underline; + } } .picked-reaction { diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 16124e50..411c0271 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -78,6 +78,13 @@ {{ $t('notifications.migrated_to') }} + + + + {{ notification.emoji }} + + +
r.name === emoji) + if (existingReaction && existingReaction.me) { + this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji }) + } else { + this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji }) + } this.closeReactionSelect() } }, diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue index c925dd71..fb43ebaf 100644 --- a/src/components/react_button/react_button.vue +++ b/src/components/react_button/react_button.vue @@ -54,6 +54,10 @@ .reaction-picker-filter { padding: 0.5em; + display: flex; + input { + flex: 1; + } } .reaction-picker-divider { diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index cef492f3..60cb8a87 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -92,6 +92,11 @@ {{ $t('settings.reply_link_preview') }} +
  • + + {{ $t('settings.emoji_reactions_on_timeline') }} + +
  • @@ -328,6 +333,11 @@ {{ $t('settings.notification_visibility_moves') }} +
  • + + {{ $t('settings.notification_visibility_emoji_reactions') }} + +
  • diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 0a82dcbe..83f07dac 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -369,6 +369,7 @@ diff --git a/src/i18n/en.json b/src/i18n/en.json index 74e71fc8..d0d654d3 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -126,7 +126,8 @@ "read": "Read!", "repeated_you": "repeated your status", "no_more_notifications": "No more notifications", - "migrated_to": "migrated to" + "migrated_to": "migrated to", + "reacted_with": "reacted with {0}" }, "polls": { "add_poll": "Add Poll", @@ -283,6 +284,7 @@ "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", + "emoji_reactions_on_timeline": "Show emoji reactions on timeline", "export_theme": "Save preset", "filtering": "Filtering", "filtering_explanation": "All statuses containing these words will be muted, one per line", @@ -331,6 +333,7 @@ "notification_visibility_mentions": "Mentions", "notification_visibility_repeats": "Repeats", "notification_visibility_moves": "User Migrates", + "notification_visibility_emoji_reactions": "Reactions", "no_rich_text_description": "Strip rich text formatting from all posts", "no_blocks": "No blocks", "no_mutes": "No mutes", diff --git a/src/i18n/fi.json b/src/i18n/fi.json index e7ed5408..ac8b2ac9 100644 --- a/src/i18n/fi.json +++ b/src/i18n/fi.json @@ -53,7 +53,8 @@ "notifications": "Ilmoitukset", "read": "Lue!", "repeated_you": "toisti viestisi", - "no_more_notifications": "Ei enempää ilmoituksia" + "no_more_notifications": "Ei enempää ilmoituksia", + "reacted_with": "lisäsi reaktion {0}" }, "polls": { "add_poll": "Lisää äänestys", @@ -140,6 +141,7 @@ "delete_account_description": "Poista tilisi ja viestisi pysyvästi.", "delete_account_error": "Virhe poistaessa tiliäsi. Jos virhe jatkuu, ota yhteyttä palvelimesi ylläpitoon.", "delete_account_instructions": "Syötä salasanasi vahvistaaksesi tilin poiston.", + "emoji_reactions_on_timeline": "Näytä emojireaktiot aikajanalla", "export_theme": "Tallenna teema", "filtering": "Suodatus", "filtering_explanation": "Kaikki viestit, jotka sisältävät näitä sanoja, suodatetaan. Yksi sana per rivi.", @@ -183,6 +185,7 @@ "notification_visibility_likes": "Tykkäykset", "notification_visibility_mentions": "Maininnat", "notification_visibility_repeats": "Toistot", + "notification_visibility_emoji_reactions": "Reaktiot", "no_rich_text_description": "Älä näytä tekstin muotoilua.", "hide_network_description": "Älä näytä seurauksiani tai seuraajiani", "nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse", diff --git a/src/modules/config.js b/src/modules/config.js index de9f041b..8381fa53 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -20,6 +20,7 @@ export const defaultState = { autoLoad: true, streaming: false, hoverPreview: true, + emojiReactionsOnTimeline: true, autohideFloatingPostButton: false, pauseOnUnfocused: true, stopGifs: false, @@ -29,7 +30,8 @@ export const defaultState = { mentions: true, likes: true, repeats: true, - moves: true + moves: true, + emojiReactions: false }, webPushNotifications: false, muteWords: [], diff --git a/src/modules/statuses.js b/src/modules/statuses.js index ea0c1749..25b62ac7 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -81,7 +81,8 @@ const visibleNotificationTypes = (rootState) => { rootState.config.notificationVisibility.mentions && 'mention', rootState.config.notificationVisibility.repeats && 'repeat', rootState.config.notificationVisibility.follows && 'follow', - rootState.config.notificationVisibility.moves && 'move' + rootState.config.notificationVisibility.moves && 'move', + rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reactions' ].filter(_ => _) } @@ -325,6 +326,10 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item } + if (notification.type === 'pleroma:emoji_reaction') { + dispatch('fetchEmojiReactionsBy', notification.status.id) + } + // Only add a new notification if we don't have one for the same action if (!state.notifications.idStore.hasOwnProperty(notification.id)) { state.notifications.maxId = notification.id > state.notifications.maxId @@ -358,7 +363,9 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot break } - if (i18nString) { + if (notification.type === 'pleroma:emoji_reaction') { + notifObj.body = rootGetters.i18n.t('notifications.reacted_with', [notification.emoji]) + } else if (i18nString) { notifObj.body = rootGetters.i18n.t('notifications.' + i18nString) } else { notifObj.body = notification.status.text @@ -371,10 +378,10 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot } if (!notification.seen && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.type)) { - let notification = new window.Notification(title, notifObj) + let desktopNotification = new window.Notification(title, notifObj) // Chrome is known for not closing notifications automatically // according to MDN, anyway. - setTimeout(notification.close.bind(notification), 5000) + setTimeout(desktopNotification.close.bind(desktopNotification), 5000) } } } else if (notification.seen) { @@ -537,12 +544,13 @@ export const mutations = { }, 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 reactionIndex = findIndex(status.emoji_reactions, { name: emoji }) + const reaction = status.emoji_reactions[reactionIndex] || { name: emoji, count: 0, accounts: [] } const newReaction = { ...reaction, count: reaction.count + 1, + me: true, accounts: [ ...reaction.accounts, currentUser @@ -558,21 +566,23 @@ export const mutations = { }, removeOwnReaction (state, { id, emoji, currentUser }) { const status = state.allStatusesObject[id] - const reactionIndex = findIndex(status.emoji_reactions, { emoji }) + const reactionIndex = findIndex(status.emoji_reactions, { name: emoji }) if (reactionIndex < 0) return const reaction = status.emoji_reactions[reactionIndex] + const accounts = reaction.accounts || [] const newReaction = { ...reaction, count: reaction.count - 1, - accounts: reaction.accounts.filter(acc => acc.id === currentUser.id) + me: false, + accounts: 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)) + set(status, 'emoji_reactions', status.emoji_reactions.filter(r => r.name !== emoji)) } }, updateStatusWithPoll (state, { id, poll }) { @@ -681,18 +691,22 @@ const statuses = { }, reactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) { const currentUser = rootState.users.currentUser + if (!currentUser) return + commit('addOwnReaction', { id, emoji, currentUser }) rootState.api.backendInteractor.reactWithEmoji({ id, emoji }).then( - status => { + ok => { dispatch('fetchEmojiReactionsBy', id) } ) }, unreactWithEmoji ({ rootState, dispatch, commit }, { id, emoji }) { const currentUser = rootState.users.currentUser + if (!currentUser) return + commit('removeOwnReaction', { id, emoji, currentUser }) rootState.api.backendInteractor.unreactWithEmoji({ id, emoji }).then( - status => { + ok => { dispatch('fetchEmojiReactionsBy', id) } ) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index b794fd58..20eaa9a0 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -74,9 +74,9 @@ 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 PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions` +const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}` +const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}` const oldfetch = window.fetch @@ -888,25 +888,27 @@ 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 fetchEmojiReactions = ({ id, credentials }) => { + return promisedRequest({ url: PLEROMA_EMOJI_REACTIONS_URL(id), credentials }) + .then((reactions) => reactions.map(r => { + r.accounts = r.accounts.map(parseUser) + return r + })) } const reactWithEmoji = ({ id, emoji, credentials }) => { return promisedRequest({ - url: PLEROMA_EMOJI_REACT_URL(id), - method: 'POST', - credentials, - payload: { emoji } + url: PLEROMA_EMOJI_REACT_URL(id, emoji), + method: 'PUT', + credentials }).then(parseStatus) } const unreactWithEmoji = ({ id, emoji, credentials }) => { return promisedRequest({ - url: PLEROMA_EMOJI_UNREACT_URL(id), - method: 'POST', - credentials, - payload: { emoji } + url: PLEROMA_EMOJI_UNREACT_URL(id, emoji), + method: 'DELETE', + credentials }).then(parseStatus) } diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 0a8abbbd..84169a7b 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -354,6 +354,7 @@ export const parseNotification = (data) => { ? null : parseUser(data.target) output.from_profile = parseUser(data.account) + output.emoji = data.emoji } else { const parsedNotice = parseStatus(data.notice) output.type = data.ntype diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js index 860620fc..b17bd7bf 100644 --- a/src/services/notification_utils/notification_utils.js +++ b/src/services/notification_utils/notification_utils.js @@ -7,7 +7,8 @@ export const visibleTypes = store => ([ store.state.config.notificationVisibility.mentions && 'mention', store.state.config.notificationVisibility.repeats && 'repeat', store.state.config.notificationVisibility.follows && 'follow', - store.state.config.notificationVisibility.moves && 'move' + store.state.config.notificationVisibility.moves && 'move', + store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction' ].filter(_ => _)) const sortById = (a, b) => { diff --git a/test/unit/specs/modules/statuses.spec.js b/test/unit/specs/modules/statuses.spec.js index e53aa388..fe42e85b 100644 --- a/test/unit/specs/modules/statuses.spec.js +++ b/test/unit/specs/modules/statuses.spec.js @@ -245,11 +245,12 @@ describe('Statuses module', () => { it('increments count in existing reaction', () => { const state = defaultState() const status = makeMockStatus({ id: '1' }) - status.emoji_reactions = [ { emoji: '😂', count: 1, accounts: [] } ] + status.emoji_reactions = [ { name: '😂', 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].me).to.eql(true) expect(state.allStatusesObject['1'].emoji_reactions[0].accounts[0].id).to.eql('me') }) @@ -261,27 +262,29 @@ describe('Statuses module', () => { 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].me).to.eql(true) 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' }] } ] + status.emoji_reactions = [ { name: '😂', count: 2, accounts: [{ id: 'me' }] } ] mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' }) - mutations.removeOwnReaction(state, { id: '1', emoji: '😂', currentUser: {} }) + mutations.removeOwnReaction(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].me).to.eql(false) 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' }] }] + status.emoji_reactions = [{ name: '😂', count: 1, accounts: [{ id: 'me' }] }] mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' }) - mutations.removeOwnReaction(state, { id: '1', emoji: '😂', currentUser: {} }) + mutations.removeOwnReaction(state, { id: '1', emoji: '😂', currentUser: { id: 'me' } }) expect(state.allStatusesObject['1'].emoji_reactions.length).to.eql(0) }) }) -- cgit v1.2.3-70-g09d2 From 01b07f01e9340935faf51e5a3c8034cc90423989 Mon Sep 17 00:00:00 2001 From: eugenijm Date: Sat, 25 Apr 2020 07:04:39 +0300 Subject: Add support for follow request notifications --- CHANGELOG.md | 3 ++ src/components/notification/notification.js | 19 ++++++++++ src/components/notification/notification.vue | 44 ++++++++++++++++------ src/components/notifications/notifications.scss | 15 ++++++++ src/i18n/en.json | 5 ++- src/modules/config.js | 3 +- src/modules/statuses.js | 22 ++++++++++- src/services/api/api.service.js | 11 ++++++ .../entity_normalizer/entity_normalizer.service.js | 5 +-- .../notification_utils/notification_utils.js | 7 +++- static/fontello.json | 14 ++++++- 11 files changed, 128 insertions(+), 20 deletions(-) (limited to 'src/services/notification_utils/notification_utils.js') diff --git a/CHANGELOG.md b/CHANGELOG.md index f45561d0..685fe629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - Emoji autocomplete will match any part of the word and not just start, for example :drool will now helpfully suggest :blobcatdrool: and :blobcatdroolreach: +### Add +- Follow request notification support + ## [2.0.2] - 2020-04-08 ### Fixed - Favorite/Repeat avatars not showing up on private instances/non-public posts diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index e7bd769e..6deee7d5 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -2,6 +2,7 @@ import Status from '../status/status.vue' import UserAvatar from '../user_avatar/user_avatar.vue' import UserCard from '../user_card/user_card.vue' import Timeago from '../timeago/timeago.vue' +import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' @@ -32,6 +33,21 @@ const Notification = { }, toggleMute () { this.unmuted = !this.unmuted + }, + approveUser () { + this.$store.state.api.backendInteractor.approveUser({ id: this.user.id }) + this.$store.dispatch('removeFollowRequest', this.user) + this.$store.dispatch('updateNotification', { + id: this.notification.id, + updater: notification => { + notification.type = 'follow' + } + }) + }, + denyUser () { + this.$store.state.api.backendInteractor.denyUser({ id: this.user.id }) + this.$store.dispatch('removeFollowRequest', this.user) + this.$store.dispatch('dismissNotification', { id: this.notification.id }) } }, computed: { @@ -57,6 +73,9 @@ const Notification = { }, needMute () { return this.user.muted + }, + isStatusNotification () { + return isStatusNotification(this.notification.type) } } } diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 51875747..02802776 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -74,6 +74,10 @@ {{ $t('notifications.followed_you') }} + + + {{ $t('notifications.follow_request') }} + {{ $t('notifications.migrated_to') }} @@ -87,30 +91,30 @@
    - + - +
    - + - +
    { each(notifications, (notification) => { - if (notification.type !== 'follow' && notification.type !== 'move') { + if (isStatusNotification(notification.type)) { notification.action = addStatusToGlobalStorage(state, notification.action).item notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item } @@ -361,13 +362,16 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot case 'move': i18nString = 'migrated_to' break + case 'follow_request': + i18nString = 'follow_request' + break } if (notification.type === 'pleroma:emoji_reaction') { notifObj.body = rootGetters.i18n.t('notifications.reacted_with', [notification.emoji]) } else if (i18nString) { notifObj.body = rootGetters.i18n.t('notifications.' + i18nString) - } else { + } else if (isStatusNotification(notification.type)) { notifObj.body = notification.status.text } @@ -521,6 +525,13 @@ export const mutations = { notification.seen = true }) }, + dismissNotification (state, { id }) { + state.notifications.data = state.notifications.data.filter(n => n.id !== id) + }, + updateNotification (state, { id, updater }) { + const notification = find(state.notifications.data, n => n.id === id) + notification && updater(notification) + }, queueFlush (state, { timeline, id }) { state.timelines[timeline].flushMarker = id }, @@ -680,6 +691,13 @@ const statuses = { credentials: rootState.users.currentUser.credentials }) }, + dismissNotification ({ rootState, commit }, { id }) { + rootState.api.backendInteractor.dismissNotification({ id }) + .then(() => commit('dismissNotification', { id })) + }, + updateNotification ({ rootState, commit }, { id, updater }) { + commit('updateNotification', { id, updater }) + }, fetchFavsAndRepeats ({ rootState, commit }, id) { Promise.all([ rootState.api.backendInteractor.fetchFavoritedByUsers({ id }), diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index ad2b2ad5..cda61ee2 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -29,6 +29,7 @@ const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials' const MASTODON_REGISTRATION_URL = '/api/v1/accounts' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications' +const MASTODON_DISMISS_NOTIFICATION_URL = id => `/api/v1/notifications/${id}/dismiss` const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite` const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite` const MASTODON_RETWEET_URL = id => `/api/v1/statuses/${id}/reblog` @@ -1010,6 +1011,15 @@ const unmuteDomain = ({ domain, credentials }) => { }) } +const dismissNotification = ({ credentials, id }) => { + return promisedRequest({ + url: MASTODON_DISMISS_NOTIFICATION_URL(id), + method: 'POST', + payload: { id }, + credentials + }) +} + export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => { return Object.entries({ ...(credentials @@ -1165,6 +1175,7 @@ const apiService = { denyUser, suggestions, markNotificationsAsSeen, + dismissNotification, vote, fetchPoll, fetchFavoritedByUsers, diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 84169a7b..6cacd0b8 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -1,4 +1,5 @@ import escape from 'escape-html' +import { isStatusNotification } from '../notification_utils/notification_utils.js' const qvitterStatusType = (status) => { if (status.is_post_verb) { @@ -346,9 +347,7 @@ 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.type === 'move' - ? null - : parseStatus(data.status) + output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null output.action = output.status // TODO: Refactor, this is unneeded output.target = output.type !== 'move' ? null diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js index b17bd7bf..eb479227 100644 --- a/src/services/notification_utils/notification_utils.js +++ b/src/services/notification_utils/notification_utils.js @@ -1,4 +1,4 @@ -import { filter, sortBy } from 'lodash' +import { filter, sortBy, includes } from 'lodash' export const notificationsFromStore = store => store.state.statuses.notifications.data @@ -7,10 +7,15 @@ export const visibleTypes = store => ([ store.state.config.notificationVisibility.mentions && 'mention', store.state.config.notificationVisibility.repeats && 'repeat', store.state.config.notificationVisibility.follows && 'follow', + store.state.config.notificationVisibility.followRequest && 'follow_request', store.state.config.notificationVisibility.moves && 'move', store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction' ].filter(_ => _)) +const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction'] + +export const isStatusNotification = (type) => includes(statusNotifications, type) + const sortById = (a, b) => { const seqA = Number(a.id) const seqB = Number(b.id) diff --git a/static/fontello.json b/static/fontello.json index 5a7086a2..5963b68b 100755 --- a/static/fontello.json +++ b/static/fontello.json @@ -345,6 +345,18 @@ "css": "link", "code": 59427, "src": "fontawesome" + }, + { + "uid": "8b80d36d4ef43889db10bc1f0dc9a862", + "css": "user", + "code": 59428, + "src": "fontawesome" + }, + { + "uid": "12f4ece88e46abd864e40b35e05b11cd", + "css": "ok", + "code": 59431, + "src": "fontawesome" } ] -} +} \ No newline at end of file -- cgit v1.2.3-70-g09d2