import { InfiniteData, QueryClient } from '@tanstack/react-query' import { getRequestQueryKey } from '../../hooks/use_suspense_api' import { ApiCollection, ApiResource, MessageResource } from '../../types' import { JoltReactionEvent } from '../../types/jolt_events' import { transformReactionEventDataToReactionCountResource } from '../jolt/transform_reaction_event_data_to_reaction_count_resource' import { getMessageQueryKey } from '../request/get_message' import { getMessagesQueryKey, getMessagesRequestArgs } from '../request/get_messages' import { updateAllRecordsInPagesData, updateOrCreateRecordInPagesData, updateRecordInPagesData, } from './page_mutations' import { deleteRecordInPagesData } from './page_mutations' export function updateCacheWithMessage( queryClient: QueryClient, queryKey: unknown[], message: MessageResource, event: 'message.created' | 'message.updated' ) { queryClient.setQueryData(queryKey, prev => { if (event === 'message.created') { // Before adding the new message, remove its pending temporary counterpart. // The Jolt echo can arrive before the HTTP response has swapped it out. let dataAfterTempRemoval = prev if (prev && message.mine && message.idempotentKey) { dataAfterTempRemoval = deleteRecordInPagesData({ data: prev, record: message, matchFn: existingMessage => isTemporaryMessageId(existingMessage.id) && existingMessage.idempotentKey === message.idempotentKey, }) } return updateOrCreateRecordInPagesData({ data: dataAfterTempRemoval, record: message, processRecord: (record, current) => mergeMessageUpdate(record, current), }) } else { return updateRecordInPagesData({ data: prev, record: message, processRecord: (record, current) => mergeMessageUpdate(record, current), }) } }) } type IndividualMessageQueryData = ApiResource export function updateCacheWithIndividualMessage( queryClient: QueryClient, queryKey: unknown[], message: MessageResource ) { queryClient.setQueryData(queryKey, prev => { if (!prev) { return { data: message, links: {}, meta: {}, } } return { ...prev, data: mergeMessageUpdate(message, prev.data), } }) } export function updateCacheReplyRoots( queryClient: QueryClient, queryKey: unknown[], root: MessageResource ) { queryClient.setQueryData(queryKey, prev => updateAllRecordsInPagesData({ data: prev, processRecord: message => message.replyRoot?.id === root.id ? { ...message, replyRoot: mergeMessageUpdate(root, message.replyRoot) } : message, }) ) } export function updateCacheWithReaction( queryClient: QueryClient, queryKey: unknown[], event: JoltReactionEvent, currentPersonId: number ) { const message = { id: event.data.data.message_sort_key } as MessageResource queryClient.setQueryData(queryKey, prev => updateRecordInPagesData({ data: prev, record: message, processRecord: (record, oldMessage) => { const reactionCounts = oldMessage.reactionCounts || [] let foundMatch = false let newReactionCounts = reactionCounts.map(reactionCount => { if (reactionCount.value === event.data.data.value) { foundMatch = true return transformReactionEventDataToReactionCountResource({ data: event.data.data, oldData: reactionCount, event: event.event, currentPersonId, }) } return reactionCount }) if (!foundMatch) { const newReactionCount = transformReactionEventDataToReactionCountResource({ data: event.data.data, event: event.event, currentPersonId, }) if (newReactionCount?.count) { newReactionCounts = [...newReactionCounts, newReactionCount] } } return { ...oldMessage, reactionCounts: newReactionCounts } }, }) ) } type MessagesQueryData = InfiniteData> export function isTemporaryMessageId(messageId?: string | null): boolean { return !!messageId && messageId.endsWith('-temp') } export function isNewMessage(message?: MessageResource): boolean { return !message?.id || isTemporaryMessageId(message.id) } export function getThreadedMessagesQueryKey(conversationId: number, replyRootId: string) { const requestArgs = getMessagesRequestArgs({ conversation_id: conversationId, reply_root_id: replyRootId, }) return getRequestQueryKey(requestArgs) } export function applyMessageDeletionToCaches( queryClient: QueryClient, conversationId: number, message: MessageResource ) { const messagesQueryKey = getMessagesQueryKey({ conversation_id: conversationId }) const isReplyRoot = message.replyRootId === message.id if (isReplyRoot) { const deletedRoot: MessageResource = { ...message, deletedAt: message.deletedAt ?? new Date().toISOString(), attachments: [], } const threadedMessagesQueryKey = getThreadedMessagesQueryKey(conversationId, message.id) const individualMessageQueryKey = getMessageQueryKey({ conversation_id: conversationId, messageId: message.id, }) updateCacheWithMessage(queryClient, messagesQueryKey, deletedRoot, 'message.updated') updateCacheWithMessage(queryClient, threadedMessagesQueryKey, deletedRoot, 'message.updated') updateCacheReplyRoots(queryClient, messagesQueryKey, deletedRoot) updateCacheReplyRoots(queryClient, threadedMessagesQueryKey, deletedRoot) updateCacheWithIndividualMessage(queryClient, individualMessageQueryKey, deletedRoot) return } queryClient.setQueryData(messagesQueryKey, prev => deleteRecordInPagesData({ data: prev, record: message }) ) if (message.replyRootId) { const threadedMessagesQueryKey = getThreadedMessagesQueryKey( conversationId, message.replyRootId ) queryClient.setQueryData(threadedMessagesQueryKey, prev => deleteRecordInPagesData({ data: prev, record: message }) ) } } export function mergeMessageUpdate( record: MessageResource, current?: MessageResource ): MessageResource { if (!current) { return record } return { ...current, ...record, // Preserve reactions from cache - they're managed via separate reaction events reactionCounts: current.reactionCounts || [], } }