import { CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries"; import { gql } from "@apollo/client"; const logLocal = (message, ...args) => { if (import.meta.env.VITE_APP_IS_TEST || !import.meta.env.PROD) { console.log(`==================== ${message} ====================`); console.dir({ ...args }); } }; // 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" }); 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 }; if (!existingConversation && conversationId) { // Attempt to read from the cache to determine if this is actually a new conversation try { const cachedConversation = client.cache.readFragment({ id: client.cache.identify({ __typename: "conversations", id: conversationId }), fragment: gql` fragment ExistingConversationCheck on conversations { id } ` }); if (cachedConversation) { logLocal("handleNewMessageSummary - Existing Conversation inferred from cache", { conversationId }); return handleNewMessageSummary({ ...message, existingConversation: true }); } } catch { logLocal("handleNewMessageSummary - Cache miss", { conversationId }); } } // 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); 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) { try { 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); } return; } logLocal("New Conversation Summary finished without work", { message }); }; 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 }; default: // Log a warning for unhandled message types logLocal("handleMessageChanged - Unhandled message type", { type: message.type }); return messageRef; } } return messageRef; }); } } }); 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)] : [newConversation]; // Prevent duplicates 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": { // Ensure `job_conversations` is properly formatted const formattedJobConversations = job_conversations.map((jc) => ({ __typename: "job_conversations", jobid: jc.jobid || jc.job?.id, conversationid: conversationId, job: jc.job || { __typename: "jobs", id: data.selectedJob.id, ro_number: data.selectedJob.ro_number, ownr_co_nm: data.selectedJob.ownr_co_nm, ownr_fn: data.selectedJob.ownr_fn, ownr_ln: data.selectedJob.ownr_ln } })); client.cache.modify({ id: client.cache.identify({ __typename: "conversations", id: conversationId }), fields: { job_conversations: (existing = []) => { // Ensure no duplicates based on both `conversationid` and `jobid` const existingLinks = new Set( existing.map((jc) => { const jobId = client.cache.readFragment({ id: client.cache.identify(jc), fragment: gql` fragment JobConversationLinkAdded on job_conversations { jobid conversationid } ` })?.jobid; return `${jobId}:${conversationId}`; // Unique identifier for a job-conversation link }) ); const newItems = formattedJobConversations.filter((jc) => { const uniqueLink = `${jc.jobid}:${jc.conversationid}`; return !existingLinks.has(uniqueLink); }); return [...existing, ...newItems]; } } }); break; } case "tag-removed": try { const conversationCacheId = client.cache.identify({ __typename: "conversations", id: conversationId }); // Evict the specific cache entry for job_conversations client.cache.evict({ id: conversationCacheId, fieldName: "job_conversations" }); // Garbage collect evicted entries client.cache.gc(); logLocal("handleConversationChanged - tag removed - Refetched conversation list after state change", { conversationId, type }); } catch (error) { console.error("Error refetching queries after conversation state change: (Tag Removed)", error); } 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 }); } }; // Existing handler for phone number opt-out const handlePhoneNumberOptedOut = async (data) => { const { bodyshopid, phone_number } = data; logLocal("handlePhoneNumberOptedOut - Start", data); try { client.cache.modify({ id: "ROOT_QUERY", fields: { phone_number_opt_out(existing = [], { readField }) { const phoneNumberExists = existing.some( (ref) => readField("phone_number", ref) === phone_number && readField("bodyshopid", ref) === bodyshopid ); if (phoneNumberExists) { logLocal("handlePhoneNumberOptedOut - Phone number already in cache", { phone_number, bodyshopid }); return existing; } const newOptOut = { __typename: "phone_number_opt_out", id: `temporary-${phone_number}-${Date.now()}`, bodyshopid, phone_number, created_at: new Date().toISOString(), updated_at: new Date().toISOString() }; return [...existing, newOptOut]; } }, broadcast: true }); client.cache.evict({ id: "ROOT_QUERY", fieldName: "phone_number_opt_out", args: { bodyshopid, search: phone_number } }); client.cache.gc(); logLocal("handlePhoneNumberOptedOut - Cache updated successfully", data); } catch (error) { console.error("Error updating cache for phone number opt-out:", error); logLocal("handlePhoneNumberOptedOut - Error", { error: error.message }); } }; // New handler for phone number opt-in const handlePhoneNumberOptedIn = async (data) => { const { bodyshopid, phone_number } = data; logLocal("handlePhoneNumberOptedIn - Start", data); try { // Update the Apollo cache for GET_PHONE_NUMBER_OPT_OUTS by removing the phone number client.cache.modify({ id: "ROOT_QUERY", fields: { phone_number_opt_out(existing = [], { readField }) { // Filter out the phone number from the opt-out list return existing.filter( (ref) => !(readField("phone_number", ref) === phone_number && readField("bodyshopid", ref) === bodyshopid) ); } }, broadcast: true // Trigger UI updates }); // Evict the cache entry to force a refetch on next query client.cache.evict({ id: "ROOT_QUERY", fieldName: "phone_number_opt_out", args: { bodyshopid, search: phone_number } }); client.cache.gc(); logLocal("handlePhoneNumberOptedIn - Cache updated successfully", data); } catch (error) { console.error("Error updating cache for phone number opt-in:", error); logLocal("handlePhoneNumberOptedIn - Error", { error: error.message }); } }; socket.on("new-message-summary", handleNewMessageSummary); socket.on("new-message-detailed", handleNewMessageDetailed); socket.on("message-changed", handleMessageChanged); socket.on("conversation-changed", handleConversationChanged); socket.on("phone-number-opted-out", handlePhoneNumberOptedOut); socket.on("phone-number-opted-in", handlePhoneNumberOptedIn); }; export const unregisterMessagingHandlers = ({ socket }) => { if (!socket) return; socket.off("new-message-summary"); socket.off("new-message-detailed"); socket.off("message-changed"); socket.off("conversation-changed"); socket.off("phone-number-opted-out"); socket.off("phone-number-opted-in"); };