import { CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries"; import { gql } from "@apollo/client"; const logLocal = (message, ...args) => { if (import.meta.env.PROD) { return; } console.log(`==================== ${message} ====================`); console.dir({ ...args }); }; export const registerMessagingHandlers = ({ socket, client }) => { if (!(socket && client)) return; const handleNewMessageSummary = async (message) => { const { conversationId, newConversation, existingConversation, isoutbound } = message; logLocal("handleNewMessageSummary - Start", { message, isNew: !existingConversation }); const queryVariables = { offset: 0 }; // Utility function to enrich conversation data const enrichConversation = (conversation, isOutbound) => ({ ...conversation, updated_at: conversation.updated_at || new Date().toISOString(), unreadcnt: conversation.unreadcnt || 0, archived: conversation.archived || false, label: conversation.label || null, job_conversations: conversation.job_conversations || [], messages_aggregate: conversation.messages_aggregate || { __typename: "messages_aggregate", aggregate: { __typename: "messages_aggregate_fields", count: isOutbound ? 0 : 1 } }, __typename: "conversations" }); // Handle new conversation if (!existingConversation && newConversation?.phone_num) { logLocal("handleNewMessageSummary - New Conversation", newConversation); try { const queryResults = client.cache.readQuery({ query: CONVERSATION_LIST_QUERY, variables: queryVariables }); const existingConversations = queryResults?.conversations || []; const enrichedConversation = enrichConversation(newConversation, isoutbound); // Avoid adding duplicate conversations if (!existingConversations.some((conv) => conv.id === enrichedConversation.id)) { client.cache.modify({ id: "ROOT_QUERY", fields: { conversations(existingConversations = []) { return [enrichedConversation, ...existingConversations]; } } }); } } catch (error) { console.error("Error updating cache for new conversation:", error); } return; } // Handle existing conversation if (existingConversation) { let conversationDetails; // Attempt to read existing conversation details from cache try { conversationDetails = client.cache.readFragment({ id: client.cache.identify({ __typename: "conversations", id: conversationId }), fragment: gql` fragment ExistingConversation on conversations { id phone_num updated_at archived label unreadcnt job_conversations { jobid conversationid } messages_aggregate { aggregate { count } } __typename } ` }); } catch (error) { logLocal("handleNewMessageSummary - Cache miss for conversation, fetching from server", { conversationId }); } // Fetch conversation details from server if not in cache if (!conversationDetails) { try { const { data } = await client.query({ query: GET_CONVERSATION_DETAILS, variables: { conversationId }, fetchPolicy: "network-only" }); conversationDetails = data?.conversations_by_pk; } catch (error) { console.error("Failed to fetch conversation details from server:", error); return; } } // Validate that conversation details were retrieved if (!conversationDetails) { console.error("Unable to retrieve conversation details. Skipping cache update."); return; } try { // Check if the conversation is already in the cache const queryResults = client.cache.readQuery({ query: CONVERSATION_LIST_QUERY, variables: queryVariables }); const isAlreadyInCache = queryResults?.conversations.some((conv) => conv.id === conversationId); if (!isAlreadyInCache) { const enrichedConversation = enrichConversation(conversationDetails, isoutbound); client.cache.modify({ id: "ROOT_QUERY", fields: { conversations(existingConversations = []) { return [enrichedConversation, ...existingConversations]; } } }); } // Update fields for the existing conversation in the cache client.cache.modify({ id: client.cache.identify({ __typename: "conversations", id: conversationId }), fields: { updated_at: () => new Date().toISOString(), archived: () => false, messages_aggregate(cached = { aggregate: { count: 0 } }) { const currentCount = cached.aggregate?.count || 0; if (!isoutbound) { return { __typename: "messages_aggregate", aggregate: { __typename: "messages_aggregate_fields", count: currentCount + 1 } }; } return cached; } } }); } catch (error) { console.error("Error updating cache for existing conversation:", error); } } }; const handleNewMessageDetailed = (message) => { const { conversationId, newMessage } = message; logLocal("handleNewMessageDetailed - Start", message); try { // Check if the conversation exists in the cache const queryResults = client.cache.readQuery({ query: GET_CONVERSATION_DETAILS, variables: { conversationId } }); if (!queryResults?.conversations_by_pk) { console.warn("Conversation not found in cache:", { conversationId }); return; } // Append the new message to the conversation's message list using cache.modify client.cache.modify({ id: client.cache.identify({ __typename: "conversations", id: conversationId }), fields: { messages(existingMessages = []) { return [...existingMessages, newMessage]; } } }); logLocal("handleNewMessageDetailed - Message appended successfully", { conversationId, newMessage }); } catch (error) { console.error("Error updating conversation messages in cache:", error); } }; const handleMessageChanged = (message) => { if (!message) { logLocal("handleMessageChanged - No message provided", message); return; } logLocal("handleMessageChanged - Start", message); try { client.cache.modify({ id: client.cache.identify({ __typename: "conversations", id: message.conversationid }), fields: { messages(existingMessages = [], { readField }) { return existingMessages.map((messageRef) => { // Check if this is the message to update if (readField("id", messageRef) === message.id) { const currentStatus = readField("status", messageRef); // Handle known types of message changes switch (message.type) { case "status-changed": // Prevent overwriting if the current status is already "delivered" if (currentStatus === "delivered") { logLocal("handleMessageChanged - Status already delivered, skipping update", { messageId: message.id }); return messageRef; } // Update the status field return { ...messageRef, status: message.status }; case "text-updated": // Handle changes to the message text return { ...messageRef, text: message.text }; // Add cases for other known message types as needed default: // Log a warning for unhandled message types logLocal("handleMessageChanged - Unhandled message type", { type: message.type }); return messageRef; } } return messageRef; // Keep other messages unchanged }); } } }); logLocal("handleMessageChanged - Message updated successfully", { messageId: message.id, type: message.type }); } catch (error) { console.error("handleMessageChanged - Error modifying cache:", error); } }; const handleConversationChanged = async (data) => { if (!data) { logLocal("handleConversationChanged - No data provided", data); return; } const { conversationId, type, job_conversations, messageIds, ...fields } = data; logLocal("handleConversationChanged - Start", data); const updatedAt = new Date().toISOString(); const updateConversationList = (newConversation) => { try { const existingList = client.cache.readQuery({ query: CONVERSATION_LIST_QUERY, variables: { offset: 0 } }); const updatedList = existingList?.conversations ? [ newConversation, ...existingList.conversations.filter((conv) => conv.id !== newConversation.id) // Prevent duplicates ] : [newConversation]; client.cache.writeQuery({ query: CONVERSATION_LIST_QUERY, variables: { offset: 0 }, data: { conversations: updatedList } }); logLocal("handleConversationChanged - Conversation list updated successfully", newConversation); } catch (error) { console.error("Error updating conversation list in the cache:", error); } }; // Handle specific types try { switch (type) { case "conversation-marked-read": if (conversationId && messageIds?.length > 0) { client.cache.modify({ id: client.cache.identify({ __typename: "conversations", id: conversationId }), fields: { messages(existingMessages = [], { readField }) { return existingMessages.map((message) => { if (messageIds.includes(readField("id", message))) { return { ...message, read: true }; } return message; }); }, messages_aggregate: () => ({ __typename: "messages_aggregate", aggregate: { __typename: "messages_aggregate_fields", count: 0 } }) } }); } break; case "conversation-created": updateConversationList({ ...fields, job_conversations, updated_at: updatedAt }); break; case "conversation-unarchived": case "conversation-archived": // Would like to someday figure out how to get this working without refetch queries, // But I have but a solid 4 hours into it, and there are just too many weird occurrences try { const listQueryVariables = { offset: 0 }; const detailsQueryVariables = { conversationId }; // Check if conversation details exist in the cache const detailsExist = !!client.cache.readQuery({ query: GET_CONVERSATION_DETAILS, variables: detailsQueryVariables }); // Refetch conversation list await client.refetchQueries({ include: [CONVERSATION_LIST_QUERY, ...(detailsExist ? [GET_CONVERSATION_DETAILS] : [])], variables: [ { query: CONVERSATION_LIST_QUERY, variables: listQueryVariables }, ...(detailsExist ? [ { query: GET_CONVERSATION_DETAILS, variables: detailsQueryVariables } ] : []) ] }); logLocal("handleConversationChanged - Refetched queries after state change", { conversationId, type }); } catch (error) { console.error("Error refetching queries after conversation state change:", error); } break; case "tag-added": client.cache.modify({ id: client.cache.identify({ __typename: "conversations", id: conversationId }), fields: { job_conversations: (existing = []) => [...existing, ...job_conversations] } }); break; case "tag-removed": client.cache.modify({ id: client.cache.identify({ __typename: "conversations", id: conversationId }), fields: { job_conversations: (existing = [], { readField }) => existing.filter((jobRef) => readField("jobid", jobRef) !== fields.jobId) } }); break; default: logLocal("handleConversationChanged - Unhandled type", { type }); client.cache.modify({ id: client.cache.identify({ __typename: "conversations", id: conversationId }), fields: { ...Object.fromEntries( Object.entries(fields).map(([key, value]) => [key, (cached) => (value !== undefined ? value : cached)]) ) } }); } } catch (error) { console.error("Error handling conversation changes:", { type, error }); } }; socket.on("new-message-summary", handleNewMessageSummary); socket.on("new-message-detailed", handleNewMessageDetailed); socket.on("message-changed", handleMessageChanged); socket.on("conversation-changed", handleConversationChanged); }; export const unregisterMessagingHandlers = ({ socket }) => { if (!socket) return; socket.off("new-message"); socket.off("new-message-summary"); socket.off("new-message-detailed"); socket.off("message-changed"); socket.off("conversation-changed"); };