From 3f7b12b2fe316db36dbd2a13404d69bb06efb4ad Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Tue, 4 Aug 2026 03:29:08 +0300 Subject: [PATCH] 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 --- .../components/ChangeHandleDialog.tsx | 6 +- src/state/queries/handle.ts | 30 +++--- src/state/queries/post-feed.ts | 6 +- src/state/queries/post.ts | 96 +++++++++---------- src/view/com/testing/TestCtrls.e2e.tsx | 10 +- 5 files changed, 76 insertions(+), 72 deletions(-) diff --git a/src/screens/Settings/components/ChangeHandleDialog.tsx b/src/screens/Settings/components/ChangeHandleDialog.tsx index a8c9474410..8cff915390 100644 --- a/src/screens/Settings/components/ChangeHandleDialog.tsx +++ b/src/screens/Settings/components/ChangeHandleDialog.tsx @@ -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', diff --git a/src/state/queries/handle.ts b/src/state/queries/handle.ts index c400e8bd85..0aa0160158 100644 --- a/src/state/queries/handle.ts +++ b/src/state/queries/handle.ts @@ -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], ) } diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index c3d5ada4d2..44bc8adf02 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -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 @@ -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 || diff --git a/src/state/queries/post.ts b/src/state/queries/post.ts index fd6ba752de..0a0664d13b 100644 --- a/src/state/queries/post.ts +++ b/src/state/queries/post.ts @@ -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({ 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 { + 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], ) } diff --git a/src/view/com/testing/TestCtrls.e2e.tsx b/src/view/com/testing/TestCtrls.e2e.tsx index f3f7743b7e..e9e3436d1c 100644 --- a/src/view/com/testing/TestCtrls.e2e.tsx +++ b/src/view/com/testing/TestCtrls.e2e.tsx @@ -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) }}