From 74f4532922ce5f62c4a64ae63de61d4d454d1593 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 3 Aug 2026 16:24:21 +0300 Subject: [PATCH] migrate the join link and join request queries to the chat client The logged-out join link preview path built an ad-hoc AtpAgent pointed at CHAT_SERVICE. It is now a module-level unauthenticated lex client against the same service, so the client is built once rather than per call. Co-Authored-By: Claude Fable 5 --- src/state/queries/join-links.ts | 53 ++++++++++++------- .../queries/messages/create-join-link.ts | 16 +++--- .../queries/messages/disable-join-link.ts | 12 ++--- src/state/queries/messages/edit-join-link.ts | 16 +++--- .../queries/messages/enable-join-link.ts | 12 ++--- src/state/queries/messages/join-requests.ts | 32 ++++++----- .../queries/messages/list-join-requests.ts | 17 +++--- .../messages/mark-join-request-read.ts | 11 ++-- .../messages/request-join-group-chat.ts | 12 ++--- .../messages/withdraw-join-group-chat.ts | 12 ++--- 10 files changed, 98 insertions(+), 95 deletions(-) diff --git a/src/state/queries/join-links.ts b/src/state/queries/join-links.ts index ca1db05558..e53edc0291 100644 --- a/src/state/queries/join-links.ts +++ b/src/state/queries/join-links.ts @@ -1,17 +1,19 @@ import {useCallback} from 'react' import { type $Typed, - AtpAgent, ChatBskyGroupDefs, type ChatBskyGroupGetJoinLinkPreviews, } from '@atproto/api' +import {type Client} from '@atproto/lex' import {type QueryClient, useQuery, useQueryClient} from '@tanstack/react-query' -import {CHAT_SERVICE, DM_SERVICE_HEADERS} from '#/lib/constants' +import {CHAT_SERVICE} from '#/lib/constants' +import {createLexClient} from '#/lib/lexClient' import {logger} from '#/logger' import {STALE} from '#/state/queries/index' import {createQueryKey, type StructuredQueryKey} from '#/state/queries/util' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' /** * The three preview shapes we currently support. Excludes the `{$type: string}` @@ -154,23 +156,34 @@ export function invalidateJoinLinkPreviewsForConvo( }) } +let publicChatClient: Client | undefined + +/** + * The unauthenticated {@link Client} for the chat service, used to resolve join + * links before the viewer has a session. + * + * It talks to {@link CHAT_SERVICE} directly rather than proxying through a PDS, + * so no `atproto-proxy` header is involved. A single module-level instance, + * because there is no session to scope it to and reconstructing it per call + * would be waste. + */ +function getPublicChatClient(): Client { + return (publicChatClient ??= createLexClient({service: CHAT_SERVICE})) +} + async function fetchJoinLinkPreviews({ - agent, + client, codes, hasSession, }: { - agent: AtpAgent + client: Client codes: string[] hasSession: boolean }) { - const previewAgent = new AtpAgent({service: CHAT_SERVICE}) - const res = hasSession - ? await agent.chat.bsky.group.getJoinLinkPreviews( - {codes}, - {headers: DM_SERVICE_HEADERS}, - ) - : await previewAgent.chat.bsky.group.getJoinLinkPreviews({codes}) - return res.data + return await (hasSession ? client : getPublicChatClient()).call( + chat.bsky.group.getJoinLinkPreviews, + {codes}, + ) } export function useJoinLinkPreviewsQuery({ @@ -188,14 +201,14 @@ export function useJoinLinkPreviewsQuery({ */ initialData?: ChatBskyGroupGetJoinLinkPreviews.OutputSchema }) { - const agent = useAgent() + const client = useChatClient() return useQuery({ queryKey: createJoinLinkPreviewQueryKey({codes: codes ?? [], hasSession}), queryFn: async () => { if (!codes) throw new Error('No invite code') try { - return await fetchJoinLinkPreviews({agent, codes, hasSession}) + return await fetchJoinLinkPreviews({client, codes, hasSession}) } catch (error) { logger.error('Failed to fetch join link preview', {safeMessage: error}) throw error @@ -208,13 +221,13 @@ export function useJoinLinkPreviewsQuery({ } export function usePrefetchJoinLinkPreviews() { - const agent = useAgent() + const client = useChatClient() const queryClient = useQueryClient() return ({codes, hasSession}: {codes: string[]; hasSession: boolean}) => { return queryClient.prefetchQuery({ queryKey: createJoinLinkPreviewQueryKey({codes, hasSession}), - queryFn: () => fetchJoinLinkPreviews({agent, codes, hasSession}), + queryFn: () => fetchJoinLinkPreviews({client, codes, hasSession}), staleTime: STALE.SECONDS.FIFTEEN, }) } @@ -226,7 +239,7 @@ export function usePrefetchJoinLinkPreviews() { * Returns undefined if the preview can't be resolved. */ export function useGetJoinLinkPreview() { - const agent = useAgent() + const client = useChatClient() const queryClient = useQueryClient() return useCallback( @@ -241,7 +254,7 @@ export function useGetJoinLinkPreview() { const data = await queryClient.fetchQuery({ queryKey: createJoinLinkPreviewQueryKey({codes: [code], hasSession}), queryFn: () => - fetchJoinLinkPreviews({agent, codes: [code], hasSession}), + fetchJoinLinkPreviews({client, codes: [code], hasSession}), staleTime: STALE.SECONDS.FIFTEEN, }) const found = data.joinLinkPreviews[0] @@ -251,6 +264,6 @@ export function useGetJoinLinkPreview() { return undefined } }, - [agent, queryClient], + [client, queryClient], ) } diff --git a/src/state/queries/messages/create-join-link.ts b/src/state/queries/messages/create-join-link.ts index fbac855466..57269b93e3 100644 --- a/src/state/queries/messages/create-join-link.ts +++ b/src/state/queries/messages/create-join-link.ts @@ -5,9 +5,9 @@ import { } from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import { rollbackConvoOptimistic, updateConvoOptimistic, @@ -24,7 +24,7 @@ export function useCreateJoinLink( }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationFn: async ({ @@ -35,11 +35,11 @@ export function useCreateJoinLink( requireApproval: boolean }) => { if (!convoId) throw new Error('No convoId provided') - const {data} = await agent.chat.bsky.group.createJoinLink( - {convoId, joinRule, requireApproval}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) - return data + return await client.call(chat.bsky.group.createJoinLink, { + convoId, + joinRule, + requireApproval, + }) }, onMutate: ({joinRule, requireApproval}) => { if (!convoId) return diff --git a/src/state/queries/messages/disable-join-link.ts b/src/state/queries/messages/disable-join-link.ts index c143feb6cc..b995dbcc6f 100644 --- a/src/state/queries/messages/disable-join-link.ts +++ b/src/state/queries/messages/disable-join-link.ts @@ -4,10 +4,10 @@ import { } from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' import {invalidateJoinLinkPreviewsForCode} from '#/state/queries/join-links' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import { rollbackConvoOptimistic, updateConvoOptimistic, @@ -24,16 +24,12 @@ export function useDisableJoinLink( }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationFn: async () => { if (!convoId) throw new Error('No convoId provided') - const {data} = await agent.chat.bsky.group.disableJoinLink( - {convoId}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) - return data + return await client.call(chat.bsky.group.disableJoinLink, {convoId}) }, onMutate: () => { if (!convoId) return diff --git a/src/state/queries/messages/edit-join-link.ts b/src/state/queries/messages/edit-join-link.ts index 7e34ce88d9..7870c1f7e3 100644 --- a/src/state/queries/messages/edit-join-link.ts +++ b/src/state/queries/messages/edit-join-link.ts @@ -5,9 +5,9 @@ import { } from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import { rollbackConvoOptimistic, updateConvoOptimistic, @@ -24,7 +24,7 @@ export function useEditJoinLink( }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationFn: async ({ @@ -35,11 +35,11 @@ export function useEditJoinLink( requireApproval: boolean }) => { if (!convoId) throw new Error('No convoId provided') - const {data} = await agent.chat.bsky.group.editJoinLink( - {convoId, joinRule, requireApproval}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) - return data + return await client.call(chat.bsky.group.editJoinLink, { + convoId, + joinRule, + requireApproval, + }) }, onMutate: ({joinRule, requireApproval}) => { if (!convoId) return diff --git a/src/state/queries/messages/enable-join-link.ts b/src/state/queries/messages/enable-join-link.ts index 4febc85441..ef3b168068 100644 --- a/src/state/queries/messages/enable-join-link.ts +++ b/src/state/queries/messages/enable-join-link.ts @@ -1,10 +1,10 @@ import {ChatBskyConvoDefs, type ChatBskyGroupEnableJoinLink} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' import {invalidateJoinLinkPreviewsForCode} from '#/state/queries/join-links' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import { rollbackConvoOptimistic, updateConvoOptimistic, @@ -21,16 +21,12 @@ export function useEnableJoinLink( }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationFn: async () => { if (!convoId) throw new Error('No convoId provided') - const {data} = await agent.chat.bsky.group.enableJoinLink( - {convoId}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) - return data + return await client.call(chat.bsky.group.enableJoinLink, {convoId}) }, onMutate: () => { if (!convoId) return diff --git a/src/state/queries/messages/join-requests.ts b/src/state/queries/messages/join-requests.ts index 179dfafaaf..d36ebd60ae 100644 --- a/src/state/queries/messages/join-requests.ts +++ b/src/state/queries/messages/join-requests.ts @@ -4,15 +4,16 @@ import { type ChatBskyGroupListJoinRequests, type ChatBskyGroupRejectJoinRequest, } from '@atproto/api' +import {type DidString} from '@atproto/syntax' import { type InfiniteData, useMutation, useQueryClient, } from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import {listConvoMembersQueryKey} from './list-convo-members' import {createListJoinRequestsQueryKey} from './list-join-requests' @@ -34,21 +35,28 @@ export function useJoinRequestMutation( }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationFn: async ({member}: {member: string}) => { if (!convoId) throw new Error('No convoId provided') - const {data} = + // callers pass an already-resolved actor did + const memberDid = member as DidString + const data = action === 'approve' - ? await agent.chat.bsky.group.approveJoinRequest( - {convoId, member}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) - : await agent.chat.bsky.group.rejectJoinRequest( - {convoId, member}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) + ? await client.call(chat.bsky.group.approveJoinRequest, { + convoId, + member: memberDid, + }) + : await client.call(chat.bsky.group.rejectJoinRequest, { + convoId, + member: memberDid, + }) + /* + * The two branches have different output schemas, so the union cannot be + * narrowed from the `action` value alone - the cast carries the generic's + * mapping through, exactly as the pre-migration code did. + */ return data as JoinRequestOutput }, onMutate: ({member}) => { diff --git a/src/state/queries/messages/list-join-requests.ts b/src/state/queries/messages/list-join-requests.ts index 64a999c118..bbb82eeca5 100644 --- a/src/state/queries/messages/list-join-requests.ts +++ b/src/state/queries/messages/list-join-requests.ts @@ -2,10 +2,10 @@ import {useEffect} from 'react' import {ChatBskyConvoDefs} from '@atproto/api' import {useInfiniteQuery, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {useMessagesEventBus} from '#/state/messages/events' import {createQueryKey} from '#/state/queries/util' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import {STALE} from '..' export const JOIN_REQUESTS_THRESHOLD = 20 @@ -22,7 +22,7 @@ export function useListJoinRequestsQuery({ convoId: string | undefined enabled?: boolean }) { - const agent = useAgent() + const client = useChatClient() const queryClient = useQueryClient() const messagesBus = useMessagesEventBus() const isEnabled = enabled !== false && !!convoId @@ -54,11 +54,12 @@ export function useListJoinRequestsQuery({ enabled: isEnabled, queryKey: createListJoinRequestsQueryKey({convoId: convoId ?? ''}), queryFn: async ({pageParam}) => { - const {data} = await agent.chat.bsky.group.listJoinRequests( - {convoId: convoId!, cursor: pageParam, limit: JOIN_REQUESTS_THRESHOLD}, - {headers: DM_SERVICE_HEADERS}, - ) - return data + return await client.call(chat.bsky.group.listJoinRequests, { + // guarded by `isEnabled` + convoId: convoId!, + cursor: pageParam, + limit: JOIN_REQUESTS_THRESHOLD, + }) }, initialPageParam: undefined as string | undefined, getNextPageParam: page => page.cursor, diff --git a/src/state/queries/messages/mark-join-request-read.ts b/src/state/queries/messages/mark-join-request-read.ts index ae6d7e88a7..3dfd995af0 100644 --- a/src/state/queries/messages/mark-join-request-read.ts +++ b/src/state/queries/messages/mark-join-request-read.ts @@ -1,9 +1,9 @@ import {ChatBskyConvoDefs} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import {RQKEY as CONVO_KEY} from './conversation' import { type ConvoListQueryData, @@ -12,15 +12,12 @@ import { export function useMarkJoinRequestsRead(convoId: string | undefined) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationFn: async () => { if (!convoId) throw new Error('No convoId provided') - await agent.chat.bsky.group.updateJoinRequestsRead( - {convoId}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) + await client.call(chat.bsky.group.updateJoinRequestsRead, {convoId}) }, onMutate: () => { if (!convoId) return diff --git a/src/state/queries/messages/request-join-group-chat.ts b/src/state/queries/messages/request-join-group-chat.ts index f4d5650346..b0a1922007 100644 --- a/src/state/queries/messages/request-join-group-chat.ts +++ b/src/state/queries/messages/request-join-group-chat.ts @@ -1,9 +1,9 @@ import {type ChatBskyGroupRequestJoin} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent, useSession} from '#/state/session' +import {useChatClient, useSession} from '#/state/session' +import {chat} from '#/lexicons' import {RQKEY_ROOT as REQUESTS_RQKEY_ROOT} from './list-conversation-requests' export function useRequestJoinGroupChat({ @@ -13,7 +13,7 @@ export function useRequestJoinGroupChat({ onSuccess?: (data: ChatBskyGroupRequestJoin.OutputSchema) => void onError?: (error: Error) => void } = {}) { - const agent = useAgent() + const client = useChatClient() const queryClient = useQueryClient() const {hasSession} = useSession() @@ -22,11 +22,7 @@ export function useRequestJoinGroupChat({ if (!hasSession) throw new Error('Must be logged in to join') if (!code) throw new Error('No invite code') - const res = await agent.chat.bsky.group.requestJoin( - {code}, - {headers: DM_SERVICE_HEADERS}, - ) - return res.data + return await client.call(chat.bsky.group.requestJoin, {code}) }, onSuccess: data => { void queryClient.invalidateQueries({queryKey: [REQUESTS_RQKEY_ROOT]}) diff --git a/src/state/queries/messages/withdraw-join-group-chat.ts b/src/state/queries/messages/withdraw-join-group-chat.ts index 2ede8c941d..736128ee25 100644 --- a/src/state/queries/messages/withdraw-join-group-chat.ts +++ b/src/state/queries/messages/withdraw-join-group-chat.ts @@ -1,9 +1,9 @@ import {type ChatBskyGroupWithdrawJoinRequest} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent, useSession} from '#/state/session' +import {useChatClient, useSession} from '#/state/session' +import {chat} from '#/lexicons' import { type ConvoRequestListQueryData, optimisticDeleteJoinRequest, @@ -17,7 +17,7 @@ export function useWithdrawJoinGroupChatRequest({ onSuccess?: (data: ChatBskyGroupWithdrawJoinRequest.OutputSchema) => void onError?: (error: Error) => void } = {}) { - const agent = useAgent() + const client = useChatClient() const queryClient = useQueryClient() const {hasSession} = useSession() @@ -27,11 +27,7 @@ export function useWithdrawJoinGroupChatRequest({ throw new Error('Must be logged in to withdraw a join request') if (!convoId) throw new Error('No convoId provided') - const res = await agent.chat.bsky.group.withdrawJoinRequest( - {convoId}, - {headers: DM_SERVICE_HEADERS}, - ) - return res.data + return await client.call(chat.bsky.group.withdrawJoinRequest, {convoId}) }, onSuccess: (data, {convoId}) => { queryClient.setQueriesData(