pleroma-fe/src/stores/notifications.js

318 lines
9.2 KiB
JavaScript
Raw Normal View History

2026-08-10 17:43:24 +03:00
import { defineStore } from 'pinia'
2026-08-19 19:19:59 +03:00
import notificationsFetcher from 'src/stores/fetchers/notifications_fetcher.js'
import { useI18nStore } from 'src/stores/i18n.js'
import { useMergedConfigStore } from 'src/stores/merged_config.js'
import { useOAuthStore } from 'src/stores/oauth.js'
import { useReportsStore } from 'src/stores/reports.js'
import { useStatusesStore } from 'src/stores/statuses.js'
import { useStreamingStore } from 'src/stores/streaming.js'
import { useSyncConfigStore } from 'src/stores/sync_config.js'
import { useUsersStore } from 'src/stores/users.js'
import { dismissNotification, markNotificationsAsSeen } from 'src/api/user.js'
2026-01-06 16:23:17 +02:00
import {
closeAllDesktopNotifications,
closeDesktopNotification,
2026-08-13 16:45:44 +03:00
} from 'src/services/desktop_notification_utils/desktop_notification_utils.js'
import {
isValidNotification,
2026-01-06 16:22:52 +02:00
maybeShowNotification,
2026-08-13 16:45:44 +03:00
} from 'src/services/notification_utils/notification_utils.js'
import { isStatusNotification } from 'src/services/notification_utils/notification_utils_sw.js'
2026-08-10 17:43:24 +03:00
export const defaultState = () => ({
desktopNotificationSilence: true,
2026-08-13 16:45:44 +03:00
maxId: '',
minId: '',
data: [],
2026-08-10 17:43:24 +03:00
statusNotificationRelations: new WeakMap(),
idStore: new Map(),
2026-08-19 19:19:59 +03:00
statusIdStore: new Set(),
2026-08-13 16:45:44 +03:00
socket: null,
streaming: false,
fetching: true,
2026-08-13 16:45:44 +03:00
fetcher: null,
paused: false,
})
2026-08-10 17:43:24 +03:00
export const useNotificationsStore = defineStore('notifications', {
state: defaultState,
actions: {
2026-08-13 16:45:44 +03:00
// Init
attachSocket() {
const et = new EventTarget()
const socket = {
name: 'notifications',
et,
}
2026-08-13 16:45:44 +03:00
et.addEventListener('notification', this.addNewNotifications)
et.addEventListener('open', this.onStreamConnect)
et.addEventListener('close', this.onStreamDisconnect)
2026-08-13 16:45:44 +03:00
this.socket = socket
useStreamingStore().addSubscriber(this.socket)
},
pause() {
this.paused = true
if (this.fetcher && this.fetching) {
this.stopFetching('Notifications paused')
}
},
resume() {
this.paused = false
if (this.fetcher && this.fetching) {
this.startFetching('Notifications resumed')
}
2026-08-13 16:45:44 +03:00
},
activate() {
this.attachSocket()
// 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(() => (this.desktopNotificationSilence = false), 10000)
2026-08-13 16:45:44 +03:00
if (this.fetcher) throw new Error('Fetcher already exists!')
this.fetcher = notificationsFetcher(useOAuthStore().token)
this.startFetching('Notifications activated')
},
deactivate() {
if (this.fetching) {
2026-08-13 16:45:44 +03:00
this.stopFetching('Notifications deactivated')
}
useStreamingStore().removeSubscriber(this.socket)
const { et } = this.socket
et.removeEventListener('notification', this.addNewNotifications)
et.removeEventListener('open', this.onStreamConnect)
et.removeEventListener('close', this.onStreamDisconnect)
2026-08-10 17:43:24 +03:00
2026-08-13 16:45:44 +03:00
const blankState = defaultState()
Object.keys(blankState).forEach((k) => {
2026-08-10 17:43:24 +03:00
this[k] = blankState[k]
2025-02-04 15:23:21 +02:00
})
2026-08-25 21:57:47 +03:00
console.debug('[Notifications] Deactivated', this.fetcher)
},
2026-08-13 16:45:44 +03:00
// Poll & Push
onStreamConnect() {
console.debug('[Notifications] Notifications stream connected')
this.streaming = true
this.stopFetching('Socket connected')
},
onStreamDisconnect() {
console.debug('[Notifications] Notifications stream disconnected')
this.streaming = false
this.startFetching('Socket disconnected')
},
startFetching(reason) {
if (this.paused) {
console.debug(
'[Notificatiosn] NOT Starting notifications fetcher because it is paused',
'Original Reason:',
reason,
)
return
}
console.debug(
'[Notifications] Starting notifications fetcher',
'Reason:',
reason,
)
2026-08-13 16:45:44 +03:00
this.fetcher.startFetching()
this.fetching = true
2026-08-13 16:45:44 +03:00
},
stopFetching(reason) {
this.fetcher.stopFetching()
this.fetching = false
console.debug(
'[Notifications] Stopped notifications fetcher',
'Reason:',
reason,
)
2026-08-13 16:45:44 +03:00
},
// Updates
2026-08-19 19:19:59 +03:00
updateExtremes(id) {
if (this.maxId === '' || id > this.maxId) {
this.maxId = id
}
if (this.minId === '' || id < this.minId) {
this.minId = id
}
2026-01-06 16:22:52 +02:00
},
2026-08-19 19:19:59 +03:00
addNewNotifications(result, older) {
const { timestamp, data } = result
2026-08-19 23:46:51 +03:00
const notifications = older ? data : [...data].reverse()
2026-08-10 17:43:24 +03:00
useUsersStore().addNewUsers({
timestamp,
data: notifications.map((n) => n.from_profile),
})
2026-08-19 19:19:59 +03:00
notifications.forEach((n) => {
n.from_profile = useUsersStore().findUser(n.from_profile.id)
})
2026-08-10 17:43:24 +03:00
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)
2026-08-19 19:19:59 +03:00
this.updateExtremes(notification.id)
return false
}
return true
})
2026-08-12 15:59:05 +03:00
useUsersStore().addNewUsers({
timestamp,
2026-08-13 01:12:48 +03:00
data: validNotifications.map(
(notification) => notification.from_profile,
),
2026-08-12 15:59:05 +03:00
})
2026-01-06 16:22:52 +02:00
const statusNotifications = validNotifications.filter(
(notification) =>
isStatusNotification(notification.type) && notification.status,
)
// Synchronous commit to add all the statuses
2026-08-10 22:26:22 +03:00
useStatusesStore().addNewStatuses({
2026-08-10 17:43:24 +03:00
timestamp,
2026-01-06 16:22:52 +02:00
statuses: statusNotifications.map(
(notification) => notification.status,
),
})
// Update references to statuses in notifications to ones in the store
2026-01-06 16:22:52 +02:00
statusNotifications.forEach((notification) => {
const id = notification.status.id
2026-08-10 22:26:22 +03:00
const referenceStatus = useStatusesStore().allStatuses.get(id)
if (referenceStatus) {
notification.status = referenceStatus
}
})
2026-01-06 16:22:52 +02:00
validNotifications.forEach((notification) => {
if (notification.type === 'pleroma:report') {
2025-01-30 21:56:07 +02:00
useReportsStore().addReport(notification.report)
}
if (notification.type === 'pleroma:emoji_reaction') {
2026-08-18 18:00:36 +03:00
useStatusesStore().fetchEmojiReactions(notification.status.id)
}
// Only add a new notification if we don't have one for the same action
2026-08-10 17:43:24 +03:00
if (!this.idStore.has(notification.id)) {
2026-08-19 19:19:59 +03:00
this.updateExtremes(notification.id)
2026-08-10 17:43:24 +03:00
2026-08-19 19:19:59 +03:00
if (older) {
2026-08-10 17:43:24 +03:00
this.data.push(notification)
2026-08-19 19:19:59 +03:00
} else {
this.data.unshift(notification)
}
this.idStore.set(notification.id, notification)
2026-08-10 17:43:24 +03:00
2026-08-21 16:23:19 +03:00
if (notification.status) {
this.statusNotificationRelations.set(
notification.status,
this.idStore.get(notification.id),
)
}
maybeShowNotification(
useMergedConfigStore().mergedConfig.notificationVisibility,
2026-08-19 23:46:51 +03:00
Object.values(
useSyncConfigStore().prefsStorage.simple.muteFilters ?? {},
),
2026-01-06 16:22:52 +02:00
notification,
useI18nStore().i18n,
)
} else if (notification.seen) {
2026-08-10 17:43:24 +03:00
this.idStore.get(notification.id).seen = true
}
})
},
2026-08-13 16:45:44 +03:00
// Seen / Dismiss
2026-08-10 17:43:24 +03:00
notificationClicked(id) {
const notification = this.idStore.get(id)
2023-11-16 20:41:41 +02:00
const { type, seen } = notification
if (!seen) {
switch (type) {
case 'mention':
case 'pleroma:report':
case 'follow_request':
break
default:
2026-08-25 18:01:07 +03:00
this.markSingleNotificationAsSeen(id)
2023-11-16 20:41:41 +02:00
}
}
},
2026-08-10 17:43:24 +03:00
markNotificationsAsSeen() {
this.data.forEach((notification) => {
notification.seen = true
})
2026-06-13 03:10:00 +03:00
markNotificationsAsSeen({
2026-08-10 17:43:24 +03:00
id: this.maxId,
2026-08-19 19:19:59 +03:00
credentials: useOAuthStore().token,
2026-06-13 03:10:00 +03:00
}).then(() => {
2026-08-10 17:43:24 +03:00
closeAllDesktopNotifications()
2026-06-13 03:10:00 +03:00
})
2026-01-06 16:22:52 +02:00
},
2026-08-19 19:19:59 +03:00
markSingleNotificationAsSeen(id) {
2026-08-10 17:43:24 +03:00
const notification = this.idStore.get(id)
if (notification) notification.seen = true
2026-06-13 03:10:00 +03:00
markNotificationsAsSeen({
single: true,
id,
2026-08-19 19:19:59 +03:00
credentials: useOAuthStore().token,
2026-06-13 03:10:00 +03:00
}).then(() => {
2026-08-10 17:43:24 +03:00
closeDesktopNotification(id)
2026-06-13 03:10:00 +03:00
})
2026-01-06 16:22:52 +02:00
},
2026-08-10 17:43:24 +03:00
dismissNotificationLocal(id) {
2026-08-19 19:19:59 +03:00
this.idStore.delete(id)
this.syncOrder()
},
2026-08-10 17:43:24 +03:00
dismissNotification(id) {
this.dismissNotificationLocal(id)
2026-06-15 20:02:22 +03:00
dismissNotification({
id,
2026-06-16 17:32:26 +03:00
credentials: useOAuthStore().token,
2026-06-15 20:02:22 +03:00
})
},
2026-08-19 19:19:59 +03:00
syncOrder() {
this.minId = ''
this.maxId = ''
this.data = this.data.filter(({ id }) => {
const present = this.idStore.has(id)
if (present) {
this.updateExtremes(id) // Side-effect
}
return present
})
},
wipeStatuses(ids) {
const set = new Set(ids)
this.data.forEach((notification) => {
2026-08-19 23:46:51 +03:00
const status =
isStatusNotification(notification.type) && notification.status
2026-08-19 19:19:59 +03:00
if (status && set.has(status.id)) {
this.idStore.delete(notification.id)
}
})
2026-08-13 16:45:44 +03:00
2026-08-19 19:19:59 +03:00
this.syncOrder()
2026-08-13 16:45:44 +03:00
},
2026-01-06 16:22:52 +02:00
},
2026-08-10 17:43:24 +03:00
})