diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index 09b405861..895fa9db1 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -2,39 +2,65 @@ import { CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS } from "../../graphql import { gql } from "@apollo/client"; const logLocal = (message, ...args) => { - if (import.meta.env.PROD) { - return; + if (import.meta.env.VITE_APP_IS_TEST || !import.meta.env.PROD) { + console.log(`==================== ${message} ====================`); + console.dir({ ...args }); } - 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 }; - // 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 + 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 + }); } - }, - __typename: "conversations" - }); + } catch (error) { + logLocal("handleNewMessageSummary - Cache miss", { conversationId }); + } + } // Handle new conversation if (!existingConversation && newConversation?.phone_num) { @@ -49,7 +75,6 @@ export const registerMessagingHandlers = ({ socket, client }) => { 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", @@ -68,81 +93,7 @@ export const registerMessagingHandlers = ({ socket, client }) => { // 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: { @@ -166,7 +117,11 @@ export const registerMessagingHandlers = ({ socket, client }) => { } catch (error) { console.error("Error updating cache for existing conversation:", error); } + + return; } + + logLocal("New Conversation Summary finished without work", { message }); }; const handleNewMessageDetailed = (message) => { diff --git a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx index fe71ee46e..16d4c0bf1 100644 --- a/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx +++ b/client/src/components/chat-conversation-list/chat-conversation-list.component.jsx @@ -1,5 +1,5 @@ import { Badge, Card, List, Space, Tag } from "antd"; -import React from "react"; +import React, { useEffect, useState } from "react"; import { connect } from "react-redux"; import { Virtuoso } from "react-virtuoso"; import { createStructuredSelector } from "reselect"; @@ -20,8 +20,25 @@ const mapDispatchToProps = (dispatch) => ({ }); function ChatConversationListComponent({ conversationList, selectedConversation, setSelectedConversation }) { + // That comma is there for a reason, do not remove it + const [, forceUpdate] = useState(false); + + // Re-render every minute + useEffect(() => { + const interval = setInterval(() => { + forceUpdate((prev) => !prev); // Toggle state to trigger re-render + }, 60000); // 1 minute in milliseconds + + return () => clearInterval(interval); // Cleanup on unmount + }, []); + + // Memoize the sorted conversation list + const sortedConversationList = React.useMemo(() => { + return _.orderBy(conversationList, ["updated_at"], ["desc"]); + }, [conversationList]); + const renderConversation = (index) => { - const item = conversationList[index]; + const item = sortedConversationList[index]; const cardContentRight = {item.updated_at}; const cardContentLeft = item.job_conversations.length > 0 @@ -64,13 +81,10 @@ function ChatConversationListComponent({ conversationList, selectedConversation, ); }; - // CAN DO: Can go back into virtuoso for additional fetch - // endReached={loadMoreConversations} // Calls loadMoreConversations when scrolled to the bottom - return (
renderConversation(index)} style={{ height: "100%", width: "100%" }} /> diff --git a/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx b/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx index 821ad1603..4203e8f5b 100644 --- a/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx +++ b/client/src/components/chat-conversation-title-tags/chat-conversation-title-tags.component.jsx @@ -20,10 +20,10 @@ export function ChatConversationTitleTags({ jobConversations, bodyshop }) { const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG); const { socket } = useContext(SocketContext); - const handleRemoveTag = (jobId) => { + const handleRemoveTag = async (jobId) => { const convId = jobConversations[0].conversationid; if (!!convId) { - removeJobConversation({ + await removeJobConversation({ variables: { conversationId: convId, jobId: jobId @@ -38,17 +38,18 @@ export function ChatConversationTitleTags({ jobConversations, bodyshop }) { } }); } - }).then(() => { - if (socket) { - // Emit the `conversation-modified` event - socket.emit("conversation-modified", { - bodyshopId: bodyshop.id, - conversationId: convId, - type: "tag-removed", - jobId: jobId - }); - } }); + + if (socket) { + // Emit the `conversation-modified` event + socket.emit("conversation-modified", { + bodyshopId: bodyshop.id, + conversationId: convId, + type: "tag-removed", + jobId: jobId + }); + } + logImEXEvent("messaging_remove_job_tag", { conversationId: convId, jobId: jobId diff --git a/client/src/components/chat-popup/chat-popup.component.jsx b/client/src/components/chat-popup/chat-popup.component.jsx index 7c0711fbc..443dfde3e 100644 --- a/client/src/components/chat-popup/chat-popup.component.jsx +++ b/client/src/components/chat-popup/chat-popup.component.jsx @@ -108,7 +108,7 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh refetch()} /> - {pollInterval > 0 && {t("messaging.labels.nopush")}} + {!socket?.connected && {t("messaging.labels.nopush")}} toggleChatVisible()} diff --git a/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx b/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx index 8d3476c7d..88c25bf81 100644 --- a/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx +++ b/client/src/components/chat-tag-ro/chat-tag-ro.container.jsx @@ -28,7 +28,7 @@ export function ChatTagRoContainer({ conversation, bodyshop }) { const executeSearch = (v) => { logImEXEvent("messaging_search_job_tag", { searchTerm: v }); - loadRo(v); + loadRo(v).catch((e) => console.error("Error in ChatTagRoContainer executeSearch:", e)); }; const debouncedExecuteSearch = _.debounce(executeSearch, 500); @@ -41,33 +41,34 @@ export function ChatTagRoContainer({ conversation, bodyshop }) { variables: { conversationId: conversation.id } }); - const handleInsertTag = (value, option) => { + const handleInsertTag = async (value, option) => { logImEXEvent("messaging_add_job_tag"); - insertTag({ + await insertTag({ variables: { jobId: option.key } - }).then(() => { - if (socket) { - // Find the job details from the search data - const selectedJob = data?.search_jobs.find((job) => job.id === option.key); - if (!selectedJob) return; - const newJobConversation = { - __typename: "job_conversations", - jobid: selectedJob.id, - conversationid: conversation.id, - job: { - __typename: "jobs", - ...selectedJob - } - }; - socket.emit("conversation-modified", { - conversationId: conversation.id, - bodyshopId: bodyshop.id, - type: "tag-added", - job_conversations: [newJobConversation] - }); - } }); + + if (socket) { + // Find the job details from the search data + const selectedJob = data?.search_jobs.find((job) => job.id === option.key); + if (!selectedJob) return; + const newJobConversation = { + __typename: "job_conversations", + jobid: selectedJob.id, + conversationid: conversation.id, + job: { + __typename: "jobs", + ...selectedJob + } + }; + socket.emit("conversation-modified", { + conversationId: conversation.id, + bodyshopId: bodyshop.id, + type: "tag-added", + job_conversations: [newJobConversation] + }); + } + setOpen(false); }; diff --git a/package-lock.json b/package-lock.json index 3b2b05a1c..6d1d243cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,7 +55,7 @@ "soap": "^1.1.6", "socket.io": "^4.8.1", "socket.io-adapter": "^2.5.5", - "ssh2-sftp-client": "^10.0.3", + "ssh2-sftp-client": "^11.0.0", "twilio": "^4.23.0", "uuid": "^10.0.0", "winston": "^3.17.0", @@ -66,6 +66,7 @@ "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^4.3.0", "concurrently": "^8.2.2", + "p-limit": "^3.1.0", "prettier": "^3.3.3", "source-map-explorer": "^2.5.2" }, @@ -6707,8 +6708,8 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -7737,16 +7738,17 @@ } }, "node_modules/ssh2-sftp-client": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/ssh2-sftp-client/-/ssh2-sftp-client-10.0.3.tgz", - "integrity": "sha512-Wlhasz/OCgrlqC8IlBZhF19Uw/X/dHI8ug4sFQybPE+0sDztvgvDf7Om6o7LbRLe68E7XkFZf3qMnqAvqn1vkQ==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/ssh2-sftp-client/-/ssh2-sftp-client-11.0.0.tgz", + "integrity": "sha512-lOjgNYtioYquhtgyHwPryFNhllkuENjvCKkUXo18w/Q4UpEffCnEUBfiOTlwFdKIhG1rhrOGnA6DeKPSF2CP6w==", + "license": "Apache-2.0", "dependencies": { "concat-stream": "^2.0.0", "promise-retry": "^2.0.1", "ssh2": "^1.15.0" }, "engines": { - "node": ">=16.20.2" + "node": ">=18.20.4" }, "funding": { "type": "individual", @@ -8691,8 +8693,8 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">=10" },