Files
bsky-social-app/src/state/queries/messages/conversation.ts
T
Samuel Newman 300a50b69a migrate runtime call sites to lex clients and sdk actions
Phase 3 tasks 3-7 (parallel wave): composer/post pipeline on
pdsClient/appviewClient with structural+instance blob guards and a golden
CID fixture test; chat Convo/EventBus/queries on the dedicated chat client;
preferences sugar to SDK actions on the PDS client; remaining state/queries
producers (usePostThread unspecced flip, video scoped-token clients,
notifications, starter packs, lists) to client.call; UI runtime sweep
(AtUri from @atproto/syntax, moderation fns from @bsky.app/sdk/moderation,
SDK RichText, ozone reason tokens, guard rewrites via #/types/bsky).

Intermediate checkpoint (hooks skipped): ~114 typecheck errors remain in
cross-boundary consumer files, resolved by the type-only codemod next.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 18:12:10 +03:00

198 lines
6.0 KiB
TypeScript

import {
type QueryClient,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
import {STALE} from '#/state/queries'
import {useOnMarkAsRead} from '#/state/queries/messages/list-conversations'
import {useChatClient} from '#/state/session'
import {chat} from '#/lexicons'
import {
RQKEY_PARTIAL as UNREAD_COUNTS_PARTIAL_KEY,
UNREAD_ACCEPTED_CAP,
UNREAD_REQUEST_CAP,
} from './get-unread-counts'
import {
type ConvoListQueryData,
getConvoFromQueryData,
RQKEY_ROOT as LIST_CONVOS_KEY,
} from './list-conversations'
export const RQKEY_ROOT = 'convo'
export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId]
export function useConvoQuery({convoId}: {convoId: string}) {
const chatClient = useChatClient()
return useQuery({
queryKey: RQKEY(convoId),
queryFn: async () => {
const data = await chatClient.call(chat.bsky.convo.getConvo, {convoId})
return data.convo
},
staleTime: STALE.INFINITY,
})
}
export function precacheConvoQuery(
queryClient: QueryClient,
convo: chat.bsky.convo.defs.ConvoView,
) {
queryClient.setQueryData(RQKEY(convo.id), convo)
}
export function useMarkAsReadMutation() {
const optimisticUpdate = useOnMarkAsRead()
const queryClient = useQueryClient()
const chatClient = useChatClient()
return useMutation({
mutationFn: async ({
convoId,
messageId,
}: {
convoId?: string
messageId?: string
}) => {
if (!convoId) throw new Error('No convoId provided')
await chatClient.call(chat.bsky.convo.updateRead, {
convoId,
messageId,
})
},
onMutate({convoId}) {
if (!convoId) throw new Error('No convoId provided')
// snapshot the list caches before the optimistic update so onError can
// restore the convo rows alongside the badge count
const prevListQueries = queryClient.getQueriesData<ConvoListQueryData>({
queryKey: [LIST_CONVOS_KEY],
})
// find the convo so we know which badge counter (if any) to decrement.
// keep scanning past a stale unreadCount === 0 cache so another cache
// holding the true unread state still drives the decrement
let unreadStatus: chat.bsky.convo.defs.ConvoView['status'] | undefined
for (const [, data] of prevListQueries) {
if (!data) continue
const convo = getConvoFromQueryData(convoId, data)
if (convo?.unreadCount) {
unreadStatus = convo.status
break
}
}
optimisticUpdate(convoId)
// the badge count query is a separate server query that the list caches
// don't feed, so decrement it here to keep the badge in sync
const prevUnreadCountsQueries =
queryClient.getQueriesData<chat.bsky.convo.getUnreadCounts.$OutputBody>(
{
queryKey: UNREAD_COUNTS_PARTIAL_KEY,
},
)
if (unreadStatus) {
queryClient.setQueriesData<chat.bsky.convo.getUnreadCounts.$OutputBody>(
{queryKey: UNREAD_COUNTS_PARTIAL_KEY},
old => {
if (!old) return old
return {
...old,
...(unreadStatus === 'request'
? {
unreadRequestConvos:
old.unreadRequestConvos >= UNREAD_REQUEST_CAP
? old.unreadRequestConvos
: Math.max(0, old.unreadRequestConvos - 1),
}
: {
unreadAcceptedConvos:
old.unreadAcceptedConvos >= UNREAD_ACCEPTED_CAP
? old.unreadAcceptedConvos
: Math.max(0, old.unreadAcceptedConvos - 1),
}),
}
},
)
}
return {prevListQueries, prevUnreadCountsQueries}
},
onError(_, __, context) {
if (context?.prevListQueries) {
for (const [queryKey, prevData] of context.prevListQueries) {
queryClient.setQueryData(queryKey, prevData)
}
}
if (context?.prevUnreadCountsQueries) {
for (const [queryKey, prevData] of context.prevUnreadCountsQueries) {
queryClient.setQueryData(queryKey, prevData)
}
}
},
onSuccess(_, {convoId}) {
if (!convoId) return
// the optimistic badge arithmetic can drift from the server (e.g. a convo
// whose status differs between caches, or a sentinel-capped count). invalidate
// so the 15s-stale count query self-corrects on next access rather than
// waiting for a log event
void queryClient.invalidateQueries({queryKey: UNREAD_COUNTS_PARTIAL_KEY})
queryClient.setQueriesData(
{queryKey: [LIST_CONVOS_KEY]},
(old?: ConvoListQueryData) => {
if (!old) return old
const existingConvo = getConvoFromQueryData(convoId, old)
if (existingConvo) {
return {
...old,
pages: old.pages.map(page => {
return {
...page,
convos: page.convos.map(convo => {
if (convo.id === convoId) {
return {
...convo,
unreadCount: 0,
}
}
return convo
}),
}
}),
}
} else {
// If we somehow marked a convo as read that doesn't exist in the
// list, then we don't need to do anything.
}
},
)
},
})
}
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<chat.bsky.actor.defs.ProfileViewBasic, void> {
const queryDatas = queryClient.getQueriesData<
chat.bsky.convo.getConvo.$OutputBody['convo']
>({
queryKey: [RQKEY_ROOT],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData) continue
for (const member of queryData.members) {
if (member.did === did) {
yield member
}
}
}
}