diff --git a/src/components/follow_request_card/follow_request_card.js b/src/components/follow_request_card/follow_request_card.js index 294dd2472..f72e3bf6c 100644 --- a/src/components/follow_request_card/follow_request_card.js +++ b/src/components/follow_request_card/follow_request_card.js @@ -1,9 +1,9 @@ import { defineAsyncComponent } from 'vue' -import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js' import BasicUserCard from '../basic_user_card/basic_user_card.vue' import { useMergedConfigStore } from 'src/stores/merged_config.js' +import { useNotificationsStore } from 'src/stores/notifications.js' import { useOAuthStore } from 'src/stores/oauth.js' import { approveUser, denyUser } from 'src/api/user.js' @@ -24,7 +24,7 @@ const FollowRequestCard = { }, methods: { findFollowRequestNotificationId() { - const notif = notificationsFromStore(this.$store).find( + const notif = useNotificationsStore().data.find( (notif) => notif.from_profile.id === this.user.id && notif.type === 'follow_request', diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js index 0c8f987c1..9a4c4c61d 100644 --- a/src/components/mobile_nav/mobile_nav.js +++ b/src/components/mobile_nav/mobile_nav.js @@ -5,7 +5,7 @@ import NavigationPins from 'src/components/navigation/navigation_pins.vue' import GestureService from '../../services/gesture_service/gesture_service' import { countExtraNotifications, - unseenNotificationsFromStore, + unseenNotifications, } from '../../services/notification_utils/notification_utils' import { useAnnouncementsStore } from 'src/stores/announcements.js' @@ -57,8 +57,7 @@ const MobileNav = { return useUsersStore().currentUser }, unseenNotifications() { - return unseenNotificationsFromStore( - this.$store, + return unseenNotifications( useMergedConfigStore().mergedConfig.notificationVisibility, useMergedConfigStore().mergedConfig.ignoreInactionableSeen, ) diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index 4eadb1657..92272facf 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -7,9 +7,8 @@ import FaviconService from '../../services/favicon_service/favicon_service.js' import { ACTIONABLE_NOTIFICATION_TYPES, countExtraNotifications, - filteredNotificationsFromStore, - notificationsFromStore, - unseenNotificationsFromStore, + filteredNotifications, + unseenNotifications, } from '../../services/notification_utils/notification_utils.js' import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js' import NotificationFilters from './notification_filters.vue' @@ -18,6 +17,7 @@ import { useAnnouncementsStore } from 'src/stores/announcements.js' import { useChatsStore } from 'src/stores/chats.js' import { useInterfaceStore } from 'src/stores/interface.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' +import { useNotificationsStore } from 'src/stores/notifications.js' import { useUsersStore } from 'src/stores/users.js' import { library } from '@fortawesome/fontawesome-svg-core' @@ -71,14 +71,13 @@ const Notifications = { return this.minimalMode ? '' : 'panel panel-default' }, notifications() { - return notificationsFromStore(this.$store) + return useNotificationsStore().data }, error() { - return this.$store.state.notifications.error + return useNotificationsStore().error }, unseenNotifications() { - return unseenNotificationsFromStore( - this.$store, + return unseenNotifications( useMergedConfigStore().mergedConfig.notificationVisibility, useMergedConfigStore().mergedConfig.ignoreInactionableSeen, ) @@ -86,18 +85,15 @@ const Notifications = { filteredNotifications() { if (this.unseenAtTop) { return [ - ...filteredNotificationsFromStore( - this.$store, + ...filteredNotifications( useMergedConfigStore().mergedConfig.notificationVisibility, ).filter((n) => this.shouldShowUnseen(n)), - ...filteredNotificationsFromStore( - this.$store, + ...filteredNotifications( useMergedConfigStore().mergedConfig.notificationVisibility, ).filter((n) => !this.shouldShowUnseen(n)), ] } else { - return filteredNotificationsFromStore( - this.$store, + return filteredNotifications( useMergedConfigStore().mergedConfig.notificationVisibility, this.filterMode, ) @@ -128,7 +124,7 @@ const Notifications = { ) }, loading() { - return this.$store.state.notifications.loading + return useNotificationsStore().loading }, noHeading() { const { layoutType } = useInterfaceStore() @@ -225,14 +221,14 @@ const Notifications = { */ notificationClicked(notification) { const { id } = notification - this.$store.dispatch('notificationClicked', { id }) + useNotificationsStore().notificationClicked(id) }, notificationInteracted(notification) { const { id } = notification - this.$store.dispatch('markSingleNotificationAsSeen', { id }) + useNotificationsStore().markSingleNotificationAsSeen(id) }, markAsSeen() { - this.$store.dispatch('markNotificationsAsSeen') + useNotificationsStore().markNotificationsAsSeen() this.seenToDisplayCount = DEFAULT_SEEN_TO_DISPLAY_COUNT }, fetchOlderNotifications() { @@ -253,7 +249,7 @@ const Notifications = { const store = this.$store const credentials = useUsersStore().currentUser.credentials - store.commit('setNotificationsLoading', { value: true }) + useNotificationsStore().setNotificationsLoading(true) notificationsFetcher .fetchAndUpdate({ store, @@ -261,7 +257,7 @@ const Notifications = { older: true, }) .then((notifs) => { - store.commit('setNotificationsLoading', { value: false }) + useNotificationsStore().setNotificationsLoading(false) if (notifs.length === 0) { this.bottomedOut = true } diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index 3a4885792..2d2886d58 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -4,7 +4,7 @@ import { mapGetters } from 'vuex' import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js' import UserCard from 'src/components/user_card/user_card.vue' import GestureService from '../../services/gesture_service/gesture_service' -import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils' +import { unseenNotifications } from '../../services/notification_utils/notification_utils' import { useAnnouncementsStore } from 'src/stores/announcements' import { useChatsStore } from 'src/stores/chats.js' @@ -77,8 +77,7 @@ const SideDrawer = { return useShoutStore().joined }, unseenNotifications() { - return unseenNotificationsFromStore( - this.$store, + return unseenNotifications( useMergedConfigStore().mergedConfig.notificationVisibility, useMergedConfigStore().mergedConfig.ignoreInactionableSeen, ) diff --git a/src/modules/api.js b/src/modules/api.js index d2c578c9c..d91fc04fb 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -5,6 +5,7 @@ import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js' import { useChatsStore } from 'src/stores/chats.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInterfaceStore } from 'src/stores/interface.js' +import { useNotificationsStore } from 'src/stores/notifications.js' import { useOAuthStore } from 'src/stores/oauth.js' import { useShoutStore } from 'src/stores/shout.js' @@ -117,9 +118,9 @@ const api = { ({ detail: message }) => { if (!message) return // pings if (message.event === 'notification') { - dispatch('addNewNotifications', { - notifications: [message.notification], - older: false, + useNotificationsStore().addNewNotifications({ + timestamp: Date.now(), + data: message.notification, }) } else if (message.event === 'update') { dispatch('addNewStatuses', { @@ -212,7 +213,6 @@ const api = { if (state.mastoUserSocketStatus !== WSConnectionStatus.ERROR) { dispatch('startFetchingTimeline', { timeline: 'friends' }) dispatch('startFetchingNotifications') - dispatch('startFetchingChats') useInterfaceStore().pushGlobalNotice({ level: 'error', messageKey: 'timeline.socket_broke', @@ -234,7 +234,6 @@ const api = { stopMastoUserSocket({ state, dispatch }) { dispatch('startFetchingTimeline', { timeline: 'friends' }) dispatch('startFetchingNotifications') - dispatch('startFetchingChats') state.mastoUserSocket.close() }, @@ -289,7 +288,6 @@ const api = { startFetchingNotifications(store) { if (store.state.fetchers.notifications) return const fetcher = notificationsFetcher.startFetching({ - store, credentials: useOAuthStore().token, }) store.commit('addFetcher', { fetcherName: 'notifications', fetcher }) diff --git a/src/modules/index.js b/src/modules/index.js index 9b469c6df..8b48e4e31 100644 --- a/src/modules/index.js +++ b/src/modules/index.js @@ -1,12 +1,10 @@ import api from './api.js' import drafts from './drafts.js' -import notifications from './notifications.js' import profileConfig from './profileConfig.js' import statuses from './statuses.js' export default { statuses, - notifications, api, profileConfig, drafts, diff --git a/src/services/desktop_notification_utils/desktop_notification_utils.js b/src/services/desktop_notification_utils/desktop_notification_utils.js index cf393aa77..aa5793080 100644 --- a/src/services/desktop_notification_utils/desktop_notification_utils.js +++ b/src/services/desktop_notification_utils/desktop_notification_utils.js @@ -4,14 +4,16 @@ import { showDesktopNotification as swDesktopNotification, } from '../sw/sw.js' +import { useNotificationsStore } from 'src/stores/notifications.js' + const state = { failCreateNotif: false } -export const showDesktopNotification = (rootState, desktopNotificationOpts) => { +export const showDesktopNotification = (desktopNotificationOpts) => { if ( !('Notification' in window && window.Notification.permission === 'granted') ) return - if (rootState.notifications.desktopNotificationSilence) { + if (useNotificationsStore().desktopNotificationSilence) { return } @@ -30,7 +32,7 @@ export const showDesktopNotification = (rootState, desktopNotificationOpts) => { } } -export const closeDesktopNotification = (rootState, { id }) => { +export const closeDesktopNotification = (id) => { if ( !('Notification' in window && window.Notification.permission === 'granted') ) diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js index 9e443bf8b..07433497a 100644 --- a/src/services/notification_utils/notification_utils.js +++ b/src/services/notification_utils/notification_utils.js @@ -1,6 +1,8 @@ import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js' import { muteFilterHits } from '../status_parser/status_parser.js' +import { useNotificationsStore } from 'src/stores/notifications.js' + import FaviconService from 'src/services/favicon_service/favicon_service.js' export const ACTIONABLE_NOTIFICATION_TYPES = new Set([ @@ -11,8 +13,6 @@ export const ACTIONABLE_NOTIFICATION_TYPES = new Set([ let cachedBadgeUrl = null -export const notificationsFromStore = (store) => store.state.notifications.data - const visibleTypes = (notificationVisibility) => { return [ notificationVisibility.likes && 'like', @@ -69,14 +69,11 @@ const isMutedNotification = (muteFilters, notification) => { } export const maybeShowNotification = ( - store, notificationVisibility, muteFilters, notification, i18n, ) => { - const rootState = store.rootState || store.state - if (notification.seen) return if (!visibleTypes(notificationVisibility).includes(notification.type)) return if ( @@ -86,28 +83,23 @@ export const maybeShowNotification = ( return const notificationObject = prepareNotificationObject(notification, i18n) - showDesktopNotification(rootState, notificationObject) + showDesktopNotification(notificationObject) } -export const filteredNotificationsFromStore = ( - store, - notificationVisibility, - types, -) => { +export const filteredNotifications = (notificationVisibility, types) => { // map is just to clone the array since sort mutates it and it causes some issues - const sortedNotifications = notificationsFromStore(store).sort(sortById) + const sortedNotifications = useNotificationsStore().data.sort(sortById) // TODO implement sorting elsewhere and make it optional return sortedNotifications.filter((notification) => (types || visibleTypes(notificationVisibility)).includes(notification.type), ) } -export const unseenNotificationsFromStore = ( - store, +export const unseenNotifications = ( notificationVisibility, ignoreInactionableSeen, ) => { - return filteredNotificationsFromStore(store, notificationVisibility).filter( + return filteredNotifications(notificationVisibility).filter( ({ seen, type }) => { if (!ignoreInactionableSeen) return !seen if (seen) return false diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index 0e0bc0277..e2780dd3e 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -3,12 +3,10 @@ import { promiseInterval } from '../promise_interval/promise_interval.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useInterfaceStore } from 'src/stores/interface.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' +import { useNotificationsStore } from 'src/stores/notifications.js' import { fetchTimeline } from 'src/api/timelines.js' -const update = ({ store, notifications, older }) => { - store.dispatch('addNewNotifications', { notifications, older }) -} // // For using include_types when fetching notifications. // Note: chat_mention excluded as pleroma-fe polls them separately @@ -25,10 +23,9 @@ const mastoApiNotificationTypes = new Set([ 'pleroma:report', ]) -const fetchAndUpdate = ({ store, credentials, older = false, sinceId }) => { +const fetchAndUpdate = ({ credentials, older = false, sinceId }) => { const args = { credentials } - const rootState = store.rootState || store.state - const timelineData = rootState.notifications + const timelineData = useNotificationsStore() const hideMutedPosts = useMergedConfigStore().mergedConfig.hideMutedPosts if (useInstanceCapabilitiesStore().pleromaChatMessagesAvailable) { @@ -43,7 +40,7 @@ const fetchAndUpdate = ({ store, credentials, older = false, sinceId }) => { if (timelineData.minId !== Number.POSITIVE_INFINITY) { args.maxId = timelineData.minId } - return fetchNotifications({ store, args, older }) + return fetchNotifications({ args, older }) } else { // fetch new notifications if ( @@ -54,7 +51,7 @@ const fetchAndUpdate = ({ store, credentials, older = false, sinceId }) => { } else if (sinceId !== null) { args.sinceId = sinceId } - const result = fetchNotifications({ store, args, older }) + const result = fetchNotifications({ args, older }) // If there's any unread notifications, try fetch notifications since // the newest read notification to check if any of the unread notifs @@ -72,7 +69,7 @@ const fetchAndUpdate = ({ store, credentials, older = false, sinceId }) => { if (minId !== Infinity) { args.sinceId = null // Don't use since_id since it sorta conflicts with min_id args.minId = minId - 1 // go beyond - fetchNotifications({ store, args, older }) + fetchNotifications({ args, older }) } } @@ -80,11 +77,13 @@ const fetchAndUpdate = ({ store, credentials, older = false, sinceId }) => { } } -const fetchNotifications = ({ store, args, older }) => { +const fetchNotifications = ({ args, older }) => { return fetchTimeline(args) .then((response) => { const notifications = response.data - update({ store, notifications, older }) + + useNotificationsStore().addNewNotifications(response) + return notifications }) .catch((error) => { @@ -97,7 +96,7 @@ const fetchNotifications = ({ store, args, older }) => { .toArray() .map((x) => x[1]) .forEach((x) => mastoApiNotificationTypes.delete(x)) - return fetchNotifications({ store, args, older }) + return fetchNotifications({ args, older }) } useInterfaceStore().pushGlobalNotice({ @@ -114,7 +113,7 @@ const startFetching = ({ credentials, store }) => { // Initially there's set flag to silence all desktop notifications so // that there won't spam of them when user just opened up the FE we // reset that flag after a while to show new notifications once again. - setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000) + setTimeout(() => useNotificationsStore().setNotificationsSilence(false), 10000) const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store }) boundFetchAndUpdate() return promiseInterval(boundFetchAndUpdate, 10000) diff --git a/src/modules/notifications.js b/src/stores/notifications.js similarity index 53% rename from src/modules/notifications.js rename to src/stores/notifications.js index 100de3974..507e40759 100644 --- a/src/modules/notifications.js +++ b/src/stores/notifications.js @@ -1,3 +1,5 @@ +import { defineStore } from 'pinia' + import { closeAllDesktopNotifications, closeDesktopNotification, @@ -17,70 +19,56 @@ import { useUsersStore } from 'src/stores/users.js' import { dismissNotification, markNotificationsAsSeen } from 'src/api/user.js' -const emptyNotifications = () => ({ +export const defaultState = () => ({ desktopNotificationSilence: true, maxId: 0, minId: Number.POSITIVE_INFINITY, data: [], - idStore: {}, + statusNotificationRelations: new WeakMap(), + idStore: new Map(), loading: false, }) -export const defaultState = () => ({ - ...emptyNotifications(), -}) - -export const notifications = { - state: defaultState(), - mutations: { - addNewNotifications(state, { notifications }) { - notifications.forEach((notification) => { - state.data.push(notification) - state.idStore[notification.id] = notification - }) - }, - clearNotifications(state) { +export const useNotificationsStore = defineStore('notifications', { + state: defaultState, + actions: { + clearNotifications() { const blankState = defaultState() - Object.keys(state).forEach((k) => { - state[k] = blankState[k] + + Object.keys(defaultState()).forEach((k) => { + this[k] = blankState[k] }) }, - updateNotificationsMinMaxId(state, id) { - state.maxId = id > state.maxId ? id : state.maxId - state.minId = id < state.minId ? id : state.minId + updateNotificationsMinMaxId(id) { + this.maxId = id > this.maxId ? id : this.maxId + this.minId = id < this.minId ? id : this.minId }, - setNotificationsLoading(state, { value }) { - state.loading = value + setNotificationsLoading(value) { + this.loading = value }, - setNotificationsSilence(state, { value }) { - state.desktopNotificationSilence = value + setNotificationsSilence(value) { + this.desktopNotificationSilence = value }, - markNotificationsAsSeen(state) { - state.data.forEach((notification) => { - notification.seen = true - }) - }, - markSingleNotificationAsSeen(state, { id }) { - const notification = state.idStore[id] - if (notification) notification.seen = true - }, - dismissNotification(state, { id }) { - state.data = state.data.filter((n) => n.id !== id) - delete state.idStore[id] - }, - updateNotification(state, { id, updater }) { - const notification = state.idStore[id] + updateNotification({ id, updater }) { + const notification = this.idStore.get(id) notification && updater(notification) }, - }, - actions: { - addNewNotifications(store, { notifications }) { - const { commit, dispatch, state, rootState } = store + addNewNotifications(result) { + const { timestamp, data: notifications } = result + + useUsersStore().addNewUsers({ + timestamp, + data: notifications.map((n) => n.from_profile), + }) + notifications.forEach( + (n) => (n.from_profile = useUsersStore().findUser(n.from_profile.id)), + ) + const validNotifications = notifications.filter((notification) => { // If invalid notification, update ids but don't add it to store if (!isValidNotification(notification)) { console.error('Invalid notification:', notification) - commit('updateNotificationsMinMaxId', notification.id) + this.updateNotificationsMinMaxId(notification.id) return false } return true @@ -92,7 +80,8 @@ export const notifications = { ) // Synchronous commit to add all the statuses - commit('addNewStatuses', { + window.vuex.commit('addNewStatuses', { + timestamp, statuses: statusNotifications.map( (notification) => notification.status, ), @@ -101,7 +90,7 @@ export const notifications = { // Update references to statuses in notifications to ones in the store statusNotifications.forEach((notification) => { const id = notification.status.id - const referenceStatus = rootState.statuses.allStatusesObject[id] + const referenceStatus = window.vuex.state.statuses.allStatusesObject[id] if (referenceStatus) { notification.status = referenceStatus @@ -114,28 +103,36 @@ export const notifications = { } if (notification.type === 'pleroma:emoji_reaction') { - dispatch('fetchEmojiReactionsBy', notification.status.id) + window.vuex.dispatch('fetchEmojiReactionsBy', notification.status.id) } // Only add a new notification if we don't have one for the same action - if (!Object.hasOwn(state.idStore, notification.id)) { - commit('updateNotificationsMinMaxId', notification.id) - commit('addNewNotifications', { notifications: [notification] }) + if (!this.idStore.has(notification.id)) { + this.updateNotificationsMinMaxId(notification.id) + + notifications.forEach((notification) => { + this.data.push(notification) + this.idStore.set(notification.id, notification) + }) + + this.statusNotificationRelations.set( + notification.status, + this.idStore.get(notification.id), + ) maybeShowNotification( - store, useMergedConfigStore().mergedConfig.notificationVisibility, Object.values(useSyncConfigStore().prefsStorage.simple.muteFilters), notification, useI18nStore().i18n, ) } else if (notification.seen) { - state.idStore[notification.id].seen = true + this.idStore.get(notification.id).seen = true } }) }, - notificationClicked({ state, dispatch }, { id }) { - const notification = state.idStore[id] + notificationClicked(id) { + const notification = this.idStore.get(id) const { type, seen } = notification if (!seen) { @@ -145,49 +142,45 @@ export const notifications = { case 'follow_request': break default: - dispatch('markSingleNotificationAsSeen', { id }) + this.markSingleNotificationAsSeen({ id }) } } }, - setNotificationsLoading({ commit }, { value }) { - commit('setNotificationsLoading', { value }) - }, - setNotificationsSilence({ commit }, { value }) { - commit('setNotificationsSilence', { value }) - }, - markNotificationsAsSeen({ rootState, state, commit }) { - commit('markNotificationsAsSeen') + markNotificationsAsSeen() { + this.data.forEach((notification) => { + notification.seen = true + }) + markNotificationsAsSeen({ - id: state.maxId, + id: this.maxId, credentials: useUsersStore().currentUser.credentials, }).then(() => { - closeAllDesktopNotifications(rootState) + closeAllDesktopNotifications() }) }, - markSingleNotificationAsSeen({ rootState, commit }, { id }) { - commit('markSingleNotificationAsSeen', { id }) + markSingleNotificationAsSeen({ id }) { + const notification = this.idStore.get(id) + if (notification) notification.seen = true + markNotificationsAsSeen({ single: true, id, credentials: useUsersStore().currentUser.credentials, }).then(() => { - closeDesktopNotification(rootState, { id }) + closeDesktopNotification(id) }) }, - dismissNotificationLocal({ commit }, { id }) { - commit('dismissNotification', { id }) + dismissNotificationLocal(id) { + this.data = this.data.filter((n) => n.id !== id) + delete this.idStore.delete(id) }, - dismissNotification({ rootState, commit }, { id }) { - commit('dismissNotification', { id }) + dismissNotification(id) { + this.dismissNotificationLocal(id) + dismissNotification({ id, credentials: useOAuthStore().token, }) }, - updateNotification({ commit }, { id, updater }) { - commit('updateNotification', { id, updater }) - }, }, -} - -export default notifications +}) diff --git a/src/stores/users.js b/src/stores/users.js index 2718d0735..558f8ed71 100644 --- a/src/stores/users.js +++ b/src/stores/users.js @@ -697,7 +697,7 @@ export const useUsersStore = defineStore('users', { .then(() => { dispatch('fetchChats', { latest: true }) setTimeout( - () => dispatch('setNotificationsSilence', false), + () => useNotificationsStore().setNotificationsSilence(false), 10000, ) })