Colocate all chat log event handlers (#10632)

This commit is contained in:
DS Boyce
2026-05-28 12:51:40 -07:00
committed by GitHub
parent 672cebef62
commit 3e46ca391f
2 changed files with 196 additions and 94 deletions
@@ -1,5 +1,6 @@
import {createContext, useCallback, useContext, useEffect, useMemo} from 'react'
import {
type ChatBskyActorDefs,
ChatBskyConvoDefs,
type ChatBskyConvoListConvos,
moderateProfile,
@@ -19,7 +20,10 @@ import {useMessagesEventBus} from '#/state/messages/events'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useAgent, useSession} from '#/state/session'
import {parseConvoView} from '#/components/dms/util'
import * as bsky from '#/types/bsky'
import {RQKEY as CONVO_KEY} from './conversation'
import {useLeftConvos} from './leave-conversation'
import {listConvoMembersQueryKey} from './list-convo-members'
const DEFAULT_LIMIT = 10
export const UNREAD_LIMIT = 20
@@ -138,6 +142,80 @@ export function ListConvosProviderInner({
events => {
if (events.type !== 'logs') return
function mutateMembers(
convoId: string,
fn: (
members: ChatBskyActorDefs.ProfileViewBasic[],
) => ChatBskyActorDefs.ProfileViewBasic[],
) {
queryClient.setQueryData<ChatBskyActorDefs.ProfileViewBasic[]>(
listConvoMembersQueryKey(convoId),
old => {
if (!old) return // query doesn't exist yet, skip
return fn(old)
},
)
}
function mutateConvoView(
convoId: string,
fn: (
convo: ChatBskyConvoDefs.ConvoView,
) => ChatBskyConvoDefs.ConvoView,
) {
queryClient.setQueryData<ChatBskyConvoDefs.ConvoView>(
CONVO_KEY(convoId),
old => (old ? fn(old) : old),
)
queryClient.setQueriesData<ConvoListQueryData>(
{queryKey: [RQKEY_ROOT]},
old => optimisticUpdate(convoId, old, fn),
)
}
function handleMemberAdded(
convoId: string,
did: string,
relatedProfiles: ChatBskyActorDefs.ProfileViewBasic[],
rev: string,
) {
const newMember = relatedProfiles.find(r => r.did === did)
if (!newMember) return
// If the optimistic add already added them, skip the
// memberCount bump to avoid double-counting.
const alreadyKnownMember =
queryClient
.getQueryData<
ChatBskyActorDefs.ProfileViewBasic[]
>(listConvoMembersQueryKey(convoId))
?.some(m => m.did === did) ?? false
mutateMembers(convoId, list =>
list.some(m => m.did === did) ? list : list.concat(newMember),
)
mutateConvoView(convoId, convo =>
addMemberToConvoView(convo, newMember, rev, alreadyKnownMember),
)
}
function handleMemberRemoved(
convoId: string,
did: string,
rev: string,
) {
// If the optimistic remove already dropped them from the full
// list, skip the memberCount decrement to avoid double-counting.
const alreadyRemovedMember =
queryClient
.getQueryData<
ChatBskyActorDefs.ProfileViewBasic[]
>(listConvoMembersQueryKey(convoId))
?.some(m => m.did === did) === false
mutateMembers(convoId, list => list.filter(m => m.did !== did))
mutateConvoView(convoId, convo =>
removeMemberFromConvoView(convo, did, rev, alreadyRemovedMember),
)
}
for (const log of events.logs) {
if (ChatBskyConvoDefs.isLogBeginConvo(log)) {
debouncedRefetch()
@@ -431,6 +509,74 @@ export function ListConvosProviderInner({
rev: log.rev,
})),
)
} else if (ChatBskyConvoDefs.isLogAddMember(log)) {
const data = log.message.data
if (
bsky.dangerousIsType<ChatBskyConvoDefs.SystemMessageDataAddMember>(
data,
ChatBskyConvoDefs.isSystemMessageDataAddMember,
)
) {
handleMemberAdded(
log.convoId,
data.member.did,
log.relatedProfiles,
log.rev,
)
}
// Refetch so the server can refresh the curated members list.
void queryClient.invalidateQueries({
queryKey: CONVO_KEY(log.convoId),
})
debouncedRefetch()
} else if (ChatBskyConvoDefs.isLogRemoveMember(log)) {
const data = log.message.data
if (
bsky.dangerousIsType<ChatBskyConvoDefs.SystemMessageDataRemoveMember>(
data,
ChatBskyConvoDefs.isSystemMessageDataRemoveMember,
)
) {
handleMemberRemoved(log.convoId, data.member.did, log.rev)
}
// Refetch so the server can refill the curated members list.
void queryClient.invalidateQueries({
queryKey: CONVO_KEY(log.convoId),
})
debouncedRefetch()
} else if (ChatBskyConvoDefs.isLogMemberJoin(log)) {
const data = log.message.data
if (
bsky.dangerousIsType<ChatBskyConvoDefs.SystemMessageDataMemberJoin>(
data,
ChatBskyConvoDefs.isSystemMessageDataMemberJoin,
)
) {
handleMemberAdded(
log.convoId,
data.member.did,
log.relatedProfiles,
log.rev,
)
}
void queryClient.invalidateQueries({
queryKey: CONVO_KEY(log.convoId),
})
debouncedRefetch()
} else if (ChatBskyConvoDefs.isLogMemberLeave(log)) {
const data = log.message.data
if (
bsky.dangerousIsType<ChatBskyConvoDefs.SystemMessageDataMemberLeave>(
data,
ChatBskyConvoDefs.isSystemMessageDataMemberLeave,
)
) {
handleMemberRemoved(log.convoId, data.member.did, log.rev)
}
void queryClient.invalidateQueries({
queryKey: CONVO_KEY(log.convoId),
})
debouncedRefetch()
} else if (ChatBskyConvoDefs.isLogRemoveReaction(log)) {
queryClient.setQueriesData(
{queryKey: [RQKEY_ROOT]},
@@ -616,6 +762,53 @@ function optimisticUpdate(
}
}
function removeMemberFromConvoView(
convo: ChatBskyConvoDefs.ConvoView,
did: string,
rev: string,
alreadyRemovedMember: boolean,
): ChatBskyConvoDefs.ConvoView {
// Member add/remove/join/leave events are only meaningful for group convos.
if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) return convo
const nextMembers = convo.members.filter(m => m.did !== did)
return {
...convo,
rev,
members: nextMembers,
kind: {
...convo.kind,
memberCount: alreadyRemovedMember
? convo.kind.memberCount
: Math.max(0, convo.kind.memberCount - 1),
},
}
}
function addMemberToConvoView(
convo: ChatBskyConvoDefs.ConvoView,
member: ChatBskyActorDefs.ProfileViewBasic,
rev: string,
alreadyKnownMember: boolean,
): ChatBskyConvoDefs.ConvoView {
// Member add/remove/join/leave events are only meaningful for group convos.
if (!ChatBskyConvoDefs.isGroupConvo(convo.kind)) return convo
const alreadyInCuratedList = convo.members.some(m => m.did === member.did)
const nextMembers = alreadyInCuratedList
? convo.members
: convo.members.concat(member)
return {
...convo,
rev,
members: nextMembers,
kind: {
...convo.kind,
memberCount: alreadyKnownMember
? convo.kind.memberCount
: convo.kind.memberCount + 1,
},
}
}
function optimisticDelete(chatId: string, old?: ConvoListQueryData) {
if (!old) return old
@@ -1,19 +1,16 @@
import {useEffect} from 'react'
import {type ChatBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api'
import {type QueryClient, useQuery, useQueryClient} from '@tanstack/react-query'
import {type ChatBskyActorDefs} from '@atproto/api'
import {type QueryClient, useQuery} from '@tanstack/react-query'
import {DM_SERVICE_HEADERS} from '#/lib/constants'
import {useMessagesEventBus} from '#/state/messages/events'
import {STALE} from '#/state/queries'
import {createQueryKey} from '#/state/queries/util'
import {useAgent} from '#/state/session'
import * as bsky from '#/types/bsky'
const RQKEY_ROOT = 'listConvoMembers'
export const listConvoMembersQueryKey = (convoId: string) =>
createQueryKey(RQKEY_ROOT, {convoId})
// group chat size is 50, so should fetch the whole list in one go
// Group chat size is at least 50, so should fetch the whole list in one go
const LIMIT = 50
export function useListConvoMembersQuery({
@@ -24,94 +21,6 @@ export function useListConvoMembersQuery({
placeholderData?: ChatBskyActorDefs.ProfileViewBasic[]
}) {
const agent = useAgent()
const queryClient = useQueryClient()
const messagesBus = useMessagesEventBus()
useEffect(() => {
const unsub = messagesBus.on(
ev => {
if (ev.type !== 'logs') return
function mutateList(
fn: (
update: ChatBskyActorDefs.ProfileViewBasic[],
) => ChatBskyActorDefs.ProfileViewBasic[],
) {
queryClient.setQueryData<ChatBskyActorDefs.ProfileViewBasic[]>(
listConvoMembersQueryKey(convoId),
old => {
if (!old) return // query doesn't exist yet, skip
return fn(old)
},
)
}
for (const log of ev.logs) {
if (ChatBskyConvoDefs.isLogAddMember(log)) {
const data = log.message.data
if (
bsky.dangerousIsType<ChatBskyConvoDefs.SystemMessageDataAddMember>(
data,
ChatBskyConvoDefs.isSystemMessageDataAddMember,
)
) {
const newMember = log.relatedProfiles.find(
r => r.did === data.member.did,
)
if (newMember) {
mutateList(list =>
list.some(m => m.did === newMember.did)
? list
: list.concat(newMember),
)
}
}
} else if (ChatBskyConvoDefs.isLogRemoveMember(log)) {
const data = log.message.data
if (
bsky.dangerousIsType<ChatBskyConvoDefs.SystemMessageDataRemoveMember>(
data,
ChatBskyConvoDefs.isSystemMessageDataRemoveMember,
)
) {
mutateList(list => list.filter(m => m.did !== data.member.did))
}
} else if (ChatBskyConvoDefs.isLogMemberJoin(log)) {
const data = log.message.data
if (
bsky.dangerousIsType<ChatBskyConvoDefs.SystemMessageDataMemberJoin>(
data,
ChatBskyConvoDefs.isSystemMessageDataMemberJoin,
)
) {
const newMember = log.relatedProfiles.find(
r => r.did === data.member.did,
)
if (newMember) {
mutateList(list =>
list.some(m => m.did === newMember.did)
? list
: list.concat(newMember),
)
}
}
} else if (ChatBskyConvoDefs.isLogMemberLeave(log)) {
const data = log.message.data
if (
bsky.dangerousIsType<ChatBskyConvoDefs.SystemMessageDataMemberLeave>(
data,
ChatBskyConvoDefs.isSystemMessageDataMemberLeave,
)
) {
mutateList(list => list.filter(m => m.did !== data.member.did))
}
}
}
},
{convoId},
)
return () => unsub()
}, [convoId, messagesBus, queryClient])
return useQuery({
queryKey: listConvoMembersQueryKey(convoId),