diff options
Diffstat (limited to 'src/state/queries/notifications/feed.ts')
-rw-r--r-- | src/state/queries/notifications/feed.ts | 217 |
1 files changed, 37 insertions, 180 deletions
diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts index 54bd87540..5c519d045 100644 --- a/src/state/queries/notifications/feed.ts +++ b/src/state/queries/notifications/feed.ts @@ -1,12 +1,22 @@ -import { - AppBskyFeedDefs, - AppBskyFeedPost, - AppBskyFeedRepost, - AppBskyFeedLike, - AppBskyNotificationListNotifications, - BskyAgent, -} from '@atproto/api' -import chunk from 'lodash.chunk' +/** + * NOTE + * The ./unread.ts API: + * + * - Provides a `checkUnread()` function to sync with the server, + * - Periodically calls `checkUnread()`, and + * - Caches the first page of notifications. + * + * IMPORTANT: This query uses ./unread.ts's cache as its first page, + * IMPORTANT: which means the cache-freshness of this query is driven by the unread API. + * + * Follow these rules: + * + * 1. Call `checkUnread()` if you want to fetch latest in the background. + * 2. Call `checkUnread({invalidate: true})` if you want latest to sync into this query's results immediately. + * 3. Don't call this query's `refetch()` if you're trying to sync latest; call `checkUnread()` instead. + */ + +import {AppBskyFeedDefs} from '@atproto/api' import { useInfiniteQuery, InfiniteData, @@ -14,50 +24,27 @@ import { useQueryClient, QueryClient, } from '@tanstack/react-query' -import {getAgent} from '../../session' import {useModerationOpts} from '../preferences' -import {shouldFilterNotif} from './util' +import {useUnreadNotificationsApi} from './unread' +import {fetchPage} from './util' +import {FeedPage} from './types' import {useMutedThreads} from '#/state/muted-threads' -import {precacheProfile as precacheResolvedUri} from '../resolve-uri' -const GROUPABLE_REASONS = ['like', 'repost', 'follow'] +export type {NotificationType, FeedNotification, FeedPage} from './types' + const PAGE_SIZE = 30 -const MS_1HR = 1e3 * 60 * 60 -const MS_2DAY = MS_1HR * 48 type RQPageParam = string | undefined -type NotificationType = - | 'post-like' - | 'feedgen-like' - | 'repost' - | 'mention' - | 'reply' - | 'quote' - | 'follow' - | 'unknown' export function RQKEY() { return ['notification-feed'] } -export interface FeedNotification { - _reactKey: string - type: NotificationType - notification: AppBskyNotificationListNotifications.Notification - additional?: AppBskyNotificationListNotifications.Notification[] - subjectUri?: string - subject?: AppBskyFeedDefs.PostView -} - -export interface FeedPage { - cursor: string | undefined - items: FeedNotification[] -} - export function useNotificationFeedQuery(opts?: {enabled?: boolean}) { const queryClient = useQueryClient() const moderationOpts = useModerationOpts() const threadMutes = useMutedThreads() + const unreads = useUnreadNotificationsApi() const enabled = opts?.enabled !== false return useInfiniteQuery< @@ -69,40 +56,21 @@ export function useNotificationFeedQuery(opts?: {enabled?: boolean}) { >({ queryKey: RQKEY(), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await getAgent().listNotifications({ + // for the first page, we check the cached page held by the unread-checker first + if (!pageParam) { + const cachedPage = unreads.getCachedUnreadPage() + if (cachedPage) { + return cachedPage + } + } + // do a normal fetch + return fetchPage({ limit: PAGE_SIZE, cursor: pageParam, + queryClient, + moderationOpts, + threadMutes, }) - - // filter out notifs by mod rules - const notifs = res.data.notifications.filter( - notif => !shouldFilterNotif(notif, moderationOpts), - ) - - // group notifications which are essentially similar (follows, likes on a post) - let notifsGrouped = groupNotifications(notifs) - - // we fetch subjects of notifications (usually posts) now instead of lazily - // in the UI to avoid relayouts - const subjects = await fetchSubjects(getAgent(), notifsGrouped) - for (const notif of notifsGrouped) { - if (notif.subjectUri) { - notif.subject = subjects.get(notif.subjectUri) - if (notif.subject) { - precacheResolvedUri(queryClient, notif.subject.author) // precache the handle->did resolution - } - } - } - - // apply thread muting - notifsGrouped = notifsGrouped.filter( - notif => !isThreadMuted(notif, threadMutes), - ) - - return { - cursor: res.data.cursor, - items: notifsGrouped, - } }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -135,114 +103,3 @@ export function findPostInQueryData( } return undefined } - -function groupNotifications( - notifs: AppBskyNotificationListNotifications.Notification[], -): FeedNotification[] { - const groupedNotifs: FeedNotification[] = [] - for (const notif of notifs) { - const ts = +new Date(notif.indexedAt) - let grouped = false - if (GROUPABLE_REASONS.includes(notif.reason)) { - for (const groupedNotif of groupedNotifs) { - const ts2 = +new Date(groupedNotif.notification.indexedAt) - if ( - Math.abs(ts2 - ts) < MS_2DAY && - notif.reason === groupedNotif.notification.reason && - notif.reasonSubject === groupedNotif.notification.reasonSubject && - notif.author.did !== groupedNotif.notification.author.did - ) { - groupedNotif.additional = groupedNotif.additional || [] - groupedNotif.additional.push(notif) - grouped = true - break - } - } - } - if (!grouped) { - const type = toKnownType(notif) - groupedNotifs.push({ - _reactKey: `notif-${notif.uri}`, - type, - notification: notif, - subjectUri: getSubjectUri(type, notif), - }) - } - } - return groupedNotifs -} - -async function fetchSubjects( - agent: BskyAgent, - groupedNotifs: FeedNotification[], -): Promise<Map<string, AppBskyFeedDefs.PostView>> { - const uris = new Set<string>() - for (const notif of groupedNotifs) { - if (notif.subjectUri) { - uris.add(notif.subjectUri) - } - } - const uriChunks = chunk(Array.from(uris), 25) - const postsChunks = await Promise.all( - uriChunks.map(uris => - agent.app.bsky.feed.getPosts({uris}).then(res => res.data.posts), - ), - ) - const map = new Map<string, AppBskyFeedDefs.PostView>() - for (const post of postsChunks.flat()) { - if ( - AppBskyFeedPost.isRecord(post.record) && - AppBskyFeedPost.validateRecord(post.record).success - ) { - map.set(post.uri, post) - } - } - return map -} - -function toKnownType( - notif: AppBskyNotificationListNotifications.Notification, -): NotificationType { - if (notif.reason === 'like') { - if (notif.reasonSubject?.includes('feed.generator')) { - return 'feedgen-like' - } - return 'post-like' - } - if ( - notif.reason === 'repost' || - notif.reason === 'mention' || - notif.reason === 'reply' || - notif.reason === 'quote' || - notif.reason === 'follow' - ) { - return notif.reason as NotificationType - } - return 'unknown' -} - -function getSubjectUri( - type: NotificationType, - notif: AppBskyNotificationListNotifications.Notification, -): string | undefined { - if (type === 'reply' || type === 'quote' || type === 'mention') { - return notif.uri - } else if (type === 'post-like' || type === 'repost') { - if ( - AppBskyFeedRepost.isRecord(notif.record) || - AppBskyFeedLike.isRecord(notif.record) - ) { - return typeof notif.record.subject?.uri === 'string' - ? notif.record.subject?.uri - : undefined - } - } -} - -function isThreadMuted(notif: FeedNotification, mutes: string[]): boolean { - if (!notif.subject) { - return false - } - const record = notif.subject.record as AppBskyFeedPost.Record // assured in fetchSubjects() - return mutes.includes(record.reply?.root.uri || notif.subject.uri) -} |