migrate the remaining agent reads outside the session layer

The last non-session `useAgent` consumers, all thin aliases over calls the
clients already make:

- `handle.ts`: `getProfile` / `resolveHandle` to the appview client.
- `post.ts`: the three post readers share one `fetchPost` helper on the appview
  client. Their consumers still want `@atproto/api` views, so the generated view
  is asserted across at that single boundary rather than at each call site.
- `post-feed.ts`: `agent.session` gated the logged-out "did any post survive
  moderation" assertion, which is a question about the session, not the
  transport - it reads `hasSession` from the session context now.
- `ChangeHandleDialog`: `agent.serviceUrl` becomes `currentAccount.service`.
- `TestCtrls.e2e`: drops the `configureProxy` call. The appview client reads
  `BLUESKY_PROXY_HEADER` when the bundle builds it, and the gate around this
  input means no bundle exists yet, so setting the constant is sufficient.

`preferences/index.ts` still writes labeler subscriptions to the agent; that
one moves with the bundle rework, which is what gives it a client to write to.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-08-04 03:29:08 +03:00
parent 92408c62a6
commit 3f7b12b2fe
5 changed files with 76 additions and 72 deletions
@@ -27,7 +27,7 @@ import {useFetchDid, useUpdateHandleMutation} from '#/state/queries/handle'
import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
import {useServiceQuery} from '#/state/queries/service'
import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile'
import {useAgent, useSession, useSessionApi} from '#/state/session'
import {useSession, useSessionApi} from '#/state/session'
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
import {atoms as a, native, useBreakpoints, useTheme} from '#/alf'
import {Admonition} from '#/components/Admonition'
@@ -63,12 +63,12 @@ export function ChangeHandleDialog({
function ChangeHandleDialogInner() {
const control = Dialog.useDialogContext()
const {_} = useLingui()
const agent = useAgent()
const {currentAccount} = useSession()
const {
data: serviceInfo,
error: serviceInfoError,
refetch,
} = useServiceQuery(agent.serviceUrl.toString())
} = useServiceQuery(currentAccount?.service ?? '')
const [page, setPage] = useState<'provided-handle' | 'own-handle'>(
'provided-handle',
+17 -13
View File
@@ -1,10 +1,10 @@
import {useCallback} from 'react'
import {type HandleString} from '@atproto/syntax'
import {type DidString, type HandleString} from '@atproto/syntax'
import {useMutation, useQueryClient} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useAgent, usePdsClient} from '#/state/session'
import {com} from '#/lexicons'
import {useAppviewClient, usePdsClient} from '#/state/session'
import {app, com} from '#/lexicons'
const handleQueryKeyRoot = 'handle'
const fetchHandleQueryKey = (handleOrDid: string) => [
@@ -16,21 +16,24 @@ const fetchDidQueryKey = (handleOrDid: string) => [didQueryKeyRoot, handleOrDid]
export function useFetchHandle() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = useAppviewClient()
return useCallback(
async (handleOrDid: string) => {
if (handleOrDid.startsWith('did:')) {
const res = await queryClient.fetchQuery({
const data = await queryClient.fetchQuery({
staleTime: STALE.MINUTES.FIVE,
queryKey: fetchHandleQueryKey(handleOrDid),
queryFn: () => agent.getProfile({actor: handleOrDid}),
queryFn: () =>
client.call(app.bsky.actor.getProfile, {
actor: handleOrDid as DidString,
}),
})
return res.data.handle
return data.handle
}
return handleOrDid
},
[queryClient, agent],
[queryClient, client],
)
}
@@ -42,7 +45,6 @@ export function useUpdateHandleMutation(opts?: {
return useMutation({
mutationFn: async ({handle}: {handle: string}) => {
// `agent.updateHandle` was a pure alias for this method
await client.call(com.atproto.identity.updateHandle, {
// callers validate the handle before submitting
handle: handle as HandleString,
@@ -59,7 +61,7 @@ export function useUpdateHandleMutation(opts?: {
export function useFetchDid() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = useAppviewClient()
return useCallback(
async (handleOrDid: string) => {
@@ -69,13 +71,15 @@ export function useFetchDid() {
queryFn: async () => {
let identifier = handleOrDid
if (!identifier.startsWith('did:')) {
const res = await agent.resolveHandle({handle: identifier})
identifier = res.data.did
const data = await client.call(com.atproto.identity.resolveHandle, {
handle: identifier as HandleString,
})
identifier = data.did
}
return identifier
},
})
},
[queryClient, agent],
[queryClient, client],
)
}
+3 -3
View File
@@ -36,7 +36,7 @@ import {moderatePost} from '#/lib/moderation/subjects'
import {logger} from '#/logger'
import {STALE} from '#/state/queries'
import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const'
import {useAgent, useAppviewClient} from '#/state/session'
import {useAppviewClient, useSession} from '#/state/session'
import * as userActionHistory from '#/state/userActionHistory'
import {KnownError} from '#/view/com/posts/PostFeedErrorMessage'
import {useFeedTuners} from '../preferences/feed-tuners'
@@ -151,7 +151,7 @@ export function usePostFeedQuery(
f => f.pinned && f.value === 'following',
) ?? -1
const enableFollowingToDiscoverFallback = followingPinnedIndex === 0
const agent = useAgent()
const {hasSession} = useSession()
const client = useAppviewClient()
const lastRun = useRef<{
data: InfiniteData<FeedPageUnselected>
@@ -214,7 +214,7 @@ export function usePostFeedQuery(
* moderations happen later, which results in some posts being shown and
* some not.
*/
if (!agent.session) {
if (!hasSession) {
assertSomePostsPassModeration(
res.feed,
preferences?.moderationPrefs ||
+46 -50
View File
@@ -1,6 +1,7 @@
import {useCallback} from 'react'
import {type AppBskyActorDefs, type AppBskyFeedDefs, AtUri} from '@atproto/api'
import {type AtUriString} from '@atproto/syntax'
import {type Client} from '@atproto/lex'
import {type AtUriString, type HandleString} from '@atproto/syntax'
import {deleteLike, deletePost, deleteRepost, like, repost} from '@bsky.app/sdk'
import {
type QueryClient,
@@ -12,16 +13,11 @@ import {
import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue'
import {updatePostShadow} from '#/state/cache/post-shadow'
import {type Shadow} from '#/state/cache/types'
import {
useAgent,
useAppviewClient,
usePdsClient,
useSession,
} from '#/state/session'
import {useAppviewClient, usePdsClient, useSession} from '#/state/session'
import * as userActionHistory from '#/state/userActionHistory'
import {useAnalytics} from '#/analytics'
import {type Metrics, toClout} from '#/analytics/metrics'
import {app} from '#/lexicons'
import {app, com} from '#/lexicons'
import {useIsThreadMuted, useSetThreadMute} from '../cache/thread-mutes'
import {findProfileQueryData} from './profile'
@@ -29,25 +25,15 @@ const RQKEY_ROOT = 'post'
export const RQKEY = (postUri: string) => [RQKEY_ROOT, postUri]
export function usePostQuery(uri: string | undefined) {
const agent = useAgent()
const client = useAppviewClient()
return useQuery<AppBskyFeedDefs.PostView>({
queryKey: RQKEY(uri || ''),
queryFn: async () => {
if (!uri) throw new Error('[unreachable] No URI provided')
const urip = new AtUri(uri)
if (!urip.host.startsWith('did:')) {
const res = await agent.resolveHandle({
handle: urip.host,
})
// @ts-expect-error TODO new-sdk-migration
urip.host = res.data.did
}
const res = await agent.getPosts({uris: [urip.toString()]})
if (res.success && res.data.posts[0]) {
return res.data.posts[0]
const post = await fetchPost(client, uri)
if (post) {
return post
}
throw new Error('No data')
@@ -56,6 +42,33 @@ export function usePostQuery(uri: string | undefined) {
})
}
/**
* Read one post by AT-URI, resolving a handle authority first when the URI
* carries one.
*
* The appview still answers with `@atproto/api`-shaped views for the callers of
* these hooks, so the generated view is asserted across at this single
* boundary rather than at every consumer.
*/
async function fetchPost(
client: Client,
uri: string,
): Promise<AppBskyFeedDefs.PostView | undefined> {
const urip = new AtUri(uri)
if (!urip.host.startsWith('did:')) {
const data = await client.call(com.atproto.identity.resolveHandle, {
handle: urip.host as HandleString,
})
urip.host = data.did
}
const data = await client.call(app.bsky.feed.getPosts, {
uris: [urip.toString()],
})
return data.posts[0]
}
export function precachePost(
queryClient: QueryClient,
uri: string,
@@ -66,59 +79,42 @@ export function precachePost(
export function useGetPost() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = useAppviewClient()
return useCallback(
async ({uri}: {uri: string}) => {
return queryClient.fetchQuery({
queryKey: RQKEY(uri || ''),
async queryFn() {
const urip = new AtUri(uri)
if (!urip.host.startsWith('did:')) {
const res = await agent.resolveHandle({
handle: urip.host,
})
// @ts-expect-error TODO new-sdk-migration
urip.host = res.data.did
}
const res = await agent.getPosts({
uris: [urip.toString()],
})
if (res.success && res.data.posts[0]) {
return res.data.posts[0]
const post = await fetchPost(client, uri)
if (post) {
return post
}
throw new Error('useGetPost: post not found')
},
})
},
[queryClient, agent],
[queryClient, client],
)
}
export function useGetPosts() {
const queryClient = useQueryClient()
const agent = useAgent()
const client = useAppviewClient()
return useCallback(
async ({uris}: {uris: string[]}) => {
return queryClient.fetchQuery({
queryKey: RQKEY(uris.join(',') || ''),
async queryFn() {
const res = await agent.getPosts({
uris,
const data = await client.call(app.bsky.feed.getPosts, {
uris: uris as AtUriString[],
})
if (res.success) {
return res.data.posts
} else {
throw new Error('useGetPosts failed')
}
// See the note on `fetchPost` about the view shapes.
return data.posts as AppBskyFeedDefs.PostView[]
},
})
},
[queryClient, agent],
[queryClient, client],
)
}
+7 -3
View File
@@ -3,7 +3,7 @@ import {LogBox, Pressable, TextInput, View} from 'react-native'
import {useQueryClient} from '@tanstack/react-query'
import {BLUESKY_PROXY_HEADER} from '#/lib/constants'
import {useAgent, useSessionApi} from '#/state/session'
import {useSessionApi} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useOnboardingDispatch} from '#/state/shell/onboarding'
import {navigate} from '../../../Navigation'
@@ -31,7 +31,6 @@ const BTN = {height: 1, width: 1, backgroundColor: 'red'}
let hasConfiguredProxy = false
export function TestCtrls() {
const agent = useAgent()
const queryClient = useQueryClient()
const {logoutEveryAccount, login} = useSessionApi()
const onboardingDispatch = useOnboardingDispatch()
@@ -74,8 +73,13 @@ export function TestCtrls() {
autoCapitalize="none"
onSubmitEditing={() => {
const header = `${proxyHeader}#bsky_appview`
/*
* The appview client reads `BLUESKY_PROXY_HEADER.get()` when the
* bundle builds it (see clients.ts), so setting the mutable constant
* retargets the proxy for the sign-ins below without reconfiguring
* anything: the gate above means no bundle exists yet.
*/
BLUESKY_PROXY_HEADER.set(header)
agent.configureProxy(header as any)
hasConfiguredProxy = true
setIsProxyConfigured(true)
}}