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 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-07-17 15:01:08 +03:00
parent 8ee62d67bd
commit 4c0d99158f
10 changed files with 36 additions and 47 deletions
@@ -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,
_,
])
+6 -7
View File
@@ -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<RichTextAPI | null>(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}))
@@ -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,
+4 -4
View File
@@ -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<string, boolean>
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])
}
+2 -3
View File
@@ -253,13 +253,12 @@ export function useListDeleteMutation() {
export function useListMuteMutation() {
const queryClient = useQueryClient()
const appviewClient = useAppviewClient()
const pdsClient = usePdsClient()
return useMutation<void, Error, {uri: string; mute: boolean}>({
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 => {
+3 -11
View File
@@ -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 (
+4 -4
View File
@@ -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 {}
+4 -4
View File
@@ -474,10 +474,10 @@ export function useProfileMuteMutationQueue(
function useProfileMuteMutation() {
const queryClient = useQueryClient()
const pdsClient = usePdsClient()
const appviewClient = useAppviewClient()
return useMutation<void, Error, {did: string}>({
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<void, Error, {did: string}>({
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()})
+2 -2
View File
@@ -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
}
+2 -2
View File
@@ -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<RichTextAPI | null>(null)