diff --git a/src/components/activity-notifications/SubscribeProfileDialog.tsx b/src/components/activity-notifications/SubscribeProfileDialog.tsx index 8a6b8314cd..6731cb3038 100644 --- a/src/components/activity-notifications/SubscribeProfileDialog.tsx +++ b/src/components/activity-notifications/SubscribeProfileDialog.tsx @@ -2,10 +2,10 @@ import {useMemo, useState} from 'react' import {View} from 'react-native' import { type AppBskyNotificationDefs, - type AppBskyNotificationListActivitySubscriptions, type ModerationOpts, type Un$Typed, } from '@atproto/api' +import {type DidString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -20,7 +20,7 @@ import {cleanError} from '#/lib/strings/errors' import {sanitizeHandle} from '#/lib/strings/handles' import {updateProfileShadow} from '#/state/cache/profile-shadow' import {RQKEY_getActivitySubscriptions} from '#/state/queries/activity-subscriptions' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {atoms as a, platform, useTheme, web} from '#/alf' import {Admonition} from '#/components/Admonition' import { @@ -37,6 +37,7 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_WEB} from '#/env' +import {app} from '#/lexicons' import type * as bsky from '#/types/bsky' export function SubscribeProfileDialog({ @@ -74,7 +75,7 @@ function DialogInner({ const ax = useAnalytics() const {_} = useLingui() const t = useTheme() - const agent = useAgent() + const client = useAppviewClient() const control = Dialog.useDialogContext() const queryClient = useQueryClient() const initialState = parseActivitySubscription( @@ -122,8 +123,9 @@ function DialogInner({ mutationFn: async ( activitySubscription: Un$Typed, ) => { - await agent.app.bsky.notification.putActivitySubscription({ - subject: profile.did, + await client.call(app.bsky.notification.putActivitySubscription, { + // the profile view carries an already-resolved did + subject: profile.did as DidString, activitySubscription, }) }, @@ -148,7 +150,7 @@ function DialogInner({ queryClient.setQueryData( RQKEY_getActivitySubscriptions, ( - old?: InfiniteData, + old?: InfiniteData, ) => { if (!old) return old return { diff --git a/src/state/cache/thread-mutes.tsx b/src/state/cache/thread-mutes.tsx index f0d9911348..17f6be9f34 100644 --- a/src/state/cache/thread-mutes.tsx +++ b/src/state/cache/thread-mutes.tsx @@ -5,9 +5,11 @@ import { useEffect, useState, } from 'react' +import {type AtUriString} from '@atproto/syntax' import * as persisted from '#/state/persisted' -import {useAgent, useSession} from '../session' +import {app} from '#/lexicons' +import {useAppviewClient, useSession} from '../session' type StateContext = Map type SetStateContext = (uri: string, value: boolean) => void @@ -56,7 +58,7 @@ export function useSetThreadMute() { } function useMigrateMutes(setThreadMute: SetStateContext) { - const agent = useAgent() + const client = useAppviewClient() const {currentAccount} = useSession() useEffect(() => { @@ -87,8 +89,11 @@ function useMigrateMutes(setThreadMute: SetStateContext) { setThreadMute(root, true) - await agent.api.app.bsky.graph - .muteThread({root}) + await client + .call(app.bsky.graph.muteThread, { + // the persisted list only ever holds post at-uris + root: root as AtUriString, + }) // not a big deal if this fails, since the post might have been deleted .catch(console.error) } @@ -100,5 +105,5 @@ function useMigrateMutes(setThreadMute: SetStateContext) { cancelled = true } } - }, [agent, currentAccount, setThreadMute]) + }, [client, currentAccount, setThreadMute]) } diff --git a/src/state/queries/activity-subscriptions.ts b/src/state/queries/activity-subscriptions.ts index 30964fe7d6..e6dcbd8829 100644 --- a/src/state/queries/activity-subscriptions.ts +++ b/src/state/queries/activity-subscriptions.ts @@ -1,7 +1,6 @@ import { type AppBskyActorDefs, type AppBskyNotificationDeclaration, - type AppBskyNotificationListActivitySubscriptions, } from '@atproto/api' import {t} from '@lingui/core/macro' import { @@ -13,23 +12,23 @@ import { useQueryClient, } from '@tanstack/react-query' -import {useAgent, useSession} from '#/state/session' +import {useAgent, useAppviewClient, useSession} from '#/state/session' import * as Toast from '#/components/Toast' +import {app} from '#/lexicons' export const RQKEY_getActivitySubscriptions = ['activity-subscriptions'] export const RQKEY_getNotificationDeclaration = ['notification-declaration'] export function useActivitySubscriptionsQuery() { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery({ queryKey: RQKEY_getActivitySubscriptions, queryFn: async ({pageParam}) => { - const response = - await agent.app.bsky.notification.listActivitySubscriptions({ - cursor: pageParam, - }) - return response.data + return await client.call( + app.bsky.notification.listActivitySubscriptions, + {cursor: pageParam}, + ) }, initialPageParam: undefined as string | undefined, getNextPageParam: prev => prev.cursor, @@ -111,7 +110,7 @@ export function* findAllProfilesInQueryData( did: string, ): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: RQKEY_getActivitySubscriptions, }) 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/settings.ts b/src/state/queries/notifications/settings.ts index 86e8a6c7f1..2cadb519dc 100644 --- a/src/state/queries/notifications/settings.ts +++ b/src/state/queries/notifications/settings.ts @@ -10,10 +10,10 @@ import { useQueryClient, } from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useAppviewClient, useChatClient} from '#/state/session' import * as Toast from '#/components/Toast' +import {app, chat} from '#/lexicons' const RQKEY_ROOT = 'notification-settings' const RQKEY_APP = [RQKEY_ROOT, 'app'] @@ -67,13 +67,13 @@ type ChatNotificationSettingsUpdate = export function useNotificationSettingsQuery({ enabled, }: {enabled?: boolean} = {}) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ queryKey: RQKEY_APP, queryFn: async (): Promise => { - const res = await agent.app.bsky.notification.getPreferences() - return appPreferencesWithoutChat(res.data.preferences) + const data = await client.call(app.bsky.notification.getPreferences) + return appPreferencesWithoutChat(data.preferences) }, enabled, }) @@ -82,21 +82,24 @@ export function useNotificationSettingsQuery({ export function useChatNotificationSettingsQuery({ enabled, }: {enabled?: boolean} = {}) { - const agent = useAgent() + const client = useChatClient() return useQuery({ queryKey: RQKEY_CHAT, queryFn: async (): Promise => { - const res = await agent.chat.bsky.notification.getPreferences(undefined, { - headers: DM_SERVICE_HEADERS, - }) - return chatPreferencesForSettings(res.data.preferences) + const data = await client.call(chat.bsky.notification.getPreferences) + return chatPreferencesForSettings(data.preferences) }, enabled, }) } export function useNotificationSettingsUpdateMutation() { - const agent = useAgent() + /* + * App preferences live on the appview and chat preferences on the chat + * service, so a combined update fans out over both clients. + */ + const appviewClient = useAppviewClient() + const chatClient = useChatClient() const queryClient = useQueryClient() return useMutation({ @@ -104,13 +107,13 @@ export function useNotificationSettingsUpdateMutation() { const {appUpdate, chatUpdate} = splitNotificationSettingsUpdate(update) await Promise.all([ hasUpdates(appUpdate) - ? agent.app.bsky.notification.putPreferencesV2(appUpdate) + ? appviewClient.call( + app.bsky.notification.putPreferencesV2, + appUpdate, + ) : undefined, hasUpdates(chatUpdate) - ? agent.chat.bsky.notification.putPreferences(chatUpdate, { - headers: DM_SERVICE_HEADERS, - encoding: 'application/json', - }) + ? chatClient.call(chat.bsky.notification.putPreferences, chatUpdate) : undefined, ]) }, 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() diff --git a/src/view/com/notifications/NotificationFeedItem.tsx b/src/view/com/notifications/NotificationFeedItem.tsx index c11cb8049b..2afb3a0f43 100644 --- a/src/view/com/notifications/NotificationFeedItem.tsx +++ b/src/view/com/notifications/NotificationFeedItem.tsx @@ -21,12 +21,13 @@ import { type ModerationOpts, } from '@atproto/api' import {TID} from '@atproto/common-web' +import {type DidString} from '@atproto/syntax' import {plural} from '@lingui/core/macro' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS, MAX_POST_LINES} from '#/lib/constants' +import {MAX_POST_LINES} from '#/lib/constants' import {useAnimatedValue} from '#/lib/hooks/useAnimatedValue' import {makeProfileLink} from '#/lib/routes/links' import {type NavigationProp} from '#/lib/routes/types' @@ -38,7 +39,7 @@ import {useProfileShadow} from '#/state/cache/profile-shadow' import {type FeedNotification} from '#/state/queries/notifications/feed' import {useProfileFollowMutationQueue} from '#/state/queries/profile' import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache' -import {useAgent, useSession} from '#/state/session' +import {useChatClient, useSession} from '#/state/session' import {FeedSourceCard} from '#/view/com/feeds/FeedSourceCard' import {Post} from '#/view/com/post/Post' import {formatCount} from '#/view/com/util/numeric/format' @@ -73,6 +74,7 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_WEB} from '#/env' +import {chat} from '#/lexicons' import * as bsky from '#/types/bsky' const MAX_AUTHORS = 5 @@ -911,21 +913,21 @@ function FollowBackButton({profile}: {profile: AppBskyActorDefs.ProfileView}) { function SayHelloBtn({profile}: {profile: AppBskyActorDefs.ProfileView}) { const {t: l} = useLingui() - const agent = useAgent() + const client = useChatClient() + const {currentAccount} = useSession() const navigation = useNavigation() const [isLoading, setIsLoading] = useState(false) const onPress = async () => { try { setIsLoading(true) - const res = await agent.api.chat.bsky.convo.getConvoForMembers( - { - members: [profile.did, agent.session!.did], - }, - {headers: DM_SERVICE_HEADERS}, - ) + const data = await client.call(chat.bsky.convo.getConvoForMembers, { + // both dids are already resolved - one from the profile view, one from + // the active session + members: [profile.did, currentAccount!.did] as DidString[], + }) navigation.navigate('MessagesConversation', { - conversation: res.data.convo.id, + conversation: data.convo.id, }) } catch (e) { logger.error('Failed to get conversation', {safeMessage: e})