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 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-08-03 16:24:21 +03:00
parent 304726b83d
commit 74f4532922
10 changed files with 98 additions and 95 deletions
+33 -20
View File
@@ -1,17 +1,19 @@
import {useCallback} from 'react' import {useCallback} from 'react'
import { import {
type $Typed, type $Typed,
AtpAgent,
ChatBskyGroupDefs, ChatBskyGroupDefs,
type ChatBskyGroupGetJoinLinkPreviews, type ChatBskyGroupGetJoinLinkPreviews,
} from '@atproto/api' } from '@atproto/api'
import {type Client} from '@atproto/lex'
import {type QueryClient, useQuery, useQueryClient} from '@tanstack/react-query' 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 {logger} from '#/logger'
import {STALE} from '#/state/queries/index' import {STALE} from '#/state/queries/index'
import {createQueryKey, type StructuredQueryKey} from '#/state/queries/util' 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}` * 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({ async function fetchJoinLinkPreviews({
agent, client,
codes, codes,
hasSession, hasSession,
}: { }: {
agent: AtpAgent client: Client
codes: string[] codes: string[]
hasSession: boolean hasSession: boolean
}) { }) {
const previewAgent = new AtpAgent({service: CHAT_SERVICE}) return await (hasSession ? client : getPublicChatClient()).call(
const res = hasSession chat.bsky.group.getJoinLinkPreviews,
? await agent.chat.bsky.group.getJoinLinkPreviews( {codes},
{codes}, )
{headers: DM_SERVICE_HEADERS},
)
: await previewAgent.chat.bsky.group.getJoinLinkPreviews({codes})
return res.data
} }
export function useJoinLinkPreviewsQuery({ export function useJoinLinkPreviewsQuery({
@@ -188,14 +201,14 @@ export function useJoinLinkPreviewsQuery({
*/ */
initialData?: ChatBskyGroupGetJoinLinkPreviews.OutputSchema initialData?: ChatBskyGroupGetJoinLinkPreviews.OutputSchema
}) { }) {
const agent = useAgent() const client = useChatClient()
return useQuery({ return useQuery({
queryKey: createJoinLinkPreviewQueryKey({codes: codes ?? [], hasSession}), queryKey: createJoinLinkPreviewQueryKey({codes: codes ?? [], hasSession}),
queryFn: async () => { queryFn: async () => {
if (!codes) throw new Error('No invite code') if (!codes) throw new Error('No invite code')
try { try {
return await fetchJoinLinkPreviews({agent, codes, hasSession}) return await fetchJoinLinkPreviews({client, codes, hasSession})
} catch (error) { } catch (error) {
logger.error('Failed to fetch join link preview', {safeMessage: error}) logger.error('Failed to fetch join link preview', {safeMessage: error})
throw error throw error
@@ -208,13 +221,13 @@ export function useJoinLinkPreviewsQuery({
} }
export function usePrefetchJoinLinkPreviews() { export function usePrefetchJoinLinkPreviews() {
const agent = useAgent() const client = useChatClient()
const queryClient = useQueryClient() const queryClient = useQueryClient()
return ({codes, hasSession}: {codes: string[]; hasSession: boolean}) => { return ({codes, hasSession}: {codes: string[]; hasSession: boolean}) => {
return queryClient.prefetchQuery({ return queryClient.prefetchQuery({
queryKey: createJoinLinkPreviewQueryKey({codes, hasSession}), queryKey: createJoinLinkPreviewQueryKey({codes, hasSession}),
queryFn: () => fetchJoinLinkPreviews({agent, codes, hasSession}), queryFn: () => fetchJoinLinkPreviews({client, codes, hasSession}),
staleTime: STALE.SECONDS.FIFTEEN, staleTime: STALE.SECONDS.FIFTEEN,
}) })
} }
@@ -226,7 +239,7 @@ export function usePrefetchJoinLinkPreviews() {
* Returns undefined if the preview can't be resolved. * Returns undefined if the preview can't be resolved.
*/ */
export function useGetJoinLinkPreview() { export function useGetJoinLinkPreview() {
const agent = useAgent() const client = useChatClient()
const queryClient = useQueryClient() const queryClient = useQueryClient()
return useCallback( return useCallback(
@@ -241,7 +254,7 @@ export function useGetJoinLinkPreview() {
const data = await queryClient.fetchQuery({ const data = await queryClient.fetchQuery({
queryKey: createJoinLinkPreviewQueryKey({codes: [code], hasSession}), queryKey: createJoinLinkPreviewQueryKey({codes: [code], hasSession}),
queryFn: () => queryFn: () =>
fetchJoinLinkPreviews({agent, codes: [code], hasSession}), fetchJoinLinkPreviews({client, codes: [code], hasSession}),
staleTime: STALE.SECONDS.FIFTEEN, staleTime: STALE.SECONDS.FIFTEEN,
}) })
const found = data.joinLinkPreviews[0] const found = data.joinLinkPreviews[0]
@@ -251,6 +264,6 @@ export function useGetJoinLinkPreview() {
return undefined return undefined
} }
}, },
[agent, queryClient], [client, queryClient],
) )
} }
@@ -5,9 +5,9 @@ import {
} from '@atproto/api' } from '@atproto/api'
import {useMutation, useQueryClient} from '@tanstack/react-query' import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useAgent} from '#/state/session' import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import { import {
rollbackConvoOptimistic, rollbackConvoOptimistic,
updateConvoOptimistic, updateConvoOptimistic,
@@ -24,7 +24,7 @@ export function useCreateJoinLink(
}, },
) { ) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const agent = useAgent() const client = useChatClient()
return useMutation({ return useMutation({
mutationFn: async ({ mutationFn: async ({
@@ -35,11 +35,11 @@ export function useCreateJoinLink(
requireApproval: boolean requireApproval: boolean
}) => { }) => {
if (!convoId) throw new Error('No convoId provided') if (!convoId) throw new Error('No convoId provided')
const {data} = await agent.chat.bsky.group.createJoinLink( return await client.call(chat.bsky.group.createJoinLink, {
{convoId, joinRule, requireApproval}, convoId,
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, joinRule,
) requireApproval,
return data })
}, },
onMutate: ({joinRule, requireApproval}) => { onMutate: ({joinRule, requireApproval}) => {
if (!convoId) return if (!convoId) return
@@ -4,10 +4,10 @@ import {
} from '@atproto/api' } from '@atproto/api'
import {useMutation, useQueryClient} from '@tanstack/react-query' import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger' import {logger} from '#/logger'
import {invalidateJoinLinkPreviewsForCode} from '#/state/queries/join-links' import {invalidateJoinLinkPreviewsForCode} from '#/state/queries/join-links'
import {useAgent} from '#/state/session' import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import { import {
rollbackConvoOptimistic, rollbackConvoOptimistic,
updateConvoOptimistic, updateConvoOptimistic,
@@ -24,16 +24,12 @@ export function useDisableJoinLink(
}, },
) { ) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const agent = useAgent() const client = useChatClient()
return useMutation({ return useMutation({
mutationFn: async () => { mutationFn: async () => {
if (!convoId) throw new Error('No convoId provided') if (!convoId) throw new Error('No convoId provided')
const {data} = await agent.chat.bsky.group.disableJoinLink( return await client.call(chat.bsky.group.disableJoinLink, {convoId})
{convoId},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
return data
}, },
onMutate: () => { onMutate: () => {
if (!convoId) return if (!convoId) return
+8 -8
View File
@@ -5,9 +5,9 @@ import {
} from '@atproto/api' } from '@atproto/api'
import {useMutation, useQueryClient} from '@tanstack/react-query' import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useAgent} from '#/state/session' import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import { import {
rollbackConvoOptimistic, rollbackConvoOptimistic,
updateConvoOptimistic, updateConvoOptimistic,
@@ -24,7 +24,7 @@ export function useEditJoinLink(
}, },
) { ) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const agent = useAgent() const client = useChatClient()
return useMutation({ return useMutation({
mutationFn: async ({ mutationFn: async ({
@@ -35,11 +35,11 @@ export function useEditJoinLink(
requireApproval: boolean requireApproval: boolean
}) => { }) => {
if (!convoId) throw new Error('No convoId provided') if (!convoId) throw new Error('No convoId provided')
const {data} = await agent.chat.bsky.group.editJoinLink( return await client.call(chat.bsky.group.editJoinLink, {
{convoId, joinRule, requireApproval}, convoId,
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, joinRule,
) requireApproval,
return data })
}, },
onMutate: ({joinRule, requireApproval}) => { onMutate: ({joinRule, requireApproval}) => {
if (!convoId) return if (!convoId) return
@@ -1,10 +1,10 @@
import {ChatBskyConvoDefs, type ChatBskyGroupEnableJoinLink} from '@atproto/api' import {ChatBskyConvoDefs, type ChatBskyGroupEnableJoinLink} from '@atproto/api'
import {useMutation, useQueryClient} from '@tanstack/react-query' import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger' import {logger} from '#/logger'
import {invalidateJoinLinkPreviewsForCode} from '#/state/queries/join-links' import {invalidateJoinLinkPreviewsForCode} from '#/state/queries/join-links'
import {useAgent} from '#/state/session' import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import { import {
rollbackConvoOptimistic, rollbackConvoOptimistic,
updateConvoOptimistic, updateConvoOptimistic,
@@ -21,16 +21,12 @@ export function useEnableJoinLink(
}, },
) { ) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const agent = useAgent() const client = useChatClient()
return useMutation({ return useMutation({
mutationFn: async () => { mutationFn: async () => {
if (!convoId) throw new Error('No convoId provided') if (!convoId) throw new Error('No convoId provided')
const {data} = await agent.chat.bsky.group.enableJoinLink( return await client.call(chat.bsky.group.enableJoinLink, {convoId})
{convoId},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
return data
}, },
onMutate: () => { onMutate: () => {
if (!convoId) return if (!convoId) return
+20 -12
View File
@@ -4,15 +4,16 @@ import {
type ChatBskyGroupListJoinRequests, type ChatBskyGroupListJoinRequests,
type ChatBskyGroupRejectJoinRequest, type ChatBskyGroupRejectJoinRequest,
} from '@atproto/api' } from '@atproto/api'
import {type DidString} from '@atproto/syntax'
import { import {
type InfiniteData, type InfiniteData,
useMutation, useMutation,
useQueryClient, useQueryClient,
} from '@tanstack/react-query' } from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger' 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 {listConvoMembersQueryKey} from './list-convo-members'
import {createListJoinRequestsQueryKey} from './list-join-requests' import {createListJoinRequestsQueryKey} from './list-join-requests'
@@ -34,21 +35,28 @@ export function useJoinRequestMutation<A extends JoinRequestAction>(
}, },
) { ) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const agent = useAgent() const client = useChatClient()
return useMutation({ return useMutation({
mutationFn: async ({member}: {member: string}) => { mutationFn: async ({member}: {member: string}) => {
if (!convoId) throw new Error('No convoId provided') 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' action === 'approve'
? await agent.chat.bsky.group.approveJoinRequest( ? await client.call(chat.bsky.group.approveJoinRequest, {
{convoId, member}, convoId,
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, member: memberDid,
) })
: await agent.chat.bsky.group.rejectJoinRequest( : await client.call(chat.bsky.group.rejectJoinRequest, {
{convoId, member}, convoId,
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, 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<A> return data as JoinRequestOutput<A>
}, },
onMutate: ({member}) => { onMutate: ({member}) => {
@@ -2,10 +2,10 @@ import {useEffect} from 'react'
import {ChatBskyConvoDefs} from '@atproto/api' import {ChatBskyConvoDefs} from '@atproto/api'
import {useInfiniteQuery, useQueryClient} from '@tanstack/react-query' import {useInfiniteQuery, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {useMessagesEventBus} from '#/state/messages/events' import {useMessagesEventBus} from '#/state/messages/events'
import {createQueryKey} from '#/state/queries/util' import {createQueryKey} from '#/state/queries/util'
import {useAgent} from '#/state/session' import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import {STALE} from '..' import {STALE} from '..'
export const JOIN_REQUESTS_THRESHOLD = 20 export const JOIN_REQUESTS_THRESHOLD = 20
@@ -22,7 +22,7 @@ export function useListJoinRequestsQuery({
convoId: string | undefined convoId: string | undefined
enabled?: boolean enabled?: boolean
}) { }) {
const agent = useAgent() const client = useChatClient()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const messagesBus = useMessagesEventBus() const messagesBus = useMessagesEventBus()
const isEnabled = enabled !== false && !!convoId const isEnabled = enabled !== false && !!convoId
@@ -54,11 +54,12 @@ export function useListJoinRequestsQuery({
enabled: isEnabled, enabled: isEnabled,
queryKey: createListJoinRequestsQueryKey({convoId: convoId ?? ''}), queryKey: createListJoinRequestsQueryKey({convoId: convoId ?? ''}),
queryFn: async ({pageParam}) => { queryFn: async ({pageParam}) => {
const {data} = await agent.chat.bsky.group.listJoinRequests( return await client.call(chat.bsky.group.listJoinRequests, {
{convoId: convoId!, cursor: pageParam, limit: JOIN_REQUESTS_THRESHOLD}, // guarded by `isEnabled`
{headers: DM_SERVICE_HEADERS}, convoId: convoId!,
) cursor: pageParam,
return data limit: JOIN_REQUESTS_THRESHOLD,
})
}, },
initialPageParam: undefined as string | undefined, initialPageParam: undefined as string | undefined,
getNextPageParam: page => page.cursor, getNextPageParam: page => page.cursor,
@@ -1,9 +1,9 @@
import {ChatBskyConvoDefs} from '@atproto/api' import {ChatBskyConvoDefs} from '@atproto/api'
import {useMutation, useQueryClient} from '@tanstack/react-query' import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger' 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 {RQKEY as CONVO_KEY} from './conversation'
import { import {
type ConvoListQueryData, type ConvoListQueryData,
@@ -12,15 +12,12 @@ import {
export function useMarkJoinRequestsRead(convoId: string | undefined) { export function useMarkJoinRequestsRead(convoId: string | undefined) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const agent = useAgent() const client = useChatClient()
return useMutation({ return useMutation({
mutationFn: async () => { mutationFn: async () => {
if (!convoId) throw new Error('No convoId provided') if (!convoId) throw new Error('No convoId provided')
await agent.chat.bsky.group.updateJoinRequestsRead( await client.call(chat.bsky.group.updateJoinRequestsRead, {convoId})
{convoId},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
}, },
onMutate: () => { onMutate: () => {
if (!convoId) return if (!convoId) return
@@ -1,9 +1,9 @@
import {type ChatBskyGroupRequestJoin} from '@atproto/api' import {type ChatBskyGroupRequestJoin} from '@atproto/api'
import {useMutation, useQueryClient} from '@tanstack/react-query' import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger' 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' import {RQKEY_ROOT as REQUESTS_RQKEY_ROOT} from './list-conversation-requests'
export function useRequestJoinGroupChat({ export function useRequestJoinGroupChat({
@@ -13,7 +13,7 @@ export function useRequestJoinGroupChat({
onSuccess?: (data: ChatBskyGroupRequestJoin.OutputSchema) => void onSuccess?: (data: ChatBskyGroupRequestJoin.OutputSchema) => void
onError?: (error: Error) => void onError?: (error: Error) => void
} = {}) { } = {}) {
const agent = useAgent() const client = useChatClient()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {hasSession} = useSession() const {hasSession} = useSession()
@@ -22,11 +22,7 @@ export function useRequestJoinGroupChat({
if (!hasSession) throw new Error('Must be logged in to join') if (!hasSession) throw new Error('Must be logged in to join')
if (!code) throw new Error('No invite code') if (!code) throw new Error('No invite code')
const res = await agent.chat.bsky.group.requestJoin( return await client.call(chat.bsky.group.requestJoin, {code})
{code},
{headers: DM_SERVICE_HEADERS},
)
return res.data
}, },
onSuccess: data => { onSuccess: data => {
void queryClient.invalidateQueries({queryKey: [REQUESTS_RQKEY_ROOT]}) void queryClient.invalidateQueries({queryKey: [REQUESTS_RQKEY_ROOT]})
@@ -1,9 +1,9 @@
import {type ChatBskyGroupWithdrawJoinRequest} from '@atproto/api' import {type ChatBskyGroupWithdrawJoinRequest} from '@atproto/api'
import {useMutation, useQueryClient} from '@tanstack/react-query' import {useMutation, useQueryClient} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useAgent, useSession} from '#/state/session' import {useChatClient, useSession} from '#/state/session'
import {chat} from '#/lexicons'
import { import {
type ConvoRequestListQueryData, type ConvoRequestListQueryData,
optimisticDeleteJoinRequest, optimisticDeleteJoinRequest,
@@ -17,7 +17,7 @@ export function useWithdrawJoinGroupChatRequest({
onSuccess?: (data: ChatBskyGroupWithdrawJoinRequest.OutputSchema) => void onSuccess?: (data: ChatBskyGroupWithdrawJoinRequest.OutputSchema) => void
onError?: (error: Error) => void onError?: (error: Error) => void
} = {}) { } = {}) {
const agent = useAgent() const client = useChatClient()
const queryClient = useQueryClient() const queryClient = useQueryClient()
const {hasSession} = useSession() const {hasSession} = useSession()
@@ -27,11 +27,7 @@ export function useWithdrawJoinGroupChatRequest({
throw new Error('Must be logged in to withdraw a join request') throw new Error('Must be logged in to withdraw a join request')
if (!convoId) throw new Error('No convoId provided') if (!convoId) throw new Error('No convoId provided')
const res = await agent.chat.bsky.group.withdrawJoinRequest( return await client.call(chat.bsky.group.withdrawJoinRequest, {convoId})
{convoId},
{headers: DM_SERVICE_HEADERS},
)
return res.data
}, },
onSuccess: (data, {convoId}) => { onSuccess: (data, {convoId}) => {
queryClient.setQueriesData<ConvoRequestListQueryData>( queryClient.setQueriesData<ConvoRequestListQueryData>(