diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 6489ef872..84fea9543 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -345,7 +345,7 @@ const afterStoreSetup = async ({ store, i18n }) => { store.dispatch('setLayoutHeight', windowHeight()) FaviconService.initFaviconService() - initServiceWorker() + initServiceWorker(store) window.addEventListener('focus', () => updateFocus()) diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 01ad395f5..a8eceab0b 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -249,7 +249,7 @@ diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index 4cbe80937..a210e19d6 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -65,7 +65,7 @@ const Notifications = { return notificationsFromStore(this.$store) }, error () { - return this.$store.state.statuses.notifications.error + return this.$store.state.notifications.error }, unseenNotifications () { return unseenNotificationsFromStore(this.$store) @@ -86,7 +86,7 @@ const Notifications = { return this.unseenNotifications.length + (this.unreadChatCount) + this.unreadAnnouncementCount }, loading () { - return this.$store.state.statuses.notifications.loading + return this.$store.state.notifications.loading }, noHeading () { const { layoutType } = this.$store.state.interface @@ -159,24 +159,16 @@ const Notifications = { updateScrollPosition () { this.showScrollTop = this.$refs.root.offsetTop < this.scrollerRef.scrollTop }, + /* "Interacted" really refers to "actionable" notifications that require user input, + * everything else (likes/repeats/reacts) cannot be acted and therefore we just clear + * the "seen" status upon any clicks on them + */ notificationClicked (notification) { - const { type, id, seen } = notification - if (!seen) { - switch (type) { - case 'mention': - case 'pleroma:report': - case 'follow_request': - break - default: - this.markOneAsSeen(id) - } - } + const { id } = notification + this.$store.dispatch('notificationClicked', { id }) }, notificationInteracted (notification) { - const { id, seen } = notification - if (!seen) this.markOneAsSeen(id) - }, - markOneAsSeen (id) { + const { id } = notification this.$store.dispatch('markSingleNotificationAsSeen', { id }) }, markAsSeen () { diff --git a/src/main.js b/src/main.js index 0b7c7674a..85eb1f4c2 100644 --- a/src/main.js +++ b/src/main.js @@ -6,6 +6,7 @@ import './lib/event_target_polyfill.js' import interfaceModule from './modules/interface.js' import instanceModule from './modules/instance.js' import statusesModule from './modules/statuses.js' +import notificationsModule from './modules/notifications.js' import listsModule from './modules/lists.js' import usersModule from './modules/users.js' import apiModule from './modules/api.js' @@ -78,6 +79,7 @@ const persistedStateOptions = { // TODO refactor users/statuses modules, they depend on each other users: usersModule, statuses: statusesModule, + notifications: notificationsModule, lists: listsModule, api: apiModule, config: configModule, diff --git a/src/modules/notifications.js b/src/modules/notifications.js new file mode 100644 index 000000000..febff4886 --- /dev/null +++ b/src/modules/notifications.js @@ -0,0 +1,169 @@ +import apiService from '../services/api/api.service.js' + +import { + isStatusNotification, + isValidNotification, + maybeShowNotification +} from '../services/notification_utils/notification_utils.js' + +import { + closeDesktopNotification, + closeAllDesktopNotifications +} from '../services/desktop_notification_utils/desktop_notification_utils.js' + +const emptyNotifications = () => ({ + desktopNotificationSilence: true, + maxId: 0, + minId: Number.POSITIVE_INFINITY, + data: [], + idStore: {}, + 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) { + state = emptyNotifications() + }, + updateNotificationsMinMaxId (state, id) { + state.maxId = id > state.maxId ? id : state.maxId + state.minId = id < state.minId ? id : state.minId + }, + setNotificationsLoading (state, { value }) { + state.loading = value + }, + setNotificationsSilence (state, { value }) { + state.desktopNotificationSilence = value + }, + markNotificationsAsSeen (state) { + state.data.forEach((notification) => { + notification.seen = true + }) + }, + markSingleNotificationAsSeen (state, { id }) { + const notification = find(state.data, n => n.id === 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 = find(state.data, n => n.id === id) + notification && updater(notification) + } + }, + actions: { + addNewNotifications (store, { notifications, older }) { + const { commit, dispatch, state, rootState } = store + 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) + return false + } + return true + }) + + const statusNotifications = validNotifications.filter(notification => isStatusNotification(notification.type) && notification.status) + + // Synchronous commit to add all the statuses + commit('addNewStatuses', { statuses: statusNotifications.map(notification => notification.status) }) + + // 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] + + if (referenceStatus) { + notification.status = referenceStatus + } + }) + + validNotifications.forEach(notification => { + if (notification.type === 'pleroma:report') { + dispatch('addReport', notification.report) + } + + 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 + // eslint-disable-next-line no-prototype-builtins + if (!state.idStore.hasOwnProperty(notification.id)) { + commit('updateNotificationsMinMaxId', notification.id) + commit('addNewNotifications', { notifications: [notification] }) + + maybeShowNotification(store, notification) + } else if (notification.seen) { + state.idStore[notification.id].seen = true + } + }) + }, + notificationClicked ({ state, commit }, id) { + const notification = state.idStore[id] + const { type, seen } = notification + + if (!seen) { + switch (type) { + case 'mention': + case 'pleroma:report': + case 'follow_request': + break + default: + commit('markSingleNotificationAsSeen', { id }) + } + } + }, + setNotificationsLoading ({ rootState, commit }, { value }) { + commit('setNotificationsLoading', { value }) + }, + setNotificationsSilence ({ rootState, commit }, { value }) { + commit('setNotificationsSilence', { value }) + }, + markNotificationsAsSeen ({ rootState, state, commit }) { + commit('markNotificationsAsSeen') + apiService.markNotificationsAsSeen({ + id: state.maxId, + credentials: rootState.users.currentUser.credentials + }).then(() => { + closeAllDesktopNotifications(rootState) + }) + }, + markSingleNotificationAsSeen ({ rootState, commit }, { id }) { + commit('markSingleNotificationAsSeen', { id }) + apiService.markNotificationsAsSeen({ + single: true, + id, + credentials: rootState.users.currentUser.credentials + }).then(() => { + closeDesktopNotification(rootState, id) + }) + }, + dismissNotificationLocal ({ rootState, commit }, { id }) { + commit('dismissNotification', { id }) + }, + dismissNotification ({ rootState, commit }, { id }) { + commit('dismissNotification', { id }) + rootState.api.backendInteractor.dismissNotification({ id }) + }, + updateNotification ({ rootState, commit }, { id, updater }) { + commit('updateNotification', { id, updater }) + } + } +} + +export default notifications diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 6e331d16a..c4e0bd852 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -12,15 +12,6 @@ import { isArray, omitBy } from 'lodash' -import { - isStatusNotification, - isValidNotification, - maybeShowNotification -} from '../services/notification_utils/notification_utils.js' -import { - closeDesktopNotification, - closeAllDesktopNotifications -} from '../services/desktop_notification_utils/desktop_notification_utils.js' import apiService from '../services/api/api.service.js' const emptyTl = (userId = 0) => ({ @@ -40,22 +31,12 @@ const emptyTl = (userId = 0) => ({ flushMarker: 0 }) -const emptyNotifications = () => ({ - desktopNotificationSilence: true, - maxId: 0, - minId: Number.POSITIVE_INFINITY, - data: [], - idStore: {}, - loading: false -}) - export const defaultState = () => ({ allStatuses: [], scrobblesNextFetch: {}, allStatusesObject: {}, conversationsObject: {}, maxId: 0, - notifications: emptyNotifications(), favorites: new Set(), timelines: { mentions: emptyTl(), @@ -158,14 +139,11 @@ const addStatusToGlobalStorage = (state, data) => { return result } +// XXX: this isn't actually really used anymore since deletes just don't appear outside streaming, thanks masto // Remove status from the global storages (arrays and objects maintaining statuses) except timelines const removeStatusFromGlobalStorage = (state, status) => { remove(state.allStatuses, { id: status.id }) - - // TODO: Need to remove from allStatusesObject? - - // Remove possible notification - remove(state.notifications.data, ({ action: { id } }) => id === status.id) + delete state.allStatusesObject[status.id] // Remove from conversation const conversationId = status.statusnet_conversation_id @@ -342,52 +320,6 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us } } -const updateNotificationsMinMaxId = (state, notification) => { - state.notifications.maxId = notification.id > state.notifications.maxId - ? notification.id - : state.notifications.maxId - state.notifications.minId = notification.id < state.notifications.minId - ? notification.id - : state.notifications.minId -} - -const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters, newNotificationSideEffects }) => { - each(notifications, (notification) => { - // If invalid notification, update ids but don't add it to store - if (!isValidNotification(notification)) { - console.error('Invalid notification:', notification) - updateNotificationsMinMaxId(state, notification) - return - } - - if (isStatusNotification(notification.type)) { - notification.action = addStatusToGlobalStorage(state, notification.action).item - notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item - } - - if (notification.type === 'pleroma:report') { - dispatch('addReport', notification.report) - } - - 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 - // eslint-disable-next-line no-prototype-builtins - if (!state.notifications.idStore.hasOwnProperty(notification.id)) { - updateNotificationsMinMaxId(state, notification) - - state.notifications.data.push(notification) - state.notifications.idStore[notification.id] = notification - - newNotificationSideEffects(notification) - } else if (notification.seen) { - state.notifications.idStore[notification.id].seen = true - } - }) -} - const removeStatus = (state, { timeline, userId }) => { const timelineObject = state.timelines[timeline] if (userId) { @@ -400,7 +332,6 @@ const removeStatus = (state, { timeline, userId }) => { export const mutations = { addNewStatuses, - addNewNotifications, removeStatus, showNewStatuses (state, { timeline }) { const oldTimeline = (state.timelines[timeline]) @@ -422,9 +353,6 @@ export const mutations = { const userId = excludeUserId ? state.timelines[timeline].userId : undefined state.timelines[timeline] = emptyTl(userId) }, - clearNotifications (state) { - state.notifications = emptyNotifications() - }, setFavorited (state, { status, value }) { const newStatus = state.allStatusesObject[status.id] @@ -507,31 +435,6 @@ export const mutations = { const newStatus = state.allStatusesObject[id] newStatus.nsfw = nsfw }, - setNotificationsLoading (state, { value }) { - state.notifications.loading = value - }, - setNotificationsSilence (state, { value }) { - state.notifications.desktopNotificationSilence = value - }, - markNotificationsAsSeen (state) { - each(state.notifications.data, (notification) => { - notification.seen = true - }) - }, - markSingleNotificationAsSeen (state, { id }) { - const notification = find(state.notifications.data, n => n.id === id) - if (notification) notification.seen = true - }, - dismissNotification (state, { id }) { - state.notifications.data = state.notifications.data.filter(n => n.id !== id) - }, - dismissNotifications (state, { finder }) { - state.notifications.data = state.notifications.data.filter(n => finder) - }, - 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 }, @@ -613,23 +516,9 @@ export const mutations = { const statuses = { state: defaultState(), actions: { - addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId, pagination }) { + addNewStatuses ({ rootState, commit, dispatch, state }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId, pagination }) { commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId, pagination }) }, - addNewNotifications (store, { notifications, older }) { - const { commit, dispatch, rootGetters } = store - - const newNotificationSideEffects = (notification) => { - maybeShowNotification(store, notification) - } - commit('addNewNotifications', { dispatch, notifications, older, rootGetters, newNotificationSideEffects }) - }, - setNotificationsLoading ({ rootState, commit }, { value }) { - commit('setNotificationsLoading', { value }) - }, - setNotificationsSilence ({ rootState, commit }, { value }) { - commit('setNotificationsSilence', { value }) - }, fetchStatus ({ rootState, dispatch }, id) { return rootState.api.backendInteractor.fetchStatus({ id }) .then((status) => dispatch('addNewStatuses', { statuses: [status] })) @@ -725,35 +614,6 @@ const statuses = { queueFlushAll ({ rootState, commit }) { commit('queueFlushAll') }, - markNotificationsAsSeen ({ rootState, commit }) { - commit('markNotificationsAsSeen') - apiService.markNotificationsAsSeen({ - id: rootState.statuses.notifications.maxId, - credentials: rootState.users.currentUser.credentials - }).then(() => { - closeAllDesktopNotifications(rootState) - }) - }, - markSingleNotificationAsSeen ({ rootState, commit }, { id }) { - commit('markSingleNotificationAsSeen', { id }) - apiService.markNotificationsAsSeen({ - single: true, - id, - credentials: rootState.users.currentUser.credentials - }).then(() => { - closeDesktopNotification(rootState, id) - }) - }, - dismissNotificationLocal ({ rootState, commit }, { id }) { - commit('dismissNotification', { id }) - }, - dismissNotification ({ rootState, commit }, { id }) { - commit('dismissNotification', { id }) - rootState.api.backendInteractor.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/modules/users.js b/src/modules/users.js index 79268bc3f..6669d6230 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -498,7 +498,7 @@ const users = { store.commit('addNewUsers', users) store.commit('addNewUsers', targetUsers) - const notificationsObject = store.rootState.statuses.notifications.idStore + const notificationsObject = store.rootState.notifications.idStore const relevantNotifications = Object.entries(notificationsObject) .filter(([k, val]) => notificationIds.includes(k)) .map(([k, val]) => val) diff --git a/src/services/desktop_notification_utils/desktop_notification_utils.js b/src/services/desktop_notification_utils/desktop_notification_utils.js index dbca41735..bde9f18cb 100644 --- a/src/services/desktop_notification_utils/desktop_notification_utils.js +++ b/src/services/desktop_notification_utils/desktop_notification_utils.js @@ -7,7 +7,7 @@ const state = { failCreateNotif: false } export const showDesktopNotification = (rootState, desktopNotificationOpts) => { if (!('Notification' in window && window.Notification.permission === 'granted')) return - if (rootState.statuses.notifications.desktopNotificationSilence) { return } + if (rootState.notifications.desktopNotificationSilence) { return } if (isSWSupported()) { swDesktopNotification(desktopNotificationOpts) diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index 610ba1ab9..85da52234 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -439,7 +439,6 @@ export const parseNotification = (data) => { output.type = mastoDict[data.type] || data.type output.seen = data.pleroma.is_seen output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null - output.action = output.status // TODO: Refactor, this is unneeded output.target = output.type !== 'move' ? null : parseUser(data.target) diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js index 01cbd5f1c..675e744ea 100644 --- a/src/services/notification_utils/notification_utils.js +++ b/src/services/notification_utils/notification_utils.js @@ -6,7 +6,7 @@ import FaviconService from 'src/services/favicon_service/favicon_service.js' let cachedBadgeUrl = null -export const notificationsFromStore = store => store.state.statuses.notifications.data +export const notificationsFromStore = store => store.state.notifications.data export const visibleTypes = store => { const rootState = store.rootState || store.state @@ -82,7 +82,7 @@ export const unseenNotificationsFromStore = store => export const prepareNotificationObject = (notification, i18n) => { if (cachedBadgeUrl === null) { const favicon = FaviconService.getOriginalFavicons()[0] - cachedBadgeUrl = favicon.favcanvas.toDataURL() + cachedBadgeUrl = favicon.favimg.href } const notifObj = { diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index 6c2472107..3c280b74f 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -21,7 +21,7 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => { const args = { credentials } const { getters } = store const rootState = store.rootState || store.state - const timelineData = rootState.statuses.notifications + const timelineData = rootState.notifications const hideMutedPosts = getters.mergedConfig.hideMutedPosts args.includeTypes = mastoApiNotificationTypes diff --git a/src/services/sw/sw.js b/src/services/sw/sw.js index d4ae8f4fe..e28f91685 100644 --- a/src/services/sw/sw.js +++ b/src/services/sw/sw.js @@ -82,12 +82,18 @@ function sendSubscriptionToBackEnd (subscription, token, notificationVisibility) return responseData }) } -export async function initServiceWorker () { +export async function initServiceWorker (store) { if (!isSWSupported()) return await getOrCreateServiceWorker() navigator.serviceWorker.addEventListener('message', (event) => { + const { dispatch } = store console.log('SW MESSAGE', event) - // TODO actually act upon click (open drawer on mobile, open chat/thread etc) + const { type, ...rest } = event + + switch (type) { + case 'notificationClicked': + dispatch('notificationClicked', { id: rest.id }) + } }) }