diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index 05eded590..7c20ea2da 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -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 diff --git a/src/stores/notifications.js b/src/stores/notifications.js index d9c388dcf..ed334306d 100644 --- a/src/stores/notifications.js +++ b/src/stores/notifications.js @@ -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 + }, }, }) diff --git a/src/stores/statuses.js b/src/stores/statuses.js index bf4a4b0cb..d508eeeca 100644 --- a/src/stores/statuses.js +++ b/src/stores/statuses.js @@ -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 }), diff --git a/src/stores/timelines.js b/src/stores/timelines.js index a9e485add..c820e1d7b 100644 --- a/src/stores/timelines.js +++ b/src/stores/timelines.js @@ -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 diff --git a/src/stores/users.js b/src/stores/users.js index 972e2ec6e..06b3f0386 100644 --- a/src/stores/users.js +++ b/src/stores/users.js @@ -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')