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);
};