[SDK] Migrate group chat and join links to the chat client (#11358)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-08-13 22:26:16 +03:00
committed by GitHub
parent 0e8e0b2ab4
commit ea4f70fb1e
21 changed files with 263 additions and 259 deletions
+38 -20
View File
@@ -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,39 @@ 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.
*
* Unlike the public appview client this does NOT wrap `networkAwareFetch`,
* matching the plain-fetch behavior of the ad-hoc agent it replaces: a
* logged-out link resolution failing says nothing useful about the session's
* reachability, so it should not move the app-wide network signal.
*/
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 +206,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 +226,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 +244,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 +259,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 +269,6 @@ export function useGetJoinLinkPreview() {
return undefined
}
},
[agent, queryClient],
[client, queryClient],
)
}
@@ -4,16 +4,17 @@ import {
type ChatBskyConvoListConvos,
type ChatBskyGroupAddMembers,
} 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 {useProfileQuery} from '#/state/queries/profile'
import {useAgent, useSession} from '#/state/session'
import {useChatClient, useSession} from '#/state/session'
import {chat} from '#/lexicons'
import type * as bsky from '#/types/bsky'
import {RQKEY as CONVO_KEY} from './conversation'
import {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations'
@@ -30,7 +31,7 @@ export function useAddGroupMembers(
},
) {
const queryClient = useQueryClient()
const agent = useAgent()
const client = useChatClient()
const {currentAccount} = useSession()
const {data: myProfile} = useProfileQuery({did: currentAccount?.did})
@@ -42,11 +43,11 @@ export function useAddGroupMembers(
profiles: bsky.profile.AnyProfileView[]
}) => {
if (!convoId) throw new Error('No convoId provided')
const {data} = await agent.chat.bsky.group.addMembers(
{convoId, members},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
return data
return await client.call(chat.bsky.group.addMembers, {
convoId,
// callers pass already-resolved actor dids
members: members as DidString[],
})
},
onMutate: ({profiles}) => {
if (!convoId) return
@@ -1,9 +1,10 @@
import {type ChatBskyGroupCreateGroup} from '@atproto/api'
import {type DidString} from '@atproto/syntax'
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 {precacheConvoQuery} from './conversation'
export function useCreateGroupChat({
@@ -14,16 +15,15 @@ export function useCreateGroupChat({
onError?: (error: Error) => void
}) {
const queryClient = useQueryClient()
const agent = useAgent()
const client = useChatClient()
return useMutation({
mutationFn: async ({name, members}: {name: string; members: string[]}) => {
const {data} = await agent.chat.bsky.group.createGroup(
{name, members},
{headers: DM_SERVICE_HEADERS},
)
return data
return await client.call(chat.bsky.group.createGroup, {
name,
// callers pass already-resolved actor dids
members: members as DidString[],
})
},
onSuccess: data => {
precacheConvoQuery(queryClient, data.convo)
@@ -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
@@ -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
@@ -1,9 +1,9 @@
import {ChatBskyConvoDefs, type ChatBskyGroupEditGroup} 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,
@@ -20,16 +20,15 @@ export function useEditGroupChatName(
},
) {
const queryClient = useQueryClient()
const agent = useAgent()
const client = useChatClient()
return useMutation({
mutationFn: async ({name: groupName}: {name: string}) => {
if (!convoId) throw new Error('No convoId provided')
const {data} = await agent.chat.bsky.group.editGroup(
{convoId, name: groupName},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
return data
return await client.call(chat.bsky.group.editGroup, {
convoId,
name: groupName,
})
},
onMutate: ({name: groupName}) => {
if (!convoId) return
+8 -8
View File
@@ -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
@@ -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
+20 -12
View File
@@ -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<A extends JoinRequestAction>(
},
) {
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<A>
},
onMutate: ({member}) => {
@@ -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,
@@ -1,8 +1,9 @@
import {type DidString} from '@atproto/syntax'
import {useInfiniteQuery} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {createQueryKey} from '#/state/queries/util'
import {useAgent} from '#/state/session'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
const listMutualGroupsQueryKeyRoot = 'list-mutual-groups'
@@ -18,7 +19,7 @@ export function useListMutualGroupsQuery({
enabled?: boolean
limit?: number
}) {
const agent = useAgent()
const client = useChatClient()
const isEnabled = enabled !== false && !!subject
return useInfiniteQuery({
@@ -27,11 +28,12 @@ export function useListMutualGroupsQuery({
enabled: isEnabled,
queryKey: createListMutualGroupsQueryKey({subject: subject ?? ''}),
queryFn: async ({pageParam}) => {
const {data} = await agent.chat.bsky.group.listMutualGroups(
{subject: subject!, cursor: pageParam, limit},
{headers: DM_SERVICE_HEADERS},
)
return data
return await client.call(chat.bsky.group.listMutualGroups, {
// guarded by `enabled`, and callers pass a resolved actor did
subject: subject as DidString,
cursor: pageParam,
limit,
})
},
initialPageParam: undefined as string | undefined,
getNextPageParam: page => page.cursor,
@@ -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
@@ -4,15 +4,16 @@ import {
type ChatBskyConvoListConvos,
type ChatBskyGroupRemoveMembers,
} 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 {RQKEY as CONVO_KEY} from './conversation'
import {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations'
import {listConvoMembersQueryKey} from './list-convo-members'
@@ -28,16 +29,16 @@ export function useRemoveFromGroupChat(
},
) {
const queryClient = useQueryClient()
const agent = useAgent()
const client = useChatClient()
return useMutation({
mutationFn: async ({members}: {members: string[]}) => {
if (!convoId) throw new Error('No convoId provided')
const {data} = await agent.chat.bsky.group.removeMembers(
{convoId, members},
{headers: DM_SERVICE_HEADERS, encoding: 'application/json'},
)
return data
return await client.call(chat.bsky.group.removeMembers, {
convoId,
// callers pass already-resolved actor dids
members: members as DidString[],
})
},
onMutate: ({members}) => {
if (!convoId) return
@@ -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]})
@@ -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<ConvoRequestListQueryData>(