diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/state/queries/notifications/feed.ts | 217 | ||||
-rw-r--r-- | src/state/queries/notifications/types.ts | 34 | ||||
-rw-r--r-- | src/state/queries/notifications/unread.tsx | 129 | ||||
-rw-r--r-- | src/state/queries/notifications/util.ts | 182 | ||||
-rw-r--r-- | src/view/com/notifications/Feed.tsx | 31 | ||||
-rw-r--r-- | src/view/screens/Notifications.tsx | 22 |
6 files changed, 369 insertions, 246 deletions
diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts index 68396143c..5c519d045 100644 --- a/src/state/queries/notifications/feed.ts +++ b/src/state/queries/notifications/feed.ts @@ -1,11 +1,22 @@ -import { - AppBskyFeedDefs, - AppBskyFeedPost, - AppBskyFeedRepost, - AppBskyFeedLike, - AppBskyNotificationListNotifications, -} 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, @@ -13,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< @@ -68,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(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, @@ -134,115 +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( - 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 => - getAgent() - .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) -} diff --git a/src/state/queries/notifications/types.ts b/src/state/queries/notifications/types.ts new file mode 100644 index 000000000..0e88f1071 --- /dev/null +++ b/src/state/queries/notifications/types.ts @@ -0,0 +1,34 @@ +import { + AppBskyNotificationListNotifications, + AppBskyFeedDefs, +} from '@atproto/api' + +export type NotificationType = + | 'post-like' + | 'feedgen-like' + | 'repost' + | 'mention' + | 'reply' + | 'quote' + | 'follow' + | 'unknown' + +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 interface CachedFeedPage { + sessDid: string // used to invalidate on session changes + syncedAt: Date + data: FeedPage | undefined +} diff --git a/src/state/queries/notifications/unread.tsx b/src/state/queries/notifications/unread.tsx index b93e1dc81..d41cfee2e 100644 --- a/src/state/queries/notifications/unread.tsx +++ b/src/state/queries/notifications/unread.tsx @@ -1,10 +1,19 @@ +/** + * A kind of companion API to ./feed.ts. See that file for more info. + */ + import React from 'react' import * as Notifications from 'expo-notifications' +import {useQueryClient} from '@tanstack/react-query' import BroadcastChannel from '#/lib/broadcast' import {useSession, getAgent} from '#/state/session' import {useModerationOpts} from '../preferences' -import {shouldFilterNotif} from './util' +import {fetchPage} from './util' +import {CachedFeedPage, FeedPage} from './types' import {isNative} from '#/platform/detection' +import {useMutedThreads} from '#/state/muted-threads' +import {RQKEY as RQKEY_NOTIFS} from './feed' +import {logger} from '#/logger' const UPDATE_INTERVAL = 30 * 1e3 // 30sec @@ -14,7 +23,8 @@ type StateContext = string interface ApiContext { markAllRead: () => Promise<void> - checkUnread: () => Promise<void> + checkUnread: (opts?: {invalidate?: boolean}) => Promise<void> + getCachedUnreadPage: () => FeedPage | undefined } const stateContext = React.createContext<StateContext>('') @@ -22,16 +32,23 @@ const stateContext = React.createContext<StateContext>('') const apiContext = React.createContext<ApiContext>({ async markAllRead() {}, async checkUnread() {}, + getCachedUnreadPage: () => undefined, }) export function Provider({children}: React.PropsWithChildren<{}>) { - const {hasSession} = useSession() + const {hasSession, currentAccount} = useSession() + const queryClient = useQueryClient() const moderationOpts = useModerationOpts() + const threadMutes = useMutedThreads() const [numUnread, setNumUnread] = React.useState('') - const checkUnreadRef = React.useRef<(() => Promise<void>) | null>(null) - const lastSyncRef = React.useRef<Date>(new Date()) + const checkUnreadRef = React.useRef<ApiContext['checkUnread'] | null>(null) + const cacheRef = React.useRef<CachedFeedPage>({ + sessDid: currentAccount?.did || '', + syncedAt: new Date(), + data: undefined, + }) // periodic sync React.useEffect(() => { @@ -46,14 +63,18 @@ export function Provider({children}: React.PropsWithChildren<{}>) { // listen for broadcasts React.useEffect(() => { const listener = ({data}: MessageEvent) => { - lastSyncRef.current = new Date() + cacheRef.current = { + sessDid: currentAccount?.did || '', + syncedAt: new Date(), + data: undefined, + } setNumUnread(data.event) } broadcast.addEventListener('message', listener) return () => { broadcast.removeEventListener('message', listener) } - }, [setNumUnread]) + }, [setNumUnread, currentAccount]) // create API const api = React.useMemo<ApiContext>(() => { @@ -61,7 +82,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { async markAllRead() { // update server await getAgent().updateSeenNotifications( - lastSyncRef.current.toISOString(), + cacheRef.current.syncedAt.toISOString(), ) // update & broadcast @@ -69,36 +90,59 @@ export function Provider({children}: React.PropsWithChildren<{}>) { broadcast.postMessage({event: ''}) }, - async checkUnread() { - if (!getAgent().session) return - - // count - const res = await getAgent().listNotifications({limit: 40}) - const filtered = res.data.notifications.filter( - notif => !notif.isRead && !shouldFilterNotif(notif, moderationOpts), - ) - const num = - filtered.length >= 30 - ? '30+' - : filtered.length === 0 - ? '' - : String(filtered.length) - if (isNative) { - Notifications.setBadgeCountAsync(Math.min(filtered.length, 30)) + async checkUnread({invalidate}: {invalidate?: boolean} = {}) { + try { + if (!getAgent().session) return + + // count + const page = await fetchPage({ + cursor: undefined, + limit: 40, + queryClient, + moderationOpts, + threadMutes, + }) + const unreadCount = countUnread(page) + const unreadCountStr = + unreadCount >= 30 + ? '30+' + : unreadCount === 0 + ? '' + : String(unreadCount) + if (isNative) { + Notifications.setBadgeCountAsync(Math.min(unreadCount, 30)) + } + + // track last sync + const now = new Date() + const lastIndexed = + page.items[0] && new Date(page.items[0].notification.indexedAt) + cacheRef.current = { + sessDid: currentAccount?.did || '', + data: page, + syncedAt: !lastIndexed || now > lastIndexed ? now : lastIndexed, + } + + // update & broadcast + setNumUnread(unreadCountStr) + if (invalidate) { + queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS()}) + } + broadcast.postMessage({event: unreadCountStr}) + } catch (e) { + logger.error('Failed to check unread notifications', {error: e}) } + }, - // track last sync - const now = new Date() - const lastIndexed = filtered[0] && new Date(filtered[0].indexedAt) - lastSyncRef.current = - !lastIndexed || now > lastIndexed ? now : lastIndexed - - // update & broadcast - setNumUnread(num) - broadcast.postMessage({event: num}) + getCachedUnreadPage() { + // return cached page if was for the current user + // (protects against session changes serving data from the past session) + if (cacheRef.current.sessDid === currentAccount?.did) { + return cacheRef.current.data + } }, } - }, [setNumUnread, moderationOpts]) + }, [setNumUnread, queryClient, moderationOpts, threadMutes, currentAccount]) checkUnreadRef.current = api.checkUnread return ( @@ -115,3 +159,20 @@ export function useUnreadNotifications() { export function useUnreadNotificationsApi() { return React.useContext(apiContext) } + +function countUnread(page: FeedPage) { + let num = 0 + for (const item of page.items) { + if (!item.notification.isRead) { + num++ + } + if (item.additional) { + for (const item2 of item.additional) { + if (!item2.isRead) { + num++ + } + } + } + } + return num +} diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts index c49d1851a..48e1b8dd8 100644 --- a/src/state/queries/notifications/util.ts +++ b/src/state/queries/notifications/util.ts @@ -3,10 +3,78 @@ import { ModerationOpts, moderateProfile, moderatePost, + AppBskyFeedDefs, + AppBskyFeedPost, + AppBskyFeedRepost, + AppBskyFeedLike, } from '@atproto/api' +import chunk from 'lodash.chunk' +import {QueryClient} from '@tanstack/react-query' +import {getAgent} from '../../session' +import {precacheProfile as precacheResolvedUri} from '../resolve-uri' +import {NotificationType, FeedNotification, FeedPage} from './types' + +const GROUPABLE_REASONS = ['like', 'repost', 'follow'] +const MS_1HR = 1e3 * 60 * 60 +const MS_2DAY = MS_1HR * 48 + +// exported api +// = + +export async function fetchPage({ + cursor, + limit, + queryClient, + moderationOpts, + threadMutes, +}: { + cursor: string | undefined + limit: number + queryClient: QueryClient + moderationOpts: ModerationOpts | undefined + threadMutes: string[] +}): Promise<FeedPage> { + const res = await getAgent().listNotifications({ + limit, + cursor, + }) + + // 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(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, + } +} + +// internal methods +// = // TODO this should be in the sdk as moderateNotification -prf -export function shouldFilterNotif( +function shouldFilterNotif( notif: AppBskyNotificationListNotifications.Notification, moderationOpts: ModerationOpts | undefined, ): boolean { @@ -36,3 +104,115 @@ export function shouldFilterNotif( // (this requires fetching the post) return false } + +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( + 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 => + getAgent() + .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) +} diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx index ba88f78c0..c496d5f7c 100644 --- a/src/view/com/notifications/Feed.tsx +++ b/src/view/com/notifications/Feed.tsx @@ -35,15 +35,13 @@ export function Feed({ const [isPTRing, setIsPTRing] = React.useState(false) const moderationOpts = useModerationOpts() - const {markAllRead} = useUnreadNotificationsApi() + const {markAllRead, checkUnread} = useUnreadNotificationsApi() const { data, - isLoading, isFetching, isFetched, isError, error, - refetch, hasNextPage, isFetchingNextPage, fetchNextPage, @@ -52,13 +50,11 @@ export function Feed({ const firstItem = data?.pages[0]?.items[0] // mark all read on fresh data + // (this will fire each time firstItem changes) React.useEffect(() => { - let cleanup if (firstItem) { - const to = setTimeout(() => markAllRead(), 250) - cleanup = () => clearTimeout(to) + markAllRead() } - return cleanup }, [firstItem, markAllRead]) const items = React.useMemo(() => { @@ -83,7 +79,7 @@ export function Feed({ const onRefresh = React.useCallback(async () => { try { setIsPTRing(true) - await refetch() + await checkUnread({invalidate: true}) } catch (err) { logger.error('Failed to refresh notifications feed', { error: err, @@ -91,7 +87,7 @@ export function Feed({ } finally { setIsPTRing(false) } - }, [refetch, setIsPTRing]) + }, [checkUnread, setIsPTRing]) const onEndReached = React.useCallback(async () => { if (isFetching || !hasNextPage || isError) return @@ -136,21 +132,6 @@ export function Feed({ [onPressRetryLoadMore, moderationOpts], ) - const showHeaderSpinner = !isPTRing && isFetching && !isLoading - const FeedHeader = React.useCallback( - () => ( - <View> - {ListHeaderComponent ? <ListHeaderComponent /> : null} - {showHeaderSpinner ? ( - <View style={{padding: 10}}> - <ActivityIndicator /> - </View> - ) : null} - </View> - ), - [ListHeaderComponent, showHeaderSpinner], - ) - const FeedFooter = React.useCallback( () => isFetchingNextPage ? ( @@ -180,7 +161,7 @@ export function Feed({ data={items} keyExtractor={item => item._reactKey} renderItem={renderItem} - ListHeaderComponent={FeedHeader} + ListHeaderComponent={ListHeaderComponent} ListFooterComponent={FeedFooter} refreshControl={ <RefreshControl diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index 8516d1667..a5226bb67 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -19,7 +19,10 @@ import {logger} from '#/logger' import {useSetMinimalShellMode} from '#/state/shell' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useUnreadNotifications} from '#/state/queries/notifications/unread' +import { + useUnreadNotifications, + useUnreadNotificationsApi, +} from '#/state/queries/notifications/unread' import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed' import {listenSoftReset, emitSoftReset} from '#/state/events' @@ -35,8 +38,9 @@ export function NotificationsScreen({}: Props) { const {screen} = useAnalytics() const pal = usePalette('default') const {isDesktop} = useWebMediaQueries() - const unreadNotifs = useUnreadNotifications() const queryClient = useQueryClient() + const unreadNotifs = useUnreadNotifications() + const unreadApi = useUnreadNotificationsApi() const hasNew = !!unreadNotifs // event handlers @@ -48,10 +52,16 @@ export function NotificationsScreen({}: Props) { const onPressLoadLatest = React.useCallback(() => { scrollToTop() - queryClient.invalidateQueries({ - queryKey: NOTIFS_RQKEY(), - }) - }, [scrollToTop, queryClient]) + if (hasNew) { + // render what we have now + queryClient.invalidateQueries({ + queryKey: NOTIFS_RQKEY(), + }) + } else { + // check with the server + unreadApi.checkUnread({invalidate: true}) + } + }, [scrollToTop, queryClient, unreadApi, hasNew]) // on-visible setup // = |