From c1a318905bcf5f6624b743763fe02c9565f7a50a Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 3 Aug 2026 19:28:24 +0300 Subject: [PATCH] migrate the notification feed and unread checker to the appview client Co-Authored-By: Claude Fable 5 --- src/state/queries/notifications/feed.ts | 6 ++-- src/state/queries/notifications/unread.tsx | 19 +++++----- src/state/queries/notifications/util.ts | 40 +++++++++++++--------- 3 files changed, 37 insertions(+), 28 deletions(-) diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts index cc24d3d0fc..51740e8d50 100644 --- a/src/state/queries/notifications/feed.ts +++ b/src/state/queries/notifications/feed.ts @@ -33,7 +33,7 @@ import { import {useModerationOpts} from '#/state/preferences/moderation-opts' import {STALE} from '#/state/queries' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {useThreadgateHiddenReplyUris} from '#/state/threadgate-hidden-replies' import type * as bsky from '#/types/bsky' import { @@ -60,7 +60,7 @@ export function useNotificationFeedQuery(opts: { enabled?: boolean filter: 'all' | 'mentions' }) { - const agent = useAgent() + const client = useAppviewClient() const queryClient = useQueryClient() const moderationOpts = useModerationOpts() const unreads = useUnreadNotificationsApi() @@ -106,7 +106,7 @@ export function useNotificationFeedQuery(opts: { ] } const {page: fetchedPage} = await fetchPage({ - agent, + client, limit: PAGE_SIZE, cursor: pageParam, queryClient, diff --git a/src/state/queries/notifications/unread.tsx b/src/state/queries/notifications/unread.tsx index bf7505f91b..e905b29ac6 100644 --- a/src/state/queries/notifications/unread.tsx +++ b/src/state/queries/notifications/unread.tsx @@ -11,6 +11,7 @@ import { useState, } from 'react' import {AppState} from 'react-native' +import {type ISODatetimeString} from '@atproto/syntax' import {useQueryClient} from '@tanstack/react-query' import {EventEmitter} from 'eventemitter3' @@ -18,7 +19,8 @@ import BroadcastChannel from '#/lib/broadcast' import {resetBadgeCount} from '#/lib/notifications/notifications' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {truncateAndInvalidate} from '#/state/queries/util' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, useSession} from '#/state/session' +import {app} from '#/lexicons' import {RQKEY as RQKEY_NOTIFS} from './feed' import {type CachedFeedPage, type FeedPage} from './types' import {fetchPage} from './util' @@ -52,7 +54,7 @@ apiContext.displayName = 'NotificationsUnreadApiContext' export function Provider({children}: React.PropsWithChildren<{}>) { const {hasSession} = useSession() - const agent = useAgent() + const client = useAppviewClient() const queryClient = useQueryClient() const moderationOpts = useModerationOpts() @@ -120,9 +122,10 @@ export function Provider({children}: React.PropsWithChildren<{}>) { return { async markAllRead() { // update server - await agent.updateSeenNotifications( - cacheRef.current.syncedAt.toISOString(), - ) + await client.call(app.bsky.notification.updateSeen, { + // toISOString always emits the Z-suffixed form the format requires + seenAt: cacheRef.current.syncedAt.toISOString() as ISODatetimeString, + }) // update & broadcast setNumUnread('') @@ -135,7 +138,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { isPoll, }: {invalidate?: boolean; isPoll?: boolean} = {}) { try { - if (!agent.session) return + if (!hasSession) return if (AppState.currentState !== 'active') { return } @@ -156,7 +159,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { // count const {page, indexedAt: lastIndexed} = await fetchPage({ - agent, + client, cursor: undefined, limit: 40, queryClient, @@ -207,7 +210,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { } }, } - }, [setNumUnread, queryClient, moderationOpts, agent]) + }, [setNumUnread, queryClient, moderationOpts, client, hasSession]) checkUnreadRef.current = api.checkUnread return ( diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts index e5d1d81885..4606507700 100644 --- a/src/state/queries/notifications/util.ts +++ b/src/state/queries/notifications/util.ts @@ -6,15 +6,17 @@ import { type AppBskyGraphDefs, AppBskyGraphStarterpack, type AppBskyNotificationListNotifications, - type AtpAgent, hasMutedWord, moderateNotification, type ModerationOpts, } from '@atproto/api' +import {type Client} from '@atproto/lex' +import {type AtUriString} from '@atproto/syntax' import {type QueryClient} from '@tanstack/react-query' import chunk from 'lodash.chunk' import {labelIsHideableOffense} from '#/lib/moderation' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' import {precacheProfile} from '../profile' import { @@ -38,7 +40,7 @@ const MS_2DAY = MS_1HR * 48 // = export async function fetchPage({ - agent, + client, cursor, limit, queryClient, @@ -46,7 +48,7 @@ export async function fetchPage({ fetchAdditionalData, reasons, }: { - agent: AtpAgent + client: Client cursor: string | undefined limit: number queryClient: QueryClient @@ -57,16 +59,16 @@ export async function fetchPage({ page: FeedPage indexedAt: string | undefined }> { - const res = await agent.listNotifications({ + const data = await client.call(app.bsky.notification.listNotifications, { limit, cursor, reasons, }) - const indexedAt = res.data.notifications[0]?.indexedAt + const indexedAt = data.notifications[0]?.indexedAt // filter out notifs by mod rules - const notifs = res.data.notifications.filter( + const notifs = data.notifications.filter( notif => !shouldFilterNotif(notif, moderationOpts), ) @@ -76,7 +78,7 @@ export async function fetchPage({ // we fetch subjects of notifications (usually posts) now instead of lazily // in the UI to avoid relayouts if (fetchAdditionalData) { - const subjects = await fetchSubjects(agent, notifsGrouped) + const subjects = await fetchSubjects(client, notifsGrouped) for (const notif of notifsGrouped) { if (notif.subjectUri) { if ( @@ -96,17 +98,17 @@ export async function fetchPage({ } } - let seenAt = res.data.seenAt ? new Date(res.data.seenAt) : new Date() + let seenAt = data.seenAt ? new Date(data.seenAt) : new Date() if (Number.isNaN(seenAt.getTime())) { seenAt = new Date() } return { page: { - cursor: res.data.cursor, + cursor: data.cursor, seenAt, items: notifsGrouped, - priority: res.data.priority ?? false, + priority: data.priority ?? false, }, indexedAt, } @@ -207,7 +209,7 @@ export function groupNotifications( } async function fetchSubjects( - agent: AtpAgent, + client: Client, groupedNotifs: FeedNotification[], ): Promise<{ posts: Map @@ -224,18 +226,22 @@ async function fetchSubjects( packUris.add(notif.notification.reasonSubject) } } - const postUriChunks = chunk(Array.from(postUris), 25) - const packUriChunks = chunk(Array.from(packUris), 25) + /* + * Both uri sets are collected from notification fields the server already + * validated as at-uris, so the branded cast reflects what the values are. + */ + const postUriChunks = chunk(Array.from(postUris) as AtUriString[], 25) + const packUriChunks = chunk(Array.from(packUris) as AtUriString[], 25) const postsChunks = await Promise.all( postUriChunks.map(uris => - agent.app.bsky.feed.getPosts({uris}).then(res => res.data.posts), + client.call(app.bsky.feed.getPosts, {uris}).then(data => data.posts), ), ) const packsChunks = await Promise.all( packUriChunks.map(uris => - agent.app.bsky.graph - .getStarterPacks({uris}) - .then(res => res.data.starterPacks), + client + .call(app.bsky.graph.getStarterPacks, {uris}) + .then(data => data.starterPacks), ), ) const postsMap = new Map()