From 9dbe24657507f35cdf6af74dc35897e4abdc70fd Mon Sep 17 00:00:00 2001 From: Dave Date: Tue, 30 Dec 2025 14:08:33 -0500 Subject: [PATCH] feature/IO-3478-Mark-Conversation-Unread: Finished --- .../registerMessagingSocketHandlers.js | 690 +++++++++--------- .../chat-mark-unread-button.component.jsx | 5 +- .../chat-message-list.styles.scss | 5 + client/src/translations/en_us/common.json | 2 +- client/src/utils/GraphQLClient.js | 20 + server/graphql-client/queries.js | 3 +- server/sms/send.js | 3 +- 7 files changed, 395 insertions(+), 333 deletions(-) diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index accee896b..02eda33e3 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -13,68 +13,227 @@ const logLocal = (message, ...args) => { } }; -// Utility function to enrich conversation data -const enrichConversation = (conversation, isOutbound) => ({ +const normalizeConversationForList = (raw, { isoutbound, isSystem } = {}) => { + const c = raw || {}; + const id = c.id; + + return { + __typename: "conversations", + id, + phone_num: c.phone_num ?? null, + updated_at: c.updated_at ?? safeIsoNow(), + unreadcnt: typeof c.unreadcnt === "number" ? c.unreadcnt : 0, + archived: c.archived ?? false, + label: c.label ?? null, + + job_conversations: Array.isArray(c.job_conversations) + ? c.job_conversations.map((jc) => { + const job = jc?.job || {}; + const jobId = jc?.jobid ?? job?.id ?? null; + + return { + __typename: "job_conversations", + jobid: jobId, + conversationid: jc?.conversationid ?? id ?? null, + job: { + __typename: "jobs", + id: jobId, + ro_number: job?.ro_number ?? null, + ownr_fn: job?.ownr_fn ?? null, + ownr_ln: job?.ownr_ln ?? null, + ownr_co_nm: job?.ownr_co_nm ?? null + } + }; + }) + : [], + + // This is the field your list badge reads (with args). We write it via a fragment with the same args. + messages_aggregate: c.messages_aggregate || { + __typename: "messages_aggregate", + aggregate: { + __typename: "messages_aggregate_fields", + count: isoutbound || isSystem ? 0 : 1 + } + } + }; +}; + +const CONVERSATION_LIST_ITEM_FRAGMENT = gql` + fragment _ConversationListItem on conversations { + id + phone_num + updated_at + unreadcnt + archived + label + job_conversations { + jobid + conversationid + job { + id + ro_number + ownr_fn + ownr_ln + ownr_co_nm + } + } + messages_aggregate(where: { read: { _eq: false }, isoutbound: { _eq: false }, is_system: { _eq: false } }) { + aggregate { + count + } + } + } +`; + +const normalizeMessageForCache = (raw, fallbackConversationId) => { + const m = raw || {}; + + return { + __typename: "messages", + id: m.id, + conversationid: m.conversationid ?? m.conversation?.id ?? fallbackConversationId, + + // Fields your UI queries expect + status: m.status ?? null, + text: m.text ?? "", + is_system: typeof m.is_system === "boolean" ? m.is_system : false, + isoutbound: typeof m.isoutbound === "boolean" ? m.isoutbound : false, + image: typeof m.image === "boolean" ? m.image : false, + image_path: m.image_path ?? null, + userid: m.userid ?? null, + created_at: m.created_at ?? new Date().toISOString(), + read: typeof m.read === "boolean" ? m.read : false + }; +}; + +const isConversationDetailsCached = (client, conversationId) => { + try { + return !!client.cache.readQuery({ + query: GET_CONVERSATION_DETAILS, + variables: { conversationId } + })?.conversations_by_pk; + } catch { + return false; + } +}; + +const isSystemMsid = (msid) => typeof msid === "string" && msid.startsWith("SYS_"); + +const safeIsoNow = () => new Date().toISOString(); + +const conversationDetailsCached = (client, conversationId) => { + try { + const res = client.cache.readQuery({ + query: GET_CONVERSATION_DETAILS, + variables: { conversationId } + }); + return !!res?.conversations_by_pk; + } catch { + return false; + } +}; + +const messageEntityCached = (client, messageId) => { + const cacheId = client.cache.identify({ __typename: "messages", id: messageId }); + if (!cacheId) return false; + + try { + client.cache.readFragment({ + id: cacheId, + fragment: gql` + fragment _MsgExists on messages { + id + } + ` + }); + return true; + } catch { + return false; + } +}; + +// Normalize/enrich conversation data so it matches what CONVERSATION_LIST_QUERY expects +const enrichConversation = (conversation, { isoutbound, isSystem }) => ({ ...conversation, - updated_at: conversation.updated_at || new Date().toISOString(), - unreadcnt: conversation.unreadcnt || 0, - archived: conversation.archived || false, - label: conversation.label || null, + updated_at: conversation.updated_at || safeIsoNow(), + unreadcnt: typeof conversation.unreadcnt === "number" ? 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 + count: isoutbound || isSystem ? 0 : 1 } }, __typename: "conversations" }); -// Can be uncommonted to test the playback of the notification sound -// window.testTone = () => { -// const notificationSound = new Audio(newMessageSound); -// notificationSound.play().catch((error) => { -// console.error("Error playing notification sound:", error); -// }); -// }; +const upsertConversationIntoOffsetZeroList = (client, conversationObj, { isoutbound, isSystem } = {}) => { + const normalized = normalizeConversationForList(conversationObj, { isoutbound, isSystem }); + if (!normalized?.id) return; + + const convCacheId = client.cache.identify(normalized); + if (!convCacheId) return; + + // Seed the entity in the normalized store so we can safely reference it. + client.cache.writeFragment({ + id: convCacheId, + fragment: CONVERSATION_LIST_ITEM_FRAGMENT, + data: normalized + }); + + // Insert/move it to top of the first page list (offset 0) only. + client.cache.modify({ + id: "ROOT_QUERY", + fields: { + conversations(existing = [], { args, readField }) { + if (!args || args.offset !== 0) return existing; + + const archivedEq = args?.where?.archived?._eq; + if (archivedEq === true) return existing; + + const without = existing.filter((c) => readField("id", c) !== normalized.id); + return [{ __ref: convCacheId }, ...without]; + } + } + }); +}; export const registerMessagingHandlers = ({ socket, client, currentUser, bodyshop }) => { if (!(socket && client)) return; const handleNewMessageSummary = async (message) => { - const { conversationId, newConversation, existingConversation, isoutbound } = message; + const { conversationId, newConversation, existingConversation, isoutbound, msid, updated_at } = message; - // True only when DB value is strictly true; falls back to true on cache miss - const isNewMessageSoundEnabled = (client) => { + const isSystem = isSystemMsid(msid); + + const isNewMessageSoundEnabled = (clientInstance) => { try { const email = currentUser?.email; - if (!email) return true; // default allow if we can't resolve user - const res = client.readQuery({ + if (!email) return true; + const res = clientInstance.readQuery({ query: QUERY_ACTIVE_ASSOCIATION_SOUND, variables: { email } }); const flag = res?.associations?.[0]?.new_message_sound; - return flag === true; // strictly true => enabled + return flag === true; } catch { - // If the query hasn't been seeded in cache yet, default ON return true; } }; logLocal("handleNewMessageSummary - Start", { message, isNew: !existingConversation }); - const queryVariables = { offset: 0 }; - if (!isoutbound) { - // Play notification sound for new inbound message (scoped to bodyshop) if (isLeaderTab(bodyshop.id) && isNewMessageSoundEnabled(client)) { playNewMessageSound(bodyshop.id); } } + // If we think it's "new", sanity-check the cache: if conversation exists, treat as existing. 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 }), @@ -86,71 +245,57 @@ export const registerMessagingHandlers = ({ socket, client, currentUser, bodysho }); if (cachedConversation) { - logLocal("handleNewMessageSummary - Existing Conversation inferred from cache", { - conversationId - }); - return handleNewMessageSummary({ - ...message, - existingConversation: true - }); + logLocal("handleNewMessageSummary - Existing Conversation inferred from cache", { conversationId }); + return handleNewMessageSummary({ ...message, existingConversation: true }); } } catch { - logLocal("handleNewMessageSummary - Cache miss", { conversationId }); + // Cache miss is normal } } - // Handle new conversation + // New conversation: upsert into offset-0 conversation list cache 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]; - } - } - }); - } + upsertConversationIntoOffsetZeroList(client, newConversation, { isoutbound, isSystem }); } catch (error) { console.error("Error updating cache for new conversation:", error); } return; } - // Handle existing conversation - if (existingConversation) { + // Existing conversation: update updated_at and unread badge (only for inbound non-system) + if (existingConversation && conversationId) { try { client.cache.modify({ id: client.cache.identify({ __typename: "conversations", id: conversationId }), fields: { - updated_at: () => new Date().toISOString(), + updated_at: () => updated_at || safeIsoNow(), 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; + + // Badge in your list uses messages_aggregate.aggregate.count with is_system excluded + messages_aggregate(cached = null) { + if (isoutbound || isSystem) return cached; + + const currentCount = cached?.aggregate?.count ?? 0; + return { + __typename: "messages_aggregate", + aggregate: { + __typename: "messages_aggregate_fields", + count: currentCount + 1 + } + }; + }, + + // Keep legacy unreadcnt reasonably in sync (if you still display it anywhere) + unreadcnt(cached) { + if (isoutbound || isSystem) return cached; + const n = typeof cached === "number" ? cached : 0; + return n + 1; } } }); + + // Optional: bubble to top of offset-0 list by rewriting entity is enough } catch (error) { console.error("Error updating cache for existing conversation:", error); } @@ -166,88 +311,82 @@ export const registerMessagingHandlers = ({ socket, client, currentUser, bodysho 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 the conversation thread isn't open/cached, don't try to append messages + if (!conversationId || !isConversationDetailsCached(client, conversationId)) return; - if (!queryResults?.conversations_by_pk) { - console.warn("Conversation not found in cache:", { conversationId }); + try { + const normalized = normalizeMessageForCache(newMessage, conversationId); + + const messageCacheId = client.cache.identify(normalized); + if (!messageCacheId) { + console.warn("handleNewMessageDetailed - Could not identify message for cache", { + conversationId, + newMessageId: newMessage?.id + }); return; } - // Append the new message to the conversation's message list using cache.modify + // Write the entity (no missing-field warnings because normalized includes is_system) + client.cache.writeFragment({ + id: messageCacheId, + fragment: gql` + fragment _IncomingMessage on messages { + id + conversationid + status + text + is_system + isoutbound + image + image_path + userid + created_at + read + } + `, + data: normalized + }); + + // Append a ref (not a raw object) and avoid duplicates client.cache.modify({ id: client.cache.identify({ __typename: "conversations", id: conversationId }), fields: { - messages(existingMessages = []) { - return [...existingMessages, newMessage]; + messages(existing = [], { readField }) { + const already = existing.some((ref) => readField("id", ref) === normalized.id); + if (already) return existing; + return [...existing, { __ref: messageCacheId }]; + }, + updated_at() { + return normalized.created_at || new Date().toISOString(); } } }); - logLocal("handleNewMessageDetailed - Message appended successfully", { - conversationId, - newMessage - }); + logLocal("handleNewMessageDetailed - Message appended successfully", { conversationId }); } catch (error) { console.error("Error updating conversation messages in cache:", error); } }; const handleMessageChanged = (message) => { - if (!message) { - logLocal("handleMessageChanged - No message provided", message); - return; - } + if (!message?.id) return; logLocal("handleMessageChanged - Start", message); + // Only update if the message entity exists locally + if (!messageEntityCached(client, message.id)) return; + try { + const msgCacheId = client.cache.identify({ __typename: "messages", id: message.id }); + client.cache.modify({ - id: client.cache.identify({ __typename: "conversations", id: message.conversationid }), + id: msgCacheId, 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; - }); + status(existing) { + return message.type === "status-changed" ? (message.status ?? existing) : existing; + }, + text(existing) { + return message.type === "text-updated" ? (message.text ?? existing) : existing; } } }); @@ -262,110 +401,78 @@ export const registerMessagingHandlers = ({ socket, client, currentUser, bodysho }; const handleConversationChanged = async (data) => { - if (!data) { - logLocal("handleConversationChanged - No data provided", data); - return; - } + if (!data?.conversationId) return; const { conversationId, type, job_conversations, - messageIds, // used by "conversation-marked-read" - messageIdsMarkedRead, // used by "conversation-marked-unread" - lastUnreadMessageId, // used by "conversation-marked-unread" - unreadCount, // used by "conversation-marked-unread" + messageIds, + messageIdsMarkedRead, + lastUnreadMessageId, + unreadCount, ...fields } = data; logLocal("handleConversationChanged - Start", data); - const updatedAt = new Date().toISOString(); + const updatedAt = safeIsoNow(); + const detailsCached = conversationDetailsCached(client, conversationId); - 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; - }); - }, - // Keep unread badge in sync (badge uses messages_aggregate.aggregate.count) - messages_aggregate: () => ({ - __typename: "messages_aggregate", - aggregate: { __typename: "messages_aggregate_fields", count: 0 } - }), - unreadcnt: () => 0 - } + case "conversation-marked-read": { + // Update message entities only if details are cached, otherwise just update counters. + if (detailsCached && Array.isArray(messageIds)) { + messageIds.forEach((id) => { + if (!messageEntityCached(client, id)) return; + client.cache.modify({ + id: client.cache.identify({ __typename: "messages", id }), + fields: { read: () => true } + }); }); } - break; - - case "conversation-marked-unread": { - if (!conversationId) break; - - const safeUnreadCount = typeof unreadCount === "number" ? unreadCount : 1; - const idsMarkedRead = Array.isArray(messageIdsMarkedRead) ? messageIdsMarkedRead : []; client.cache.modify({ id: client.cache.identify({ __typename: "conversations", id: conversationId }), fields: { - // Bubble the conversation up in the list (since UI sorts by updated_at) + messages_aggregate: () => ({ + __typename: "messages_aggregate", + aggregate: { __typename: "messages_aggregate_fields", count: 0 } + }), + unreadcnt: () => 0, + updated_at: () => updatedAt + } + }); + + break; + } + + case "conversation-marked-unread": { + const safeUnreadCount = typeof unreadCount === "number" ? unreadCount : 1; + const idsMarkedRead = Array.isArray(messageIdsMarkedRead) ? messageIdsMarkedRead : []; + + if (detailsCached) { + idsMarkedRead.forEach((id) => { + if (!messageEntityCached(client, id)) return; + client.cache.modify({ + id: client.cache.identify({ __typename: "messages", id }), + fields: { read: () => true } + }); + }); + + if (lastUnreadMessageId && messageEntityCached(client, lastUnreadMessageId)) { + client.cache.modify({ + id: client.cache.identify({ __typename: "messages", id: lastUnreadMessageId }), + fields: { read: () => false } + }); + } + } + + client.cache.modify({ + id: client.cache.identify({ __typename: "conversations", id: conversationId }), + fields: { updated_at: () => updatedAt, - - // If details are already cached, flip the read flags appropriately - messages(existingMessages = [], { readField }) { - if (!Array.isArray(existingMessages) || existingMessages.length === 0) return existingMessages; - - return existingMessages.map((msg) => { - const id = readField("id", msg); - - if (lastUnreadMessageId && id === lastUnreadMessageId) { - return { ...msg, read: false }; - } - - if (idsMarkedRead.includes(id)) { - return { ...msg, read: true }; - } - - return msg; - }); - }, - - // Update unread badge messages_aggregate: () => ({ __typename: "messages_aggregate", aggregate: { @@ -373,8 +480,6 @@ export const registerMessagingHandlers = ({ socket, client, currentUser, bodysho count: safeUnreadCount } }), - - // Optional: keep legacy/parallel unread field consistent if present unreadcnt: () => safeUnreadCount } }); @@ -382,89 +487,46 @@ export const registerMessagingHandlers = ({ socket, client, currentUser, bodysho break; } - case "conversation-created": - updateConversationList({ ...fields, job_conversations, updated_at: updatedAt }); + case "conversation-created": { + const conv = enrichConversation( + { id: conversationId, job_conversations, ...fields, updated_at: updatedAt }, + { isoutbound: false, isSystem: false } + ); + upsertConversationIntoOffsetZeroList(client, conv); break; + } case "conversation-unarchived": - case "conversation-archived": - 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); - } + case "conversation-archived": { + // Correct refetch usage: this refetches any ACTIVE watchers for these documents. + await client.refetchQueries({ + include: [CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS] + }); break; + } case "tag-added": { - // Ensure `job_conversations` is properly formatted - const formattedJobConversations = job_conversations.map((jc) => ({ + const formattedJobConversations = (job_conversations || []).map((jc) => ({ __typename: "job_conversations", jobid: jc.jobid || jc.job?.id, conversationid: conversationId, - job: jc.job || { + 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 + id: jc.job?.id, + ro_number: jc.job?.ro_number, + ownr_co_nm: jc.job?.ownr_co_nm, + ownr_fn: jc.job?.ownr_fn, + ownr_ln: jc.job?.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]; + job_conversations(existing = [], { readField }) { + const seen = new Set(existing.map((x) => readField("jobid", x)).filter(Boolean)); + const incoming = formattedJobConversations.filter((x) => x.jobid && !seen.has(x.jobid)); + return [...existing, ...incoming]; } } }); @@ -472,46 +534,32 @@ export const registerMessagingHandlers = ({ socket, client, currentUser, bodysho 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); - } + case "tag-removed": { + const conversationCacheId = client.cache.identify({ __typename: "conversations", id: conversationId }); + client.cache.evict({ + id: conversationCacheId, + fieldName: "job_conversations" + }); + client.cache.gc(); break; + } - default: - logLocal("handleConversationChanged - Unhandled type", { type }); + default: { + // Safe partial updates to the conversation entity 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)]) - ) - } + 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); @@ -521,22 +569,18 @@ export const registerMessagingHandlers = ({ socket, client, currentUser, bodysho id: "ROOT_QUERY", fields: { phone_number_opt_out(existing = [], { readField }) { - const phoneNumberExists = existing.some( + const exists = 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; - } + if (exists) 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() + created_at: safeIsoNow(), + updated_at: safeIsoNow() }; return [...existing, newOptOut]; @@ -551,46 +595,36 @@ export const registerMessagingHandlers = ({ socket, client, currentUser, bodysho 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 + broadcast: true }); - // 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 }); } }; diff --git a/client/src/components/chat-mark-unread-button/chat-mark-unread-button.component.jsx b/client/src/components/chat-mark-unread-button/chat-mark-unread-button.component.jsx index 47c14b9cf..b8e70a43a 100644 --- a/client/src/components/chat-mark-unread-button/chat-mark-unread-button.component.jsx +++ b/client/src/components/chat-mark-unread-button/chat-mark-unread-button.component.jsx @@ -1,4 +1,3 @@ -import { MailOutlined } from "@ant-design/icons"; import { Button, Tooltip } from "antd"; import { useTranslation } from "react-i18next"; @@ -9,7 +8,9 @@ export default function ChatMarkUnreadButton({ disabled, loading, onMarkUnread }