notifications unit test
This commit is contained in:
parent
1f1c0c1300
commit
080a2b00ba
16 changed files with 676 additions and 366 deletions
143
src/stores/fetchers/notifications_fetcher.js
Normal file
143
src/stores/fetchers/notifications_fetcher.js
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import { ref } from 'vue'
|
||||
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
import { useMergedConfigStore } from 'src/stores/merged_config.js'
|
||||
import { useNotificationsStore } from 'src/stores/notifications.js'
|
||||
|
||||
import { fetchTimeline } from 'src/api/timelines.js'
|
||||
import { promiseInterval } from 'src/services/promise_interval/promise_interval.js'
|
||||
|
||||
// For using include_types when fetching notifications.
|
||||
// Note: chat_mention excluded as pleroma-fe polls them separately
|
||||
const mastoApiNotificationTypes = new Set([
|
||||
'mention',
|
||||
'status',
|
||||
'favourite',
|
||||
'reblog',
|
||||
'follow',
|
||||
'follow_request',
|
||||
'move',
|
||||
'poll',
|
||||
'pleroma:emoji_reaction',
|
||||
'pleroma:report',
|
||||
])
|
||||
|
||||
const notificationsFetcher = (credentials) => {
|
||||
const interval = ref(null)
|
||||
const loading = ref(false)
|
||||
const bottomedOut = ref(false)
|
||||
|
||||
const fetchNotifications = async ({ args, older }) => {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
const response = await fetchTimeline(args)
|
||||
const notifications = response.data
|
||||
if (older && notifications.length === 0) bottomedOut.value = true
|
||||
|
||||
useNotificationsStore().addNewNotifications(response)
|
||||
} catch (error) {
|
||||
if (
|
||||
error.statusCode === 400 &&
|
||||
error.statusText.includes('Invalid value for enum')
|
||||
) {
|
||||
error.statusText
|
||||
.matchAll(/(\w+) - Invalid value for enum./g)
|
||||
.toArray()
|
||||
.map((x) => x[1])
|
||||
.forEach((x) => mastoApiNotificationTypes.delete(x))
|
||||
|
||||
// Retry
|
||||
return await fetchNotifications({ args, older })
|
||||
}
|
||||
|
||||
console.error('Notifications Error', error)
|
||||
useInterfaceStore().pushGlobalNotice({
|
||||
level: 'error',
|
||||
messageKey: 'notifications.error',
|
||||
messageArgs: [error.message],
|
||||
timeout: 5000,
|
||||
})
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const fetchAndUpdate = async ({ older = false, sinceId } = {}) => {
|
||||
const args = { credentials }
|
||||
const timelineData = useNotificationsStore()
|
||||
const hideMutedPosts = useMergedConfigStore().mergedConfig.hideMutedPosts
|
||||
|
||||
if (useInstanceCapabilitiesStore().pleromaChatMessagesAvailable) {
|
||||
mastoApiNotificationTypes.add('pleroma:chat_mention')
|
||||
}
|
||||
|
||||
args.includeTypes = [...mastoApiNotificationTypes]
|
||||
args.withMuted = !hideMutedPosts
|
||||
|
||||
args.timeline = 'notifications'
|
||||
if (older) {
|
||||
if (timelineData.minId !== Number.POSITIVE_INFINITY) {
|
||||
args.maxId = timelineData.minId
|
||||
}
|
||||
return await fetchNotifications({ args, older })
|
||||
} else {
|
||||
// fetch new notifications
|
||||
if (
|
||||
sinceId === undefined &&
|
||||
timelineData.maxId !== Number.POSITIVE_INFINITY
|
||||
) {
|
||||
args.sinceId = timelineData.maxId
|
||||
} else if (sinceId !== null) {
|
||||
args.sinceId = sinceId
|
||||
}
|
||||
const result = await fetchNotifications({ args, older })
|
||||
|
||||
// If there's any unread notifications, try fetch notifications since
|
||||
// the newest read notification to check if any of the unread notifs
|
||||
// have changed their 'seen' state (marked as read in another session), so
|
||||
// we can update the state in this session to mark them as read as well.
|
||||
// The normal maxId-check does not tell if older notifications have changed
|
||||
const notifications = timelineData.data
|
||||
const readNotifsIds = notifications.filter((n) => n.seen).map((n) => n.id)
|
||||
const unreadNotifsIds = notifications
|
||||
.filter((n) => !n.seen)
|
||||
.map((n) => n.id)
|
||||
|
||||
if (readNotifsIds.length > 0 && unreadNotifsIds.length > 0) {
|
||||
const minId = Math.min(...unreadNotifsIds) // Oldest known unread notification
|
||||
if (minId !== Infinity) {
|
||||
args.sinceId = null // Don't use since_id since it sorta conflicts with min_id
|
||||
args.minId = minId - 1 // go beyond
|
||||
fetchNotifications({ args, older })
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
const startFetching = () => {
|
||||
if (interval.value) throw new Error('Interval already exists!')
|
||||
|
||||
fetchAndUpdate()
|
||||
|
||||
interval.value = promiseInterval(fetchAndUpdate, 10000)
|
||||
}
|
||||
|
||||
const stopFetching = () => {
|
||||
interval.value.stop()
|
||||
interval.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
loading,
|
||||
bottomedOut,
|
||||
startFetching,
|
||||
stopFetching,
|
||||
fetchOlder: () => fetchAndUpdate({ older: true }),
|
||||
}
|
||||
}
|
||||
|
||||
export default notificationsFetcher
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { ref } from 'vue'
|
||||
import { promiseInterval } from 'src/services/promise_interval/promise_interval.js'
|
||||
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
import { useInterfaceStore } from 'src/stores/interface.js'
|
||||
|
|
@ -9,6 +8,7 @@ import { ARGUMENT_MAP, useTimelinesStore } from 'src/stores/timelines.js'
|
|||
import { useUsersStore } from 'src/stores/users.js'
|
||||
|
||||
import { fetchTimeline } from 'src/api/timelines.js'
|
||||
import { promiseInterval } from 'src/services/promise_interval/promise_interval.js'
|
||||
|
||||
const REPLY_VISIBILITY_TIMELINES = new Set([
|
||||
'friends',
|
||||
|
|
@ -49,11 +49,7 @@ const timelineFetcher = (timeline, argument, credentials) => {
|
|||
|
||||
return fetchTimeline(args)
|
||||
.then(({ data: statuses, pagination, timestamp }) => {
|
||||
if (
|
||||
!older &&
|
||||
statuses.length >= 20 &&
|
||||
numStatusesBeforeFetch > 0
|
||||
) {
|
||||
if (!older && statuses.length >= 20 && numStatusesBeforeFetch > 0) {
|
||||
useTimelinesStore().queueFlush(timeline.name, timeline.maxId)
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +107,6 @@ const timelineFetcher = (timeline, argument, credentials) => {
|
|||
startFetching,
|
||||
stopFetching,
|
||||
fetchOlder: () => fetchAndUpdate({ showImmediately: true, older: true }),
|
||||
fetchNewer: () => fetchAndUpdate({ showImmediately: true, older: false }),
|
||||
loading,
|
||||
bottomedOut,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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'
|
||||
|
|
@ -19,7 +20,6 @@ import {
|
|||
maybeShowNotification,
|
||||
} 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'
|
||||
|
||||
export const defaultState = () => ({
|
||||
desktopNotificationSilence: true,
|
||||
|
|
@ -28,7 +28,7 @@ export const defaultState = () => ({
|
|||
data: [],
|
||||
statusNotificationRelations: new WeakMap(),
|
||||
idStore: new Map(),
|
||||
loading: false,
|
||||
statusIdStore: new Set(),
|
||||
socket: null,
|
||||
streaming: false,
|
||||
fetcher: null,
|
||||
|
|
@ -108,30 +108,35 @@ export const useNotificationsStore = defineStore('notifications', {
|
|||
},
|
||||
|
||||
// Updates
|
||||
updateNotificationsMinMaxId(id) {
|
||||
this.maxId = id > this.maxId ? id : this.maxId
|
||||
this.minId = id < this.minId ? id : this.minId
|
||||
updateExtremes(id) {
|
||||
if (this.maxId === '' || id > this.maxId) {
|
||||
this.maxId = id
|
||||
}
|
||||
if (this.minId === '' || id < this.minId) {
|
||||
this.minId = id
|
||||
}
|
||||
},
|
||||
updateNotification({ id, updater }) {
|
||||
const notification = this.idStore.get(id)
|
||||
notification && updater(notification)
|
||||
},
|
||||
addNewNotifications(result) {
|
||||
const { timestamp, data: notifications } = result
|
||||
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)),
|
||||
)
|
||||
|
||||
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.updateNotificationsMinMaxId(notification.id)
|
||||
this.updateExtremes(notification.id)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
|
@ -178,12 +183,14 @@ export const useNotificationsStore = defineStore('notifications', {
|
|||
|
||||
// Only add a new notification if we don't have one for the same action
|
||||
if (!this.idStore.has(notification.id)) {
|
||||
this.updateNotificationsMinMaxId(notification.id)
|
||||
this.updateExtremes(notification.id)
|
||||
|
||||
notifications.forEach((notification) => {
|
||||
if (older) {
|
||||
this.data.push(notification)
|
||||
this.idStore.set(notification.id, notification)
|
||||
})
|
||||
} else {
|
||||
this.data.unshift(notification)
|
||||
}
|
||||
this.idStore.set(notification.id, notification)
|
||||
|
||||
this.statusNotificationRelations.set(
|
||||
notification.status,
|
||||
|
|
@ -192,7 +199,7 @@ export const useNotificationsStore = defineStore('notifications', {
|
|||
|
||||
maybeShowNotification(
|
||||
useMergedConfigStore().mergedConfig.notificationVisibility,
|
||||
Object.values(useSyncConfigStore().prefsStorage.simple.muteFilters),
|
||||
Object.values(useSyncConfigStore().prefsStorage.simple.muteFilters ?? {}),
|
||||
notification,
|
||||
useI18nStore().i18n,
|
||||
)
|
||||
|
|
@ -225,26 +232,26 @@ export const useNotificationsStore = defineStore('notifications', {
|
|||
|
||||
markNotificationsAsSeen({
|
||||
id: this.maxId,
|
||||
credentials: useUsersStore().currentUser.credentials,
|
||||
credentials: useOAuthStore().token,
|
||||
}).then(() => {
|
||||
closeAllDesktopNotifications()
|
||||
})
|
||||
},
|
||||
markSingleNotificationAsSeen({ id }) {
|
||||
markSingleNotificationAsSeen(id) {
|
||||
const notification = this.idStore.get(id)
|
||||
if (notification) notification.seen = true
|
||||
|
||||
markNotificationsAsSeen({
|
||||
single: true,
|
||||
id,
|
||||
credentials: useUsersStore().currentUser.credentials,
|
||||
credentials: useOAuthStore().token,
|
||||
}).then(() => {
|
||||
closeDesktopNotification(id)
|
||||
})
|
||||
},
|
||||
dismissNotificationLocal(id) {
|
||||
this.data = this.data.filter((n) => n.id !== id)
|
||||
delete this.idStore.delete(id)
|
||||
this.idStore.delete(id)
|
||||
this.syncOrder()
|
||||
},
|
||||
dismissNotification(id) {
|
||||
this.dismissNotificationLocal(id)
|
||||
|
|
@ -254,10 +261,27 @@ export const useNotificationsStore = defineStore('notifications', {
|
|||
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)
|
||||
}
|
||||
})
|
||||
|
||||
// Misc
|
||||
setLoading(value) {
|
||||
this.loading = value
|
||||
this.syncOrder()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useOAuthStore } from 'src/stores/oauth.js'
|
|||
import { useStatusesStore } from 'src/stores/statuses.js'
|
||||
import { useUsersStore } from 'src/stores/users.js'
|
||||
|
||||
import { search2 } from 'src/api/public.js'
|
||||
import { search2, searchUsers } from 'src/api/public.js'
|
||||
|
||||
export const useSearchStore = defineStore('search', {
|
||||
actions: {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import {
|
|||
fetchRebloggedByUsers,
|
||||
fetchStatus,
|
||||
fetchStatusSource,
|
||||
search2,
|
||||
} from 'src/api/public.js'
|
||||
import {
|
||||
bookmarkStatus,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
import { first, last, max, min } from 'lodash'
|
||||
import { first, last } from 'lodash'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import timelineFetcher from 'src/stores/fetchers/timeline_fetcher.js'
|
||||
import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js'
|
||||
import { useOAuthStore } from 'src/stores/oauth.js'
|
||||
import { useStatusesStore } from 'src/stores/statuses.js'
|
||||
import { TIMELINE_STREAM_MAP, useStreamingStore } from 'src/stores/streaming.js'
|
||||
import { useUsersStore } from 'src/stores/users.js'
|
||||
|
||||
import timelineFetcher from 'src/stores/fetchers/timeline_fetcher.js'
|
||||
|
||||
const emptyTl = (name, argument = null) => {
|
||||
const result = {
|
||||
|
|
@ -164,7 +161,7 @@ export const useTimelinesStore = defineStore('timelines', {
|
|||
try {
|
||||
this.deactivate(name, true)
|
||||
} catch (e) {
|
||||
console.error(`Failed to deactivate timeline ${name}`)
|
||||
console.error(`Failed to deactivate timeline ${name}:`, e)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
|
@ -178,7 +175,7 @@ export const useTimelinesStore = defineStore('timelines', {
|
|||
showImmediately = false,
|
||||
noIdUpdate = false,
|
||||
pagination = {},
|
||||
older = false
|
||||
older = false,
|
||||
},
|
||||
) {
|
||||
if (statuses.length === 0) return
|
||||
|
|
@ -195,10 +192,7 @@ export const useTimelinesStore = defineStore('timelines', {
|
|||
}
|
||||
|
||||
if (!noIdUpdate) {
|
||||
this.updateTimelineExtremes(
|
||||
timeline,
|
||||
pagination,
|
||||
)
|
||||
this.updateTimelineExtremes(timeline, pagination)
|
||||
}
|
||||
|
||||
const filtered = statuses.filter((id) => !timeline.statusIds.has(id))
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ import {
|
|||
fetchFriends,
|
||||
fetchUser,
|
||||
fetchUserByName,
|
||||
searchUsers,
|
||||
verifyCredentials,
|
||||
} from 'src/api/public.js'
|
||||
import {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue