notifications work
This commit is contained in:
parent
c13ee07530
commit
7e6d835125
5 changed files with 250 additions and 133 deletions
|
|
@ -7,7 +7,6 @@ import { useNotificationsStore } from 'src/stores/notifications.js'
|
|||
|
||||
import { fetchTimeline } from 'src/api/timelines.js'
|
||||
|
||||
//
|
||||
// For using include_types when fetching notifications.
|
||||
// Note: chat_mention excluded as pleroma-fe polls them separately
|
||||
const mastoApiNotificationTypes = new Set([
|
||||
|
|
@ -23,7 +22,11 @@ const mastoApiNotificationTypes = new Set([
|
|||
'pleroma:report',
|
||||
])
|
||||
|
||||
const fetchAndUpdate = ({ credentials, older = false, sinceId }) => {
|
||||
const fetchAndUpdate = (
|
||||
{ credentials },
|
||||
{ older = false, sinceId }
|
||||
) => {
|
||||
useNotificationsStore().setLoading(true)
|
||||
const args = { credentials }
|
||||
const timelineData = useNotificationsStore()
|
||||
const hideMutedPosts = useMergedConfigStore().mergedConfig.hideMutedPosts
|
||||
|
|
@ -107,24 +110,38 @@ const fetchNotifications = ({ args, older }) => {
|
|||
})
|
||||
console.error(error)
|
||||
})
|
||||
.finally(() => {
|
||||
useNotificationsStore().setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
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(
|
||||
() => useNotificationsStore().setNotificationsSilence(false),
|
||||
10000,
|
||||
)
|
||||
const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store })
|
||||
boundFetchAndUpdate()
|
||||
return promiseInterval(boundFetchAndUpdate, 10000)
|
||||
}
|
||||
|
||||
const notificationsFetcher = {
|
||||
fetchAndUpdate,
|
||||
startFetching,
|
||||
const notificationsFetcher = (credentials) => {
|
||||
const state = {
|
||||
interval: null,
|
||||
}
|
||||
|
||||
const boundFetchAndUpdate = ({ older = false, sinceId } = {}) =>
|
||||
fetchAndUpdate({ credentials }, { older, sinceId })
|
||||
|
||||
const startFetching = () => {
|
||||
if (state.interval) throw new Error('Interval already exists!')
|
||||
|
||||
boundFetchAndUpdate()
|
||||
|
||||
state.interval = promiseInterval(boundFetchAndUpdate, 10000)
|
||||
}
|
||||
|
||||
const stopFetching = () => {
|
||||
state.interval.stop()
|
||||
state.interval = null
|
||||
}
|
||||
|
||||
return {
|
||||
startFetching,
|
||||
stopFetching,
|
||||
fetchAndUpdate: boundFetchAndUpdate,
|
||||
}
|
||||
}
|
||||
|
||||
export default notificationsFetcher
|
||||
|
|
|
|||
|
|
@ -3,12 +3,13 @@ import { defineStore } from 'pinia'
|
|||
import {
|
||||
closeAllDesktopNotifications,
|
||||
closeDesktopNotification,
|
||||
} from '../services/desktop_notification_utils/desktop_notification_utils.js'
|
||||
} from 'src/services/desktop_notification_utils/desktop_notification_utils.js'
|
||||
import {
|
||||
isValidNotification,
|
||||
maybeShowNotification,
|
||||
} from '../services/notification_utils/notification_utils.js'
|
||||
import { isStatusNotification } from '../services/notification_utils/notification_utils_sw.js'
|
||||
} from 'src/services/notification_utils/notification_utils.js'
|
||||
import { isStatusNotification } from 'src/services/notification_utils/notification_utils_sw.js'
|
||||
import notificationsFetcher from 'src/services/notifications_fetcher/notifications_fetcher.service.js'
|
||||
|
||||
import { useI18nStore } from 'src/stores/i18n.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
|
|
@ -16,40 +17,113 @@ import { useOAuthStore } from 'src/stores/oauth.js'
|
|||
import { useReportsStore } from 'src/stores/reports.js'
|
||||
import { useStatusesStore } from 'src/stores/statuses.js'
|
||||
import { useSyncConfigStore } from 'src/stores/sync_config.js'
|
||||
import { useStreamingStore } from 'src/stores/streaming.js'
|
||||
import { useUsersStore } from 'src/stores/users.js'
|
||||
|
||||
import { dismissNotification, markNotificationsAsSeen } from 'src/api/user.js'
|
||||
|
||||
export const defaultState = () => ({
|
||||
desktopNotificationSilence: true,
|
||||
maxId: 0,
|
||||
minId: Number.POSITIVE_INFINITY,
|
||||
maxId: '',
|
||||
minId: '',
|
||||
data: [],
|
||||
statusNotificationRelations: new WeakMap(),
|
||||
idStore: new Map(),
|
||||
loading: false,
|
||||
socket: null,
|
||||
streaming: false,
|
||||
fetcher: null,
|
||||
})
|
||||
|
||||
export const useNotificationsStore = defineStore('notifications', {
|
||||
state: defaultState,
|
||||
actions: {
|
||||
clearNotifications() {
|
||||
const blankState = defaultState()
|
||||
// Init
|
||||
attachSocket() {
|
||||
const et = new EventTarget()
|
||||
const handleNotificationMessage = ({ data, timestamp }) => {
|
||||
console.log(data)
|
||||
this.addNewNotifications({ statuses: [data.notification], timestamp })
|
||||
}
|
||||
const notificationHandler = ({ detail: message }) => {
|
||||
handleNotificationMessage(message)
|
||||
}
|
||||
const openHandler = () => this.onStreamConnect()
|
||||
const closeHandler = () => this.onStreamDisconnect()
|
||||
const socket = {
|
||||
et,
|
||||
handlers: {
|
||||
openHandler,
|
||||
closeHandler,
|
||||
notificationHandler,
|
||||
}
|
||||
}
|
||||
|
||||
Object.keys(defaultState()).forEach((k) => {
|
||||
et.addEventListener('notification', notificationHandler)
|
||||
et.addEventListener('open', openHandler)
|
||||
et.addEventListener('close', closeHandler)
|
||||
|
||||
useStreamingStore().addSubscriber(socket)
|
||||
this.socket = socket
|
||||
},
|
||||
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.streaming) {
|
||||
this.stopFetching('Notifications deactivated')
|
||||
}
|
||||
|
||||
useStreamingStore().removeSubscriber(this.socket)
|
||||
const { openHandler, closeHandler, notificationHandler } = timeline.socket.handlers
|
||||
|
||||
this.socket.et.removeEventListener('notification', openHandler)
|
||||
this.socket.et.removeEventListener('notification', closeHandler)
|
||||
this.socket.et.removeEventListener('notification', notificationHandler)
|
||||
|
||||
const blankState = defaultState()
|
||||
Object.keys(blankState).forEach((k) => {
|
||||
this[k] = blankState[k]
|
||||
})
|
||||
},
|
||||
|
||||
// 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) {
|
||||
console.debug('[Notifications] Starting fetching notifications', 'Reason:', reason)
|
||||
this.fetcher.startFetching()
|
||||
},
|
||||
stopFetching(reason) {
|
||||
console.debug('[Notifications] Stopped fetching notifications', 'Reason:', reason)
|
||||
this.fetcher.stopFetching()
|
||||
},
|
||||
|
||||
// Updates
|
||||
updateNotificationsMinMaxId(id) {
|
||||
this.maxId = id > this.maxId ? id : this.maxId
|
||||
this.minId = id < this.minId ? id : this.minId
|
||||
},
|
||||
setNotificationsLoading(value) {
|
||||
this.loading = value
|
||||
},
|
||||
setNotificationsSilence(value) {
|
||||
this.desktopNotificationSilence = value
|
||||
},
|
||||
updateNotification({ id, updater }) {
|
||||
const notification = this.idStore.get(id)
|
||||
notification && updater(notification)
|
||||
|
|
@ -139,6 +213,8 @@ export const useNotificationsStore = defineStore('notifications', {
|
|||
}
|
||||
})
|
||||
},
|
||||
|
||||
// Seen / Dismiss
|
||||
notificationClicked(id) {
|
||||
const notification = this.idStore.get(id)
|
||||
const { type, seen } = notification
|
||||
|
|
@ -190,5 +266,10 @@ export const useNotificationsStore = defineStore('notifications', {
|
|||
credentials: useOAuthStore().token,
|
||||
})
|
||||
},
|
||||
|
||||
// Misc
|
||||
setLoading(value) {
|
||||
this.loading = value
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export const defaultState = () => ({
|
|||
scrobblesNextFetch: {},
|
||||
conversations: new Map(),
|
||||
favorites: new Set(),
|
||||
socket: null,
|
||||
})
|
||||
|
||||
const getLatestScrobble = (user) => {
|
||||
|
|
@ -84,22 +85,36 @@ export const useStatusesStore = defineStore('statuses', {
|
|||
const handleStatusMessage = ({ data, timestamp }) => {
|
||||
this.addNewStatuses({ statuses: [data.status], timestamp })
|
||||
}
|
||||
|
||||
et.addEventListener('update', ({ detail: message }) => {
|
||||
handleStatusMessage(message)
|
||||
})
|
||||
|
||||
et.addEventListener('status.update', ({ detail: message }) => {
|
||||
handleStatusMessage(message)
|
||||
})
|
||||
|
||||
et.addEventListener('delete', ({ detail: message }) => {
|
||||
const handleUpdate = ({ detail: message }) => handleStatusMessage(message)
|
||||
const handleDelete = ({ detail: message }) => {
|
||||
console.log('DELETE', message)
|
||||
this.deleteStatus(message.data)
|
||||
})
|
||||
}
|
||||
const socket = {
|
||||
et,
|
||||
handlers: {
|
||||
handleUpdate, handleDelete
|
||||
}
|
||||
}
|
||||
|
||||
useStreamingStore().addSubscriber({ et })
|
||||
et.addEventListener('update', handleUpdate)
|
||||
et.addEventListener('status.update', handleUpdate)
|
||||
et.addEventListener('delete', handleDelete)
|
||||
|
||||
useStreamingStore().addSubscriber(socket)
|
||||
this.socket = socket
|
||||
},
|
||||
resetStatuses() {
|
||||
this.socket.et.removeEventListener('update', this.socket.handleUpdate)
|
||||
this.socket.et.removeEventListener('status.update', this.socket.handleUpdate)
|
||||
this.socket.et.removeEventListener('delete', this.socket.handleDelete)
|
||||
|
||||
const emptyState = defaultState()
|
||||
Object.entries(emptyState).forEach(([key, value]) => {
|
||||
this[key] = value
|
||||
})
|
||||
},
|
||||
|
||||
addNewStatuses({ statuses, timestamp }) {
|
||||
// Sanity check
|
||||
if (!Array.isArray(statuses)) {
|
||||
|
|
@ -209,6 +224,8 @@ export const useStatusesStore = defineStore('statuses', {
|
|||
|
||||
return [map.get(newStatus.id), true]
|
||||
},
|
||||
|
||||
// Fetches
|
||||
fetchStatus(id) {
|
||||
return fetchStatus({ id }).then(({ data: status, timestamp }) =>
|
||||
this.addNewStatuses({ statuses: [status], timestamp }),
|
||||
|
|
|
|||
|
|
@ -75,6 +75,86 @@ export const defaultState = () => {
|
|||
export const useTimelinesStore = defineStore('timelines', {
|
||||
state: defaultState,
|
||||
actions: {
|
||||
// (De)Initialization stuff
|
||||
activate(timelineName, argument, persistent) {
|
||||
const timeline = this[timelineName]
|
||||
if (timeline.persistent && !persistent) return
|
||||
|
||||
if (
|
||||
timelineName === 'favourites' &&
|
||||
!useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
timeline.fetcher = timelineFetcher(
|
||||
timeline,
|
||||
argument,
|
||||
useOAuthStore().token,
|
||||
)
|
||||
|
||||
this.startFetchingTimeline(timelineName, argument, 'Timeline activated')
|
||||
|
||||
const streamName = TIMELINE_STREAM_MAP[timelineName]
|
||||
|
||||
if (streamName) {
|
||||
const et = new EventTarget()
|
||||
const openHandler = () => this.onStreamConnect(timelineName, argument)
|
||||
const closeHandler = () => this.onStreamDisconnect(timelineName, argument)
|
||||
const messageHandler = () => ({ detail: message }) =>
|
||||
this.onStreamMessage(timelineName, argument, message)
|
||||
|
||||
et.addEventListener('open', openHandler)
|
||||
et.addEventListener('close', closeHandler)
|
||||
et.addEventListener('update', messageHandler)
|
||||
|
||||
timeline.socket = {
|
||||
stream: {
|
||||
name: streamName,
|
||||
argument,
|
||||
},
|
||||
et,
|
||||
handlers: {
|
||||
openHandler,
|
||||
closeHandler,
|
||||
messageHandler,
|
||||
}
|
||||
}
|
||||
|
||||
useStreamingStore().addSubscriber(timeline.socket)
|
||||
}
|
||||
},
|
||||
deactivate(timelineName, persistent) {
|
||||
const timeline = this[timelineName]
|
||||
if (timeline.persistent && !persistent) return
|
||||
if (!timeline.streaming) {
|
||||
this.stopFetchingTimeline(timelineName, 'Timeline deactivation')
|
||||
}
|
||||
|
||||
if (data.socket) {
|
||||
useStreamingStore().removeSubscriber(timeline.socket)
|
||||
const { openHandler, closeHandler, messageHandler } = timeline.socket.handlers
|
||||
et.removeEventListener('open', openHandler)
|
||||
et.removeEventListener('close', closeHandler)
|
||||
et.removeEventListener('message', messageHandler)
|
||||
}
|
||||
|
||||
this[timelineName] = emptyTl(timelineName)
|
||||
},
|
||||
activatePersistents() {
|
||||
TIMELINES.forEach((name) => {
|
||||
if (this[name].persistent) {
|
||||
this.activate(name, undefined, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
deactivateAll() {
|
||||
TIMELINES.forEach((name) => {
|
||||
this.deactivate(name, true)
|
||||
})
|
||||
},
|
||||
|
||||
// Update stuff
|
||||
addStatusesToTimeline(
|
||||
timelineName,
|
||||
argument,
|
||||
|
|
@ -146,65 +226,6 @@ export const useTimelinesStore = defineStore('timelines', {
|
|||
}
|
||||
})
|
||||
},
|
||||
|
||||
activatePersistents() {
|
||||
TIMELINES.forEach((name) => {
|
||||
if (this[name].persistent) {
|
||||
this.activate(name, undefined, true)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
activate(timelineName, argument, persistent) {
|
||||
const timeline = this[timelineName]
|
||||
if (timeline.persistent && !persistent) return
|
||||
|
||||
if (
|
||||
timelineName === 'favourites' &&
|
||||
!useInstanceCapabilitiesStore().pleromaPublicFavouritesAvailable
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
timeline.fetcher = timelineFetcher(
|
||||
timeline,
|
||||
argument,
|
||||
useOAuthStore().token,
|
||||
)
|
||||
|
||||
this.startFetchingTimeline(timelineName, argument)
|
||||
|
||||
const streamName = TIMELINE_STREAM_MAP[timelineName]
|
||||
|
||||
if (streamName) {
|
||||
const et = new EventTarget()
|
||||
et.addEventListener('open', () =>
|
||||
this.onStreamConnect(timelineName, argument),
|
||||
)
|
||||
et.addEventListener('close', () =>
|
||||
this.onStreamDisconnect(timelineName, argument),
|
||||
)
|
||||
et.addEventListener('update', ({ detail: message }) =>
|
||||
this.onStreamMessage(timelineName, argument, message),
|
||||
)
|
||||
|
||||
timeline.socket = {
|
||||
stream: {
|
||||
name: streamName,
|
||||
argument,
|
||||
},
|
||||
et,
|
||||
}
|
||||
|
||||
useStreamingStore().addSubscriber(timeline.socket)
|
||||
}
|
||||
},
|
||||
deactivate(timelineName) {
|
||||
const timeline = this[timelineName]
|
||||
if (timeline.persistent) return
|
||||
this.clearTimeline(timelineName)
|
||||
},
|
||||
|
||||
onStreamMessage(timeline, argument, event) {
|
||||
// This relies on statuses store to process this event first
|
||||
const status = useStatusesStore().allStatuses.get(event.data.status.id)
|
||||
|
|
@ -214,26 +235,24 @@ export const useTimelinesStore = defineStore('timelines', {
|
|||
})
|
||||
},
|
||||
|
||||
// Poll & Push
|
||||
onStreamConnect(timeline) {
|
||||
console.log('STREAM OK', timeline)
|
||||
console.debug('[Timelines] Stream connected', timeline)
|
||||
this[timeline].streaming = true
|
||||
this.stopFetchingTimeline(timeline)
|
||||
this.stopFetchingTimeline(timeline, 'Socket connected')
|
||||
},
|
||||
|
||||
onStreamDisconnect(timeline, argument) {
|
||||
console.log('STREAM DED', timeline, argument)
|
||||
console.debug('[Timelines] Stream disconnected', timeline, argument)
|
||||
this[timeline].streaming = false
|
||||
this.startFetchingTimeline(timeline, argument)
|
||||
this.startFetchingTimeline(timeline, argument, 'Socket disconnected')
|
||||
},
|
||||
|
||||
// Fetchers
|
||||
startFetchingTimeline(timelineName, argument) {
|
||||
console.log('START FETCHING', timelineName, argument)
|
||||
startFetchingTimeline(timelineName, argument, reason) {
|
||||
console.debug('[Timelines] Starting fetching timeline', timelineName, argument, 'Reason:', reason)
|
||||
const timeline = this[timelineName]
|
||||
timeline.fetcher.startFetching()
|
||||
},
|
||||
stopFetchingTimeline(timelineName) {
|
||||
console.log('STOP FETCHING', timelineName)
|
||||
stopFetchingTimeline(timelineName, reason) {
|
||||
console.debug('[Timelines] Stopped fetching timeline', timelineName, 'Reason:', reason)
|
||||
const timeline = this[timelineName]
|
||||
timeline.fetcher.stopFetching()
|
||||
},
|
||||
|
|
@ -254,13 +273,6 @@ export const useTimelinesStore = defineStore('timelines', {
|
|||
timeline.minId = minNew
|
||||
}
|
||||
},
|
||||
resetStatuses() {
|
||||
const emptyState = defaultState()
|
||||
|
||||
Object.entries(emptyState).forEach(([key, value]) => {
|
||||
this[key] = value
|
||||
})
|
||||
},
|
||||
showNewStatuses(timelineName) {
|
||||
const timeline = this[timelineName]
|
||||
|
||||
|
|
@ -276,15 +288,6 @@ export const useTimelinesStore = defineStore('timelines', {
|
|||
this.updateTimelineExtremes(timeline, [...timeline.statuses.keys()])
|
||||
},
|
||||
clearTimeline(timeline) {
|
||||
console.log('CLEAR TIMELINE', timeline)
|
||||
const data = this[timeline]
|
||||
if (!data.streaming) {
|
||||
this.stopFetchingTimeline(timeline)
|
||||
}
|
||||
if (data.socket) {
|
||||
useStreamingStore().removeSubscriber(data.socket)
|
||||
}
|
||||
this[timeline] = emptyTl(timeline)
|
||||
},
|
||||
queueFlush(timeline, id) {
|
||||
this[timeline].flushMarker = id
|
||||
|
|
|
|||
|
|
@ -651,13 +651,12 @@ export const useUsersStore = defineStore('users', {
|
|||
})
|
||||
.then(() => {
|
||||
this.clearCurrentUser()
|
||||
store.dispatch('disconnectFromSocket')
|
||||
store.dispatch('stopFetchingTimeline', 'friends')
|
||||
store.dispatch('stopFetchingNotifications')
|
||||
useListsStore().stopFetching()
|
||||
useBookmarkFoldersStore().stopFetching()
|
||||
store.dispatch('stopFetchingFollowRequests')
|
||||
store.commit('clearNotifications')
|
||||
useTimelinesStore().deactivateAll()
|
||||
useStatusesStore().resetStatuses()
|
||||
useNotificationsStore().clearNotifications()
|
||||
useChatsStore().resetChats()
|
||||
|
|
@ -726,6 +725,8 @@ export const useUsersStore = defineStore('users', {
|
|||
}
|
||||
|
||||
// DMs and Home
|
||||
useStatusesStore().attachSocket()
|
||||
useNotificationsStore().activate()
|
||||
useTimelinesStore().activatePersistents()
|
||||
|
||||
if (useInstanceCapabilitiesStore().pleromaChatMessagesAvailable) {
|
||||
|
|
@ -735,8 +736,6 @@ export const useUsersStore = defineStore('users', {
|
|||
|
||||
useListsStore().startFetching()
|
||||
useBookmarkFoldersStore().startFetching()
|
||||
useStatusesStore().attachSocket()
|
||||
//useNotificationsStore().attachSocket()
|
||||
|
||||
if (user.locked) {
|
||||
dispatch('startFetchingFollowRequests')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue