feature/IO-3000-messaging-sockets-migrations2 -

- Conversation Labels Synced
- Job Tagging Synced

Signed-off-by: Dave Richer <dave@imexsystems.ca>
This commit is contained in:
Dave Richer
2024-11-20 18:22:27 -08:00
parent 250faa672f
commit 06afd6da5b
9 changed files with 142 additions and 75 deletions

View File

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

View File

@@ -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,

View File

@@ -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 (
<Space wrap>
<PhoneNumberFormatter>{conversation && conversation.phone_num}</PhoneNumberFormatter>
<ChatLabelComponent conversation={conversation} />
<ChatLabelComponent conversation={conversation} bodyshop={bodyshop} />
<ChatPrintButton conversation={conversation} />
<ChatConversationTitleTags jobConversations={(conversation && conversation.job_conversations) || []} />
<ChatTagRoContainer conversation={conversation || []} />
<ChatConversationTitleTags
jobConversations={(conversation && conversation.job_conversations) || []}
bodyshop={bodyshop}
/>
<ChatTagRoContainer conversation={conversation || []} bodyshop={bodyshop} />
<ChatArchiveButton conversation={conversation} />
</Space>
);

View File

@@ -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 <LoadingSkeleton />;
@@ -18,7 +24,7 @@ export default function ChatConversationComponent({ subState, conversation, mess
onMouseDown={handleMarkConversationAsRead}
onKeyDown={handleMarkConversationAsRead}
>
<ChatConversationTitle conversation={conversation} />
<ChatConversationTitle conversation={conversation} bodyshop={bodyshop} />
<ChatMessageListComponent messages={messages} />
<ChatSendMessage conversation={conversation} />
</div>

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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