342 lines
9.8 KiB
JavaScript
342 lines
9.8 KiB
JavaScript
import { defineStore } from 'pinia'
|
|
|
|
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'
|
|
import {
|
|
closeAllDesktopNotifications,
|
|
closeDesktopNotification,
|
|
} from 'src/services/desktop_notification_utils/desktop_notification_utils.js'
|
|
import {
|
|
isValidNotification,
|
|
maybeShowNotification,
|
|
} from 'src/services/notification_utils/notification_utils.js'
|
|
import { isStatusNotification } from 'src/services/notification_utils/notification_utils_sw.js'
|
|
|
|
export const defaultState = () => ({
|
|
// Prevents desktop notification spam on startup
|
|
desktopNotificationSilence: true,
|
|
|
|
// Pagination
|
|
maxId: '',
|
|
minId: '',
|
|
|
|
// Order
|
|
data: [],
|
|
|
|
// TODO: Implement!
|
|
// Useful for making notification as seen
|
|
// when interacting with status
|
|
statusNotificationRelations: new WeakMap(),
|
|
|
|
// ID to Object notification
|
|
idStore: new Map(),
|
|
|
|
// Reference to WS subscriber
|
|
socket: null,
|
|
|
|
// Indicates whether notifications receive push updates
|
|
streaming: false,
|
|
|
|
// Indicates whether notifications are SUPPOSED to be fetching
|
|
// this is partiualrly useful for when pausing/resuming. I.e.
|
|
// whether we need to start fetching again if timeline was resumed.
|
|
fetching: true,
|
|
|
|
// Reference to fetcher, used for polling for new notifications
|
|
// and manually fetching old notifications
|
|
fetcher: null,
|
|
|
|
// Whether notifications fetcher has been paused - it stops fetching
|
|
// (but still receives pushes!)
|
|
paused: false,
|
|
})
|
|
|
|
export const useNotificationsStore = defineStore('notifications', {
|
|
state: defaultState,
|
|
actions: {
|
|
// Init
|
|
attachSocket() {
|
|
const et = new EventTarget()
|
|
const socket = {
|
|
name: 'notifications',
|
|
et,
|
|
}
|
|
|
|
et.addEventListener('notification', this.addNewNotifications)
|
|
et.addEventListener('open', this.onStreamConnect)
|
|
et.addEventListener('close', this.onStreamDisconnect)
|
|
|
|
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')
|
|
}
|
|
},
|
|
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)
|
|
|
|
if (this.fetcher) throw new Error('Fetcher already exists!')
|
|
this.fetcher = notificationsFetcher(useOAuthStore().token)
|
|
this.startFetching('Notifications activated')
|
|
},
|
|
deactivate() {
|
|
if (this.fetching) {
|
|
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)
|
|
|
|
const blankState = defaultState()
|
|
Object.keys(blankState).forEach((k) => {
|
|
this[k] = blankState[k]
|
|
})
|
|
console.debug('[Notifications] Deactivated', this.fetcher)
|
|
},
|
|
|
|
// 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,
|
|
)
|
|
this.fetcher.startFetching()
|
|
this.fetching = true
|
|
},
|
|
stopFetching(reason) {
|
|
this.fetcher.stopFetching()
|
|
this.fetching = false
|
|
console.debug(
|
|
'[Notifications] Stopped notifications fetcher',
|
|
'Reason:',
|
|
reason,
|
|
)
|
|
},
|
|
|
|
// Updates
|
|
updateExtremes(id) {
|
|
if (this.maxId === '' || id > this.maxId) {
|
|
this.maxId = id
|
|
}
|
|
if (this.minId === '' || id < this.minId) {
|
|
this.minId = id
|
|
}
|
|
},
|
|
addNewNotifications(result, older) {
|
|
const { timestamp, data } = result
|
|
|
|
const notifications = older ? data : [...data].reverse()
|
|
|
|
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)
|
|
this.updateExtremes(notification.id)
|
|
return false
|
|
}
|
|
return true
|
|
})
|
|
|
|
useUsersStore().addNewUsers({
|
|
timestamp,
|
|
data: validNotifications.map(
|
|
(notification) => notification.from_profile,
|
|
),
|
|
})
|
|
|
|
const statusNotifications = validNotifications.filter(
|
|
(notification) =>
|
|
isStatusNotification(notification.type) && notification.status,
|
|
)
|
|
|
|
// Synchronous commit to add all the statuses
|
|
useStatusesStore().addNewStatuses({
|
|
timestamp,
|
|
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 = useStatusesStore().allStatuses.get(id)
|
|
|
|
if (referenceStatus) {
|
|
notification.status = referenceStatus
|
|
}
|
|
})
|
|
|
|
validNotifications.forEach((notification) => {
|
|
if (notification.type === 'pleroma:report') {
|
|
useReportsStore().addReport(notification.report)
|
|
}
|
|
|
|
if (notification.type === 'pleroma:emoji_reaction') {
|
|
useStatusesStore().fetchEmojiReactions(notification.status.id)
|
|
}
|
|
|
|
// Only add a new notification if we don't have one for the same action
|
|
if (!this.idStore.has(notification.id)) {
|
|
this.updateExtremes(notification.id)
|
|
|
|
if (older) {
|
|
this.data.push(notification)
|
|
} else {
|
|
this.data.unshift(notification)
|
|
}
|
|
this.idStore.set(notification.id, notification)
|
|
|
|
if (notification.status) {
|
|
this.statusNotificationRelations.set(
|
|
notification.status,
|
|
this.idStore.get(notification.id),
|
|
)
|
|
}
|
|
|
|
maybeShowNotification(
|
|
useMergedConfigStore().mergedConfig.notificationVisibility,
|
|
Object.values(
|
|
useSyncConfigStore().prefsStorage.simple.muteFilters ?? {},
|
|
),
|
|
notification,
|
|
useI18nStore().i18n,
|
|
)
|
|
} else if (notification.seen) {
|
|
this.idStore.get(notification.id).seen = true
|
|
}
|
|
})
|
|
},
|
|
|
|
// Seen / Dismiss
|
|
notificationClicked(id) {
|
|
const notification = this.idStore.get(id)
|
|
const { type, seen } = notification
|
|
|
|
if (!seen) {
|
|
switch (type) {
|
|
case 'mention':
|
|
case 'pleroma:report':
|
|
case 'follow_request':
|
|
break
|
|
default:
|
|
this.markSingleNotificationAsSeen(id)
|
|
}
|
|
}
|
|
},
|
|
markNotificationsAsSeen() {
|
|
this.data.forEach((notification) => {
|
|
notification.seen = true
|
|
})
|
|
|
|
markNotificationsAsSeen({
|
|
id: this.maxId,
|
|
credentials: useOAuthStore().token,
|
|
}).then(() => {
|
|
closeAllDesktopNotifications()
|
|
})
|
|
},
|
|
markSingleNotificationAsSeen(id) {
|
|
const notification = this.idStore.get(id)
|
|
if (notification) notification.seen = true
|
|
|
|
markNotificationsAsSeen({
|
|
single: true,
|
|
id,
|
|
credentials: useOAuthStore().token,
|
|
}).then(() => {
|
|
closeDesktopNotification(id)
|
|
})
|
|
},
|
|
dismissNotificationLocal(id) {
|
|
this.idStore.delete(id)
|
|
this.syncOrder()
|
|
},
|
|
dismissNotification(id) {
|
|
this.dismissNotificationLocal(id)
|
|
|
|
dismissNotification({
|
|
id,
|
|
credentials: useOAuthStore().token,
|
|
})
|
|
},
|
|
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) => {
|
|
const status =
|
|
isStatusNotification(notification.type) && notification.status
|
|
if (status && set.has(status.id)) {
|
|
this.idStore.delete(notification.id)
|
|
}
|
|
})
|
|
|
|
this.syncOrder()
|
|
},
|
|
},
|
|
})
|