From 4c0d99158f21125d00aac9fc1f66ca511e5c0117 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 17 Jul 2026 15:01:08 +0300 Subject: [PATCH] route appview state and facet resolution off the pds client Audit of usePdsClient call sites: the PDS client is for com.atproto.* account/repo operations. Appview-private state (actor/list/thread mutes, notification seen-state) and mention/facet resolution now use the appview-routed client. useRichText also regains logged-out mention resolution via useLexClient's public fallback (usePdsClient throws when logged out, silently leaving mentions unresolved on StarterPackLanding and web ProfileHoverCard). Co-Authored-By: Claude Fable 5 --- .../dialogs/lists/CreateOrEditListDialog.tsx | 8 ++++---- src/components/hooks/useRichText.ts | 13 ++++++------- src/screens/Messages/components/MessagesList.tsx | 11 +++++------ src/state/cache/thread-mutes.tsx | 8 ++++---- src/state/queries/list.ts | 5 ++--- src/state/queries/notifications/unread.tsx | 14 +++----------- src/state/queries/post.ts | 8 ++++---- src/state/queries/profile.ts | 8 ++++---- src/state/queries/starter-packs.ts | 4 ++-- src/view/screens/Profile.tsx | 4 ++-- 10 files changed, 36 insertions(+), 47 deletions(-) diff --git a/src/components/dialogs/lists/CreateOrEditListDialog.tsx b/src/components/dialogs/lists/CreateOrEditListDialog.tsx index 19272906c3..8f7d31331e 100644 --- a/src/components/dialogs/lists/CreateOrEditListDialog.tsx +++ b/src/components/dialogs/lists/CreateOrEditListDialog.tsx @@ -15,7 +15,7 @@ import { useListCreateMutation, useListMetadataMutation, } from '#/state/queries/list' -import {usePdsClient} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {EditableUserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme, web} from '#/alf' @@ -134,7 +134,7 @@ function DialogInner({ const {_} = useLingui() const t = useTheme() - const pdsClient = usePdsClient() + const appviewClient = useAppviewClient() const control = Dialog.useDialogContext() const { mutateAsync: createListMutation, @@ -228,7 +228,7 @@ function DialogInner({ {cleanNewlines: true}, ) - await richText.detectFacets(pdsClient) + await richText.detectFacets(appviewClient) richText = shortenLinks(richText) richText = stripInvalidMentions(richText) @@ -276,7 +276,7 @@ function DialogInner({ setImageError, activePurpose, isCurateList, - pdsClient, + appviewClient, _, ]) diff --git a/src/components/hooks/useRichText.ts b/src/components/hooks/useRichText.ts index 90cb59f3d0..3444a63308 100644 --- a/src/components/hooks/useRichText.ts +++ b/src/components/hooks/useRichText.ts @@ -1,20 +1,19 @@ import {useEffect, useState} from 'react' import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' -import {usePdsClient} from '#/state/session' +import {useLexClient} from '#/state/session' export function useRichText(text: string): [RichTextAPI, boolean] { const [prevText, setPrevText] = useState(text) const [rawRT, setRawRT] = useState(() => new RichTextAPI({text})) const [resolvedRT, setResolvedRT] = useState(null) /* - * Facet detection resolves handles via `com.atproto.identity.resolveHandle`, - * which the account (PDS) client serves. We standardize on the account client - * where a session is in scope (design section B). Logged out, this is the - * throwing client; `detectFacets` will reject, and the raw (unresolved) - * RichText is returned in the meantime. + * Facet/mention resolution is an appview job - it resolves handles via + * `com.atproto.identity.resolveHandle` through the appview. `useLexClient` + * falls back to the public client when logged out, so mentions still resolve + * on logged-out surfaces (StarterPackLandingScreen, web ProfileHoverCard). */ - const client = usePdsClient() + const client = useLexClient() if (text !== prevText) { setPrevText(text) setRawRT(new RichTextAPI({text})) diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index 0cada1a9f2..bd76b526c2 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -45,7 +45,7 @@ import {type ConvoState, ConvoStatus} from '#/state/messages/convo/types' import {useGetJoinLinkPreview} from '#/state/queries/join-links' import {useGetPost} from '#/state/queries/post' import {createEmbedViewRecordFromPost} from '#/state/queries/postgate/util' -import {usePdsClient, useSession} from '#/state/session' +import {useAppviewClient, useSession} from '#/state/session' import {List, type ListMethods} from '#/view/com/util/List' import {MessageComposer} from '#/screens/Messages/components/MessageComposer' import {MessageListError} from '#/screens/Messages/components/MessageListError' @@ -141,10 +141,9 @@ export function MessagesList({ const convoState = useConvoActive() /* * Facet detection resolves handles via `com.atproto.identity.resolveHandle`, - * which the account (PDS) client serves - chat requires a session, so the - * client is always live here. + * which the appview client serves (design section B). */ - const pdsClient = usePdsClient() + const appviewClient = useAppviewClient() const {hasSession, currentAccount} = useSession() const getPost = useGetPost() const getJoinLinkPreview = useGetJoinLinkPreview() @@ -618,7 +617,7 @@ export function MessagesList({ replyTo = {messageId: reply.id} } - await rt.detectFacets(pdsClient) + await rt.detectFacets(appviewClient) rt = shortenLinks(rt) rt = stripInvalidMentions(rt) @@ -671,7 +670,7 @@ export function MessagesList({ } }, [ - pdsClient, + appviewClient, convoState, getPost, getJoinLinkPreview, diff --git a/src/state/cache/thread-mutes.tsx b/src/state/cache/thread-mutes.tsx index ef0a579ccd..8f1247607a 100644 --- a/src/state/cache/thread-mutes.tsx +++ b/src/state/cache/thread-mutes.tsx @@ -9,7 +9,7 @@ import {type AtUriString} from '@atproto/syntax' import * as persisted from '#/state/persisted' import {app} from '#/lexicons' -import {usePdsClient, useSession} from '../session' +import {useAppviewClient, useSession} from '../session' type StateContext = Map type SetStateContext = (uri: string, value: boolean) => void @@ -58,7 +58,7 @@ export function useSetThreadMute() { } function useMigrateMutes(setThreadMute: SetStateContext) { - const pdsClient = usePdsClient() + const appviewClient = useAppviewClient() const {currentAccount} = useSession() useEffect(() => { @@ -89,7 +89,7 @@ function useMigrateMutes(setThreadMute: SetStateContext) { setThreadMute(root, true) - await pdsClient + await appviewClient .call(app.bsky.graph.muteThread, {root: root as AtUriString}) // not a big deal if this fails, since the post might have been deleted .catch(console.error) @@ -102,5 +102,5 @@ function useMigrateMutes(setThreadMute: SetStateContext) { cancelled = true } } - }, [pdsClient, currentAccount, setThreadMute]) + }, [appviewClient, currentAccount, setThreadMute]) } diff --git a/src/state/queries/list.ts b/src/state/queries/list.ts index 98001770c1..16f798a10c 100644 --- a/src/state/queries/list.ts +++ b/src/state/queries/list.ts @@ -253,13 +253,12 @@ export function useListDeleteMutation() { export function useListMuteMutation() { const queryClient = useQueryClient() const appviewClient = useAppviewClient() - const pdsClient = usePdsClient() return useMutation({ mutationFn: async ({uri, mute}) => { if (mute) { - await pdsClient.call(muteActorList, {list: uri as AtUriString}) + await appviewClient.call(muteActorList, {list: uri as AtUriString}) } else { - await pdsClient.call(unmuteActorList, {list: uri as AtUriString}) + await appviewClient.call(unmuteActorList, {list: uri as AtUriString}) } await whenAppViewReady(appviewClient, uri, v => { diff --git a/src/state/queries/notifications/unread.tsx b/src/state/queries/notifications/unread.tsx index a21b7e4053..05d62a0bea 100644 --- a/src/state/queries/notifications/unread.tsx +++ b/src/state/queries/notifications/unread.tsx @@ -20,7 +20,7 @@ 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 {useAppviewClient, usePdsClient, useSession} from '#/state/session' +import {useAppviewClient, useSession} from '#/state/session' import {RQKEY as RQKEY_NOTIFS} from './feed' import {type CachedFeedPage, type FeedPage} from './types' import {fetchPage} from './util' @@ -55,7 +55,6 @@ apiContext.displayName = 'NotificationsUnreadApiContext' export function Provider({children}: React.PropsWithChildren<{}>) { const {hasSession} = useSession() const appviewClient = useAppviewClient() - const pdsClient = usePdsClient() const queryClient = useQueryClient() const moderationOpts = useModerationOpts() @@ -123,7 +122,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { return { async markAllRead() { // update server - await pdsClient.call( + await appviewClient.call( updateSeenNotifications, // toISOString() always yields a valid datetime string cacheRef.current.syncedAt.toISOString() as DatetimeString, @@ -212,14 +211,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { } }, } - }, [ - setNumUnread, - queryClient, - moderationOpts, - appviewClient, - pdsClient, - hasSession, - ]) + }, [setNumUnread, queryClient, moderationOpts, appviewClient, hasSession]) checkUnreadRef.current = api.checkUnread return ( diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts index fc23844069..4db433fde6 100644 --- a/src/state/queries/post.ts +++ b/src/state/queries/post.ts @@ -414,14 +414,14 @@ export function useThreadMuteMutationQueue( } function useThreadMuteMutation() { - const pdsClient = usePdsClient() + const appviewClient = useAppviewClient() return useMutation< {}, Error, {uri: string} // the root post's uri >({ mutationFn: async ({uri}) => { - await pdsClient.call(app.bsky.graph.muteThread, { + await appviewClient.call(app.bsky.graph.muteThread, { root: uri as AtUriString, }) return {} @@ -430,10 +430,10 @@ function useThreadMuteMutation() { } function useThreadUnmuteMutation() { - const pdsClient = usePdsClient() + const appviewClient = useAppviewClient() return useMutation<{}, Error, {uri: string}>({ mutationFn: async ({uri}) => { - await pdsClient.call(app.bsky.graph.unmuteThread, { + await appviewClient.call(app.bsky.graph.unmuteThread, { root: uri as AtUriString, }) return {} diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts index db12545a64..fec930fbc9 100644 --- a/src/state/queries/profile.ts +++ b/src/state/queries/profile.ts @@ -474,10 +474,10 @@ export function useProfileMuteMutationQueue( function useProfileMuteMutation() { const queryClient = useQueryClient() - const pdsClient = usePdsClient() + const appviewClient = useAppviewClient() return useMutation({ mutationFn: async ({did}) => { - await pdsClient.call(muteActor, {actor: did as AtIdentifierString}) + await appviewClient.call(muteActor, {actor: did as AtIdentifierString}) }, onSuccess() { void queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()}) @@ -487,10 +487,10 @@ function useProfileMuteMutation() { function useProfileUnmuteMutation() { const queryClient = useQueryClient() - const pdsClient = usePdsClient() + const appviewClient = useAppviewClient() return useMutation({ mutationFn: async ({did}) => { - await pdsClient.call(unmuteActor, {actor: did as AtIdentifierString}) + await appviewClient.call(unmuteActor, {actor: did as AtIdentifierString}) }, onSuccess() { void queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()}) diff --git a/src/state/queries/starter-packs.ts b/src/state/queries/starter-packs.ts index ffc57ab7d0..9a5778ec6a 100644 --- a/src/state/queries/starter-packs.ts +++ b/src/state/queries/starter-packs.ts @@ -110,7 +110,7 @@ export function useCreateStarterPackMutation({ let descriptionFacets: app.bsky.richtext.facet.Main[] | undefined if (description) { const rt = new RichText({text: description}) - await rt.detectFacets(pdsClient) + await rt.detectFacets(appviewClient) descriptionFacets = rt.facets } @@ -178,7 +178,7 @@ export function useEditStarterPackMutation({ let descriptionFacets: app.bsky.richtext.facet.Main[] | undefined if (description) { const rt = new RichText({text: description}) - await rt.detectFacets(pdsClient) + await rt.detectFacets(appviewClient) descriptionFacets = rt.facets } diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 58c5aa1138..315f59009c 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -29,7 +29,7 @@ import {useLabelerInfoQuery} from '#/state/queries/labeler' import {resetProfilePostsQueries} from '#/state/queries/post-feed' import {useProfileQuery} from '#/state/queries/profile' import {useResolveDidQuery} from '#/state/queries/resolve-uri' -import {usePdsClient, useSession} from '#/state/session' +import {useLexClient, useSession} from '#/state/session' import {ProfileFeedgens} from '#/view/com/feeds/ProfileFeedgens' import {ProfileLists} from '#/view/com/lists/ProfileLists' import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader' @@ -610,7 +610,7 @@ function ProfileScreenLoaded({ } function useRichText(text: string): [RichTextAPI, boolean] { - const client = usePdsClient() + const client = useLexClient() const [prevText, setPrevText] = useState(text) const [rawRT, setRawRT] = useState(() => new RichTextAPI({text})) const [resolvedRT, setResolvedRT] = useState(null)