extract chat log event handler for testability
Pure refactor: move the log-event handling out of the ListConvosProviderInner effect closure into an exported handleConvoLogEvents function, so tests can drive it directly with a QueryClient and a batch of logs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ import throttle from 'lodash.throttle'
|
|||||||
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
import {DM_SERVICE_HEADERS} from '#/lib/constants'
|
||||||
import {useCurrentConvoId} from '#/state/messages/current-convo-id'
|
import {useCurrentConvoId} from '#/state/messages/current-convo-id'
|
||||||
import {useMessagesEventBus} from '#/state/messages/events'
|
import {useMessagesEventBus} from '#/state/messages/events'
|
||||||
|
import {type MessagesEventBusEvent} from '#/state/messages/events/types'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {invalidateJoinLinkPreviewsForConvo} from '#/state/queries/join-links'
|
import {invalidateJoinLinkPreviewsForConvo} from '#/state/queries/join-links'
|
||||||
import {useAgent, useSession} from '#/state/session'
|
import {useAgent, useSession} from '#/state/session'
|
||||||
@@ -207,7 +208,63 @@ export function ListConvosProviderInner({
|
|||||||
const unsub = messagesBus.on(
|
const unsub = messagesBus.on(
|
||||||
events => {
|
events => {
|
||||||
if (events.type !== 'logs') return
|
if (events.type !== 'logs') return
|
||||||
|
handleConvoLogEvents({
|
||||||
|
queryClient,
|
||||||
|
logs: events.logs,
|
||||||
|
currentConvoId,
|
||||||
|
currentAccountDid: currentAccount?.did,
|
||||||
|
onRefetchNeeded: debouncedRefetch,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// get events for all chats
|
||||||
|
convoId: undefined,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => unsub()
|
||||||
|
}, [
|
||||||
|
messagesBus,
|
||||||
|
currentConvoId,
|
||||||
|
queryClient,
|
||||||
|
currentAccount?.did,
|
||||||
|
debouncedRefetch,
|
||||||
|
])
|
||||||
|
|
||||||
|
const ctx = useMemo(() => {
|
||||||
|
const convos = data?.pages.flatMap(page => page.convos) ?? []
|
||||||
|
return {
|
||||||
|
accepted: convos.filter(conv => conv.status === 'accepted'),
|
||||||
|
request: convos.filter(conv => conv.status === 'request'),
|
||||||
|
}
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListConvosContext.Provider value={ctx}>
|
||||||
|
{children}
|
||||||
|
</ListConvosContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a batch of chat log events to the React Query caches (convo lists,
|
||||||
|
* single-convo, members, requests). Exported as a pure function - separate
|
||||||
|
* from the event-bus subscription in ListConvosProviderInner - so it can be
|
||||||
|
* unit-tested directly without mounting the provider.
|
||||||
|
*/
|
||||||
|
export function handleConvoLogEvents({
|
||||||
|
queryClient,
|
||||||
|
logs,
|
||||||
|
currentConvoId,
|
||||||
|
currentAccountDid,
|
||||||
|
onRefetchNeeded,
|
||||||
|
}: {
|
||||||
|
queryClient: QueryClient
|
||||||
|
logs: Extract<MessagesEventBusEvent, {type: 'logs'}>['logs']
|
||||||
|
currentConvoId: string | undefined
|
||||||
|
currentAccountDid: string | undefined
|
||||||
|
onRefetchNeeded: () => void
|
||||||
|
}): void {
|
||||||
function mutateMembers(
|
function mutateMembers(
|
||||||
convoId: string,
|
convoId: string,
|
||||||
fn: (
|
fn: (
|
||||||
@@ -225,9 +282,7 @@ export function ListConvosProviderInner({
|
|||||||
|
|
||||||
function updateConvoInAllLists(
|
function updateConvoInAllLists(
|
||||||
convoId: string,
|
convoId: string,
|
||||||
fn: (
|
fn: (convo: ChatBskyConvoDefs.ConvoView) => ChatBskyConvoDefs.ConvoView,
|
||||||
convo: ChatBskyConvoDefs.ConvoView,
|
|
||||||
) => ChatBskyConvoDefs.ConvoView,
|
|
||||||
) {
|
) {
|
||||||
queryClient.setQueriesData<ConvoListQueryData>(
|
queryClient.setQueriesData<ConvoListQueryData>(
|
||||||
{queryKey: [RQKEY_ROOT]},
|
{queryKey: [RQKEY_ROOT]},
|
||||||
@@ -241,9 +296,7 @@ export function ListConvosProviderInner({
|
|||||||
|
|
||||||
function mutateConvoView(
|
function mutateConvoView(
|
||||||
convoId: string,
|
convoId: string,
|
||||||
fn: (
|
fn: (convo: ChatBskyConvoDefs.ConvoView) => ChatBskyConvoDefs.ConvoView,
|
||||||
convo: ChatBskyConvoDefs.ConvoView,
|
|
||||||
) => ChatBskyConvoDefs.ConvoView,
|
|
||||||
) {
|
) {
|
||||||
queryClient.setQueryData<ChatBskyConvoDefs.ConvoView>(
|
queryClient.setQueryData<ChatBskyConvoDefs.ConvoView>(
|
||||||
CONVO_KEY(convoId),
|
CONVO_KEY(convoId),
|
||||||
@@ -290,11 +343,7 @@ export function ListConvosProviderInner({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleMemberRemoved(
|
function handleMemberRemoved(convoId: string, did: string, rev: string) {
|
||||||
convoId: string,
|
|
||||||
did: string,
|
|
||||||
rev: string,
|
|
||||||
) {
|
|
||||||
// If the optimistic remove already dropped them from the full
|
// If the optimistic remove already dropped them from the full
|
||||||
// list, skip the memberCount decrement to avoid double-counting.
|
// list, skip the memberCount decrement to avoid double-counting.
|
||||||
const alreadyRemovedMember =
|
const alreadyRemovedMember =
|
||||||
@@ -312,9 +361,9 @@ export function ListConvosProviderInner({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const log of events.logs) {
|
for (const log of logs) {
|
||||||
if (ChatBskyConvoDefs.isLogBeginConvo(log)) {
|
if (ChatBskyConvoDefs.isLogBeginConvo(log)) {
|
||||||
debouncedRefetch()
|
onRefetchNeeded()
|
||||||
} else if (ChatBskyConvoDefs.isLogLeaveConvo(log)) {
|
} else if (ChatBskyConvoDefs.isLogLeaveConvo(log)) {
|
||||||
deleteConvoFromAllLists(log.convoId)
|
deleteConvoFromAllLists(log.convoId)
|
||||||
// The viewer is no longer in this convo (they left on another
|
// The viewer is no longer in this convo (they left on another
|
||||||
@@ -369,7 +418,7 @@ export function ListConvosProviderInner({
|
|||||||
// the remaining logs in this batch still apply - the bus advances
|
// the remaining logs in this batch still apply - the bus advances
|
||||||
// its cursor past this batch, so a dropped log is never
|
// its cursor past this batch, so a dropped log is never
|
||||||
// redelivered.
|
// redelivered.
|
||||||
debouncedRefetch()
|
onRefetchNeeded()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,9 +431,7 @@ export function ListConvosProviderInner({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// add relatedProfiles to members list, but making sure to dedupe
|
// add relatedProfiles to members list, but making sure to dedupe
|
||||||
const relatedProfilesSansMembers = (
|
const relatedProfilesSansMembers = (logRef.relatedProfiles ?? []).filter(
|
||||||
logRef.relatedProfiles ?? []
|
|
||||||
).filter(
|
|
||||||
profile =>
|
profile =>
|
||||||
!foundConvo.members.some(member => member.did === profile.did),
|
!foundConvo.members.some(member => member.did === profile.did),
|
||||||
)
|
)
|
||||||
@@ -399,7 +446,7 @@ export function ListConvosProviderInner({
|
|||||||
foundConvo.id !== currentConvoId
|
foundConvo.id !== currentConvoId
|
||||||
? (ChatBskyConvoDefs.isMessageView(logRef.message) ||
|
? (ChatBskyConvoDefs.isMessageView(logRef.message) ||
|
||||||
ChatBskyConvoDefs.isDeletedMessageView(logRef.message)) &&
|
ChatBskyConvoDefs.isDeletedMessageView(logRef.message)) &&
|
||||||
logRef.message.sender.did !== currentAccount?.did
|
logRef.message.sender.did !== currentAccountDid
|
||||||
? foundConvo.unreadCount + 1
|
? foundConvo.unreadCount + 1
|
||||||
: foundConvo.unreadCount
|
: foundConvo.unreadCount
|
||||||
: 0,
|
: 0,
|
||||||
@@ -418,10 +465,7 @@ export function ListConvosProviderInner({
|
|||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
return {
|
return {
|
||||||
...page,
|
...page,
|
||||||
convos: [
|
convos: [updatedConvo, ...filterConvoFromPage(page.convos)],
|
||||||
updatedConvo,
|
|
||||||
...filterConvoFromPage(page.convos),
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -487,8 +531,7 @@ export function ListConvosProviderInner({
|
|||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
} else if (ChatBskyConvoDefs.isLogAcceptConvo(log)) {
|
} else if (ChatBskyConvoDefs.isLogAcceptConvo(log)) {
|
||||||
const requestQueries =
|
const requestQueries = queryClient.getQueriesData<ConvoListQueryData>({
|
||||||
queryClient.getQueriesData<ConvoListQueryData>({
|
|
||||||
queryKey: RQKEY_PARTIAL('request'),
|
queryKey: RQKEY_PARTIAL('request'),
|
||||||
})
|
})
|
||||||
let foundConvo: ChatBskyConvoDefs.ConvoView | null = null
|
let foundConvo: ChatBskyConvoDefs.ConvoView | null = null
|
||||||
@@ -501,7 +544,7 @@ export function ListConvosProviderInner({
|
|||||||
// Use continue (not return) so the remaining logs in this batch
|
// Use continue (not return) so the remaining logs in this batch
|
||||||
// still apply - the bus advances its cursor past this batch, so a
|
// still apply - the bus advances its cursor past this batch, so a
|
||||||
// dropped log is never redelivered.
|
// dropped log is never redelivered.
|
||||||
debouncedRefetch()
|
onRefetchNeeded()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (log.rev <= foundConvo.rev) {
|
if (log.rev <= foundConvo.rev) {
|
||||||
@@ -545,7 +588,7 @@ export function ListConvosProviderInner({
|
|||||||
},
|
},
|
||||||
(old?: ConvoListQueryData) => {
|
(old?: ConvoListQueryData) => {
|
||||||
if (!old) {
|
if (!old) {
|
||||||
debouncedRefetch()
|
onRefetchNeeded()
|
||||||
return old
|
return old
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -645,11 +688,11 @@ export function ListConvosProviderInner({
|
|||||||
ChatBskyConvoDefs.isLogDisableJoinLink(log)
|
ChatBskyConvoDefs.isLogDisableJoinLink(log)
|
||||||
) {
|
) {
|
||||||
// Join link data not included in the log event, trigger refetch to get it
|
// Join link data not included in the log event, trigger refetch to get it
|
||||||
debouncedRefetch()
|
onRefetchNeeded()
|
||||||
} else if (ChatBskyConvoDefs.isLogEditGroup(log)) {
|
} else if (ChatBskyConvoDefs.isLogEditGroup(log)) {
|
||||||
// Updated group details (name etc.) aren't included in the log
|
// Updated group details (name etc.) aren't included in the log
|
||||||
// event, so refetch to pick them up.
|
// event, so refetch to pick them up.
|
||||||
debouncedRefetch()
|
onRefetchNeeded()
|
||||||
} else if (
|
} else if (
|
||||||
ChatBskyConvoDefs.isLogApproveJoinRequest(log) ||
|
ChatBskyConvoDefs.isLogApproveJoinRequest(log) ||
|
||||||
ChatBskyConvoDefs.isLogRejectJoinRequest(log)
|
ChatBskyConvoDefs.isLogRejectJoinRequest(log)
|
||||||
@@ -693,7 +736,7 @@ export function ListConvosProviderInner({
|
|||||||
} else if (ChatBskyConvoDefs.isLogOutgoingJoinRequest(log)) {
|
} else if (ChatBskyConvoDefs.isLogOutgoingJoinRequest(log)) {
|
||||||
// Viewer isn't in the chat yet, but the inbox surfaces outgoing
|
// Viewer isn't in the chat yet, but the inbox surfaces outgoing
|
||||||
// requests, so refetch to pick up the new entry.
|
// requests, so refetch to pick up the new entry.
|
||||||
debouncedRefetch()
|
onRefetchNeeded()
|
||||||
} else if (ChatBskyConvoDefs.isLogWithdrawIncomingJoinRequest(log)) {
|
} else if (ChatBskyConvoDefs.isLogWithdrawIncomingJoinRequest(log)) {
|
||||||
// A requester rescinded their request to a group the viewer owns.
|
// A requester rescinded their request to a group the viewer owns.
|
||||||
// Mirror of isLogIncomingJoinRequest: decrement the counts.
|
// Mirror of isLogIncomingJoinRequest: decrement the counts.
|
||||||
@@ -715,9 +758,7 @@ export function ListConvosProviderInner({
|
|||||||
log.convoId,
|
log.convoId,
|
||||||
withRevGuard(log.rev, convo => {
|
withRevGuard(log.rev, convo => {
|
||||||
// add relatedProfiles to members list, but making sure to dedupe
|
// add relatedProfiles to members list, but making sure to dedupe
|
||||||
const relatedProfilesSansMembers = (
|
const relatedProfilesSansMembers = (log.relatedProfiles ?? []).filter(
|
||||||
log.relatedProfiles ?? []
|
|
||||||
).filter(
|
|
||||||
profile =>
|
profile =>
|
||||||
!convo.members.some(member => member.did === profile.did),
|
!convo.members.some(member => member.did === profile.did),
|
||||||
)
|
)
|
||||||
@@ -752,7 +793,7 @@ export function ListConvosProviderInner({
|
|||||||
void queryClient.invalidateQueries({
|
void queryClient.invalidateQueries({
|
||||||
queryKey: CONVO_KEY(log.convoId),
|
queryKey: CONVO_KEY(log.convoId),
|
||||||
})
|
})
|
||||||
debouncedRefetch()
|
onRefetchNeeded()
|
||||||
} else if (ChatBskyConvoDefs.isLogRemoveMember(log)) {
|
} else if (ChatBskyConvoDefs.isLogRemoveMember(log)) {
|
||||||
const data = log.message.data
|
const data = log.message.data
|
||||||
if (
|
if (
|
||||||
@@ -767,7 +808,7 @@ export function ListConvosProviderInner({
|
|||||||
void queryClient.invalidateQueries({
|
void queryClient.invalidateQueries({
|
||||||
queryKey: CONVO_KEY(log.convoId),
|
queryKey: CONVO_KEY(log.convoId),
|
||||||
})
|
})
|
||||||
debouncedRefetch()
|
onRefetchNeeded()
|
||||||
} else if (ChatBskyConvoDefs.isLogMemberJoin(log)) {
|
} else if (ChatBskyConvoDefs.isLogMemberJoin(log)) {
|
||||||
const data = log.message.data
|
const data = log.message.data
|
||||||
if (
|
if (
|
||||||
@@ -786,7 +827,7 @@ export function ListConvosProviderInner({
|
|||||||
void queryClient.invalidateQueries({
|
void queryClient.invalidateQueries({
|
||||||
queryKey: CONVO_KEY(log.convoId),
|
queryKey: CONVO_KEY(log.convoId),
|
||||||
})
|
})
|
||||||
debouncedRefetch()
|
onRefetchNeeded()
|
||||||
} else if (ChatBskyConvoDefs.isLogMemberLeave(log)) {
|
} else if (ChatBskyConvoDefs.isLogMemberLeave(log)) {
|
||||||
const data = log.message.data
|
const data = log.message.data
|
||||||
if (
|
if (
|
||||||
@@ -800,7 +841,7 @@ export function ListConvosProviderInner({
|
|||||||
void queryClient.invalidateQueries({
|
void queryClient.invalidateQueries({
|
||||||
queryKey: CONVO_KEY(log.convoId),
|
queryKey: CONVO_KEY(log.convoId),
|
||||||
})
|
})
|
||||||
debouncedRefetch()
|
onRefetchNeeded()
|
||||||
} else if (ChatBskyConvoDefs.isLogRemoveReaction(log)) {
|
} else if (ChatBskyConvoDefs.isLogRemoveReaction(log)) {
|
||||||
queryClient.setQueriesData(
|
queryClient.setQueriesData(
|
||||||
{queryKey: [RQKEY_ROOT]},
|
{queryKey: [RQKEY_ROOT]},
|
||||||
@@ -837,35 +878,6 @@ export function ListConvosProviderInner({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
// get events for all chats
|
|
||||||
convoId: undefined,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return () => unsub()
|
|
||||||
}, [
|
|
||||||
messagesBus,
|
|
||||||
currentConvoId,
|
|
||||||
queryClient,
|
|
||||||
currentAccount?.did,
|
|
||||||
debouncedRefetch,
|
|
||||||
])
|
|
||||||
|
|
||||||
const ctx = useMemo(() => {
|
|
||||||
const convos = data?.pages.flatMap(page => page.convos) ?? []
|
|
||||||
return {
|
|
||||||
accepted: convos.filter(conv => conv.status === 'accepted'),
|
|
||||||
request: convos.filter(conv => conv.status === 'request'),
|
|
||||||
}
|
|
||||||
}, [data])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ListConvosContext.Provider value={ctx}>
|
|
||||||
{children}
|
|
||||||
</ListConvosContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useUnreadMessageCount() {
|
export function useUnreadMessageCount() {
|
||||||
|
|||||||
Reference in New Issue
Block a user