import { InfiniteData, useQueryClient } from '@tanstack/react-query' import { ApiCollection, MessageResource } from '../types' import { JoltReactionEvent, JoltTypingEvent } from '../types/jolt_events' import { MessageCreatedEvent, MessageUpdatedEvent, MessageDeletedEvent, } from '../types/jolt_events/message_events' import { deleteRecordInPagesData } from '../utils' import { updateCacheWithMessage, updateCacheWithIndividualMessage, updateCacheWithReaction, updateCacheReplyRoots, getThreadedMessagesQueryKey, } from '../utils/cache/messages_cache' import { isBehindLiveEdge, MessagesWindow } from '../utils/cache/messages_window' import { transformMessageEventDataToMessageResource } from '../utils/jolt/transform_message_event_data_to_message_resource' import { completeMessageCreationTracking } from '../utils/performance_tracking' import { getMessageQueryKey } from '../utils/request/get_message' import { getMessagesRequestArgs } from '../utils/request/get_messages' import { useApiClient } from './use_api_client' import { useCurrentPerson } from './use_current_person' import { useJoltChannel, useJoltEvent } from './use_jolt' import { getRequestQueryKey } from './use_suspense_api' import { TYPING_TIMEOUT_INTERVAL, useTypingStatusCache } from './use_typing_status_cache' interface Props { conversationId: number } export function useConversationMessagesJoltEvents({ conversationId }: Props) { const queryClient = useQueryClient() const currentPerson = useCurrentPerson() const joltChannel = useJoltChannel(`chat.conversations.${conversationId}`) const messagesRequestArgs = getMessagesRequestArgs({ conversation_id: conversationId }) const messagesQueryKey = getRequestQueryKey(messagesRequestArgs) const apiClient = useApiClient() const { addTypingEvent, removeTypingEventById, removeAllTypingEventsByAuthorId } = useTypingStatusCache(conversationId) // While behind, a new message would land after a gap; the newer fetch brings it in. const acceptsCreates = (queryKey: unknown[]) => !isBehindLiveEdge(queryClient.getQueryData(queryKey)) const handleMessageUpdateOrCreate = async (e: MessageCreatedEvent | MessageUpdatedEvent) => { const { data } = e.data const message = transformMessageEventDataToMessageResource({ data, currentPersonId: currentPerson.id, }) const isCreate = e.event === 'message.created' const isReplyRootUpdate = e.event === 'message.updated' && data.reply_root_id === data.sort_key if (e.event === 'message.created' && data.author_id) { removeAllTypingEventsByAuthorId(data.author_id) if (data.idempotent_key && data.author_id === currentPerson.id) { completeMessageCreationTracking({ apiClient, idempotentKey: data.idempotent_key }) } } // Update the main conversation cache if (!isCreate || acceptsCreates(messagesQueryKey)) { updateCacheWithMessage(queryClient, messagesQueryKey, message, e.event) } // If message has a reply_root_id, also update the threaded cache if (data.reply_root_id) { const threadedMessagesQueryKey = getThreadedMessagesQueryKey( conversationId, data.reply_root_id ) if (!isCreate || acceptsCreates(threadedMessagesQueryKey)) { updateCacheWithMessage(queryClient, threadedMessagesQueryKey, message, e.event) } if (isReplyRootUpdate) { updateCacheReplyRoots(queryClient, messagesQueryKey, message) updateCacheReplyRoots(queryClient, threadedMessagesQueryKey, message) } } // Update individual message query cache so reply shadow messages update immediately if (e.event === 'message.updated') { const individualMessageQueryKey = getMessageQueryKey({ conversation_id: conversationId, messageId: data.sort_key, }) updateCacheWithIndividualMessage(queryClient, individualMessageQueryKey, message) } } const handleMessageDeleted = async (e: MessageDeletedEvent) => { const { data } = e.data const message = transformMessageEventDataToMessageResource({ data, currentPersonId: currentPerson.id, }) // Update the main conversation cache queryClient.setQueryData(messagesQueryKey, prev => deleteRecordInPagesData({ data: prev, record: message }) ) // If message has a reply_root_id, also update the threaded cache if (data.reply_root_id) { const threadedMessagesQueryKey = getThreadedMessagesQueryKey( conversationId, data.reply_root_id ) queryClient.setQueryData(threadedMessagesQueryKey, prev => deleteRecordInPagesData({ data: prev, record: message }) ) } } const handleReactionJoltEvent = async (e: JoltReactionEvent) => { // Update the main conversation cache and capture the reply_root_id if present updateCacheWithReaction(queryClient, messagesQueryKey, e, currentPerson.id) const replyRootId = e.data.data.reply_root_id // If the message has a reply_root_id, also update the threaded cache if (replyRootId) { const threadedMessagesQueryKey = getThreadedMessagesQueryKey(conversationId, replyRootId) updateCacheWithReaction(queryClient, threadedMessagesQueryKey, e, currentPerson.id) } } const handleTypingEvent = async (e: JoltTypingEvent) => { const { data } = e.data addTypingEvent(data) setTimeout(() => removeTypingEventById(data.id), TYPING_TIMEOUT_INTERVAL) } useJoltEvent(joltChannel, 'message.created', handleMessageUpdateOrCreate) useJoltEvent(joltChannel, 'message.updated', handleMessageUpdateOrCreate) useJoltEvent(joltChannel, 'message.destroyed', handleMessageDeleted) useJoltEvent(joltChannel, 'reaction.*', handleReactionJoltEvent) useJoltEvent(joltChannel, 'typing.broadcast', handleTypingEvent) } type QueryData = InfiniteData>