From 06afd6da5bdff46aa63bad3307e5ea501164c7a3 Mon Sep 17 00:00:00 2001 From: Dave Richer Date: Wed, 20 Nov 2024 18:22:27 -0800 Subject: [PATCH] feature/IO-3000-messaging-sockets-migrations2 - - Conversation Labels Synced - Job Tagging Synced Signed-off-by: Dave Richer --- .../registerMessagingSocketHandlers.js | 97 +++++++------------ ...chat-conversation-title-tags.component.jsx | 16 ++- .../chat-conversation-title.component.jsx | 11 ++- .../chat-conversation.component.jsx | 10 +- .../chat-conversation.container.jsx | 1 + .../chat-label/chat-label.component.jsx | 14 ++- .../chat-tag-ro/chat-tag-ro.container.jsx | 32 +++++- client/src/utils/GraphQLClient.js | 17 ++++ server/web-sockets/redisSocketEvents.js | 19 +++- 9 files changed, 142 insertions(+), 75 deletions(-) diff --git a/client/src/components/chat-affix/registerMessagingSocketHandlers.js b/client/src/components/chat-affix/registerMessagingSocketHandlers.js index 339acd9a6..120c184dd 100644 --- a/client/src/components/chat-affix/registerMessagingSocketHandlers.js +++ b/client/src/components/chat-affix/registerMessagingSocketHandlers.js @@ -97,70 +97,45 @@ export const registerMessagingHandlers = ({ socket, client }) => { }; const handleConversationChanged = (data) => { - const { type, conversationId, jobId, label } = data; + const { conversationId, type, job_conversations, ...fields } = data; - switch (type) { - case "conversation-marked-read": - client.cache.modify({ - id: client.cache.identify({ - __typename: "conversations", - id: conversationId - }), - fields: { - messages_aggregate: () => ({ aggregate: { count: 0 } }) - } - }); + // Identify the conversation in the Apollo cache + const cacheId = client.cache.identify({ + __typename: "conversations", + id: conversationId + }); - // Optionally, refetch queries if needed - // client.refetchQueries({ - // include: [CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS] - // }); - break; - case "tag-added": - client.cache.modify({ - id: client.cache.identify({ - __typename: "conversations", - id: conversationId - }), - fields: { - job_conversations(existingJobConversations = []) { - return [...existingJobConversations, { __ref: `jobs:${jobId}` }]; - } - } - }); - break; - - case "tag-removed": - client.cache.modify({ - id: client.cache.identify({ - __typename: "conversations", - id: conversationId - }), - fields: { - job_conversations(existingJobConversations = []) { - return existingJobConversations.filter((jobRef) => jobRef.__ref !== `jobs:${jobId}`); - } - } - }); - break; - - case "label-changed": - client.cache.modify({ - id: client.cache.identify({ - __typename: "conversations", - id: conversationId - }), - fields: { - label() { - return label; - } - } - }); - break; - - default: - console.warn(`Unhandled conversation change type: ${type}`); + if (!cacheId) { + console.error(`Could not find conversation with id: ${conversationId}`); + return; } + + client.cache.modify({ + id: cacheId, + fields: { + ...Object.fromEntries( + Object.entries(fields).map(([key, value]) => [ + key, + (cached) => (value !== undefined ? value : cached) // Update with new value or keep existing + ]) + ), + ...(type === "conversation-marked-read" && { + messages_aggregate: () => ({ + aggregate: { count: 0 } // Reset unread count + }) + }), + ...(type === "tag-added" && { + job_conversations: (existing = []) => { + // Merge existing job_conversations with new ones + return [...existing, ...job_conversations]; + } + }), + ...(type === "tag-removed" && { + job_conversations: (existing = [], { readField }) => + existing.filter((jobConversationRef) => readField("jobid", jobConversationRef) !== data.jobId) + }) + } + }); }; socket.on("new-message-summary", handleNewMessageSummary); 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 d851e6c92..944ade8b0 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 @@ -1,13 +1,15 @@ import { useMutation } from "@apollo/client"; import { Tag } from "antd"; -import React from "react"; +import React, { useContext } from "react"; import { Link } from "react-router-dom"; import { logImEXEvent } from "../../firebase/firebase.utils"; import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries"; import OwnerNameDisplay from "../owner-name-display/owner-name-display.component"; +import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; -export default function ChatConversationTitleTags({ jobConversations }) { +export default function ChatConversationTitleTags({ jobConversations, bodyshop }) { const [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG); + const { socket } = useContext(SocketContext); const handleRemoveTag = (jobId) => { const convId = jobConversations[0].conversationid; @@ -27,6 +29,16 @@ export default function ChatConversationTitleTags({ jobConversations }) { } }); } + }).then(() => { + 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, diff --git a/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx b/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx index 41cf0b441..243f699a0 100644 --- a/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx +++ b/client/src/components/chat-conversation-title/chat-conversation-title.component.jsx @@ -7,14 +7,17 @@ import ChatLabelComponent from "../chat-label/chat-label.component"; import ChatPrintButton from "../chat-print-button/chat-print-button.component"; import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container"; -export default function ChatConversationTitle({ conversation }) { +export default function ChatConversationTitle({ conversation, bodyshop }) { return ( {conversation && conversation.phone_num} - + - - + + ); diff --git a/client/src/components/chat-conversation/chat-conversation.component.jsx b/client/src/components/chat-conversation/chat-conversation.component.jsx index d4a9695c0..32e52d5f2 100644 --- a/client/src/components/chat-conversation/chat-conversation.component.jsx +++ b/client/src/components/chat-conversation/chat-conversation.component.jsx @@ -6,7 +6,13 @@ import ChatSendMessage from "../chat-send-message/chat-send-message.component"; import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx"; import "./chat-conversation.styles.scss"; -export default function ChatConversationComponent({ subState, conversation, messages, handleMarkConversationAsRead }) { +export default function ChatConversationComponent({ + subState, + conversation, + messages, + handleMarkConversationAsRead, + bodyshop +}) { const [loading, error] = subState; if (loading) return ; @@ -18,7 +24,7 @@ export default function ChatConversationComponent({ subState, conversation, mess onMouseDown={handleMarkConversationAsRead} onKeyDown={handleMarkConversationAsRead} > - + diff --git a/client/src/components/chat-conversation/chat-conversation.container.jsx b/client/src/components/chat-conversation/chat-conversation.container.jsx index 58107ad7c..0fb60aea4 100644 --- a/client/src/components/chat-conversation/chat-conversation.container.jsx +++ b/client/src/components/chat-conversation/chat-conversation.container.jsx @@ -74,6 +74,7 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) { conversation={convoData ? convoData.conversations_by_pk : {}} messages={convoData ? convoData.conversations_by_pk.messages : []} handleMarkConversationAsRead={handleMarkConversationAsRead} + bodyshop={bodyshop} /> ); } diff --git a/client/src/components/chat-label/chat-label.component.jsx b/client/src/components/chat-label/chat-label.component.jsx index 7157d02c8..e577bbf23 100644 --- a/client/src/components/chat-label/chat-label.component.jsx +++ b/client/src/components/chat-label/chat-label.component.jsx @@ -1,14 +1,16 @@ import { PlusOutlined } from "@ant-design/icons"; import { useMutation } from "@apollo/client"; import { Input, notification, Spin, Tag, Tooltip } from "antd"; -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import { useTranslation } from "react-i18next"; import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries"; +import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; -export default function ChatLabel({ conversation }) { +export default function ChatLabel({ conversation, bodyshop }) { const [loading, setLoading] = useState(false); const [editing, setEditing] = useState(false); const [value, setValue] = useState(conversation.label); + const { socket } = useContext(SocketContext); const { t } = useTranslation(); const [updateLabel] = useMutation(UPDATE_CONVERSATION_LABEL); @@ -26,6 +28,14 @@ export default function ChatLabel({ conversation }) { }) }); } else { + if (socket) { + socket.emit("conversation-modified", { + type: "label-updated", + conversationId: conversation.id, + bodyshopId: bodyshop.id, + label: value + }); + } setEditing(false); } } catch (error) { 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 f9b6fa5ad..6e01ea5ed 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 @@ -2,16 +2,18 @@ import { PlusOutlined } from "@ant-design/icons"; import { useLazyQuery, useMutation } from "@apollo/client"; import { Tag } from "antd"; import _ from "lodash"; -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import { useTranslation } from "react-i18next"; import { logImEXEvent } from "../../firebase/firebase.utils"; import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries"; import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries"; import ChatTagRo from "./chat-tag-ro.component"; +import SocketContext from "../../contexts/SocketIO/socketContext.jsx"; -export default function ChatTagRoContainer({ conversation }) { +export default function ChatTagRoContainer({ conversation, bodyshop }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); + const { socket } = useContext(SocketContext); const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS); @@ -32,7 +34,31 @@ export default function ChatTagRoContainer({ conversation }) { const handleInsertTag = (value, option) => { logImEXEvent("messaging_add_job_tag"); - insertTag({ variables: { jobId: option.key } }); + + 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] + }); + } + }); setOpen(false); }; diff --git a/client/src/utils/GraphQLClient.js b/client/src/utils/GraphQLClient.js index 789bfdf10..e0d409bd7 100644 --- a/client/src/utils/GraphQLClient.js +++ b/client/src/utils/GraphQLClient.js @@ -149,6 +149,23 @@ const cache = new InMemoryCache({ fields: { conversations: offsetLimitPagination() } + }, + conversations: { + fields: { + job_conversations: { + keyArgs: false, // Indicates that all job_conversations share the same key + merge(existing = [], incoming) { + // Merge existing and incoming job_conversations + const merged = [ + ...existing, + ...incoming.filter( + (incomingItem) => !existing.some((existingItem) => existingItem.__ref === incomingItem.__ref) + ) + ]; + return merged; + } + } + } } } }); diff --git a/server/web-sockets/redisSocketEvents.js b/server/web-sockets/redisSocketEvents.js index 8e1358994..d2dd9eb7b 100644 --- a/server/web-sockets/redisSocketEvents.js +++ b/server/web-sockets/redisSocketEvents.js @@ -145,7 +145,6 @@ const redisSocketEvents = ({ error: error.message, stack: error.stack }); - socket.emit("error", { message: "Failed to join conversation" }); } }; const leaveConversationRoom = ({ bodyshopId, conversationId }) => { @@ -162,6 +161,24 @@ const redisSocketEvents = ({ } }; + const conversationModified = ({ bodyshopId, conversationId, ...fields }) => { + try { + // Retrieve the room name for the conversation + const room = getBodyshopConversationRoom({ bodyshopId, conversationId }); + // Emit the updated data to all clients in the room + io.to(room).emit("conversation-changed", { + conversationId, + ...fields + }); + } catch (error) { + logger.log("Failed to handle conversation modification", "error", "io-redis", null, { + error: error.message, + stack: error.stack + }); + } + }; + + socket.on("conversation-modified", conversationModified); socket.on("join-bodyshop-conversation", joinConversationRoom); socket.on("leave-bodyshop-conversation", leaveConversationRoom); };