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:
@@ -97,70 +97,45 @@ export const registerMessagingHandlers = ({ socket, client }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleConversationChanged = (data) => {
|
const handleConversationChanged = (data) => {
|
||||||
const { type, conversationId, jobId, label } = data;
|
const { conversationId, type, job_conversations, ...fields } = data;
|
||||||
|
|
||||||
switch (type) {
|
// Identify the conversation in the Apollo cache
|
||||||
case "conversation-marked-read":
|
const cacheId = client.cache.identify({
|
||||||
client.cache.modify({
|
__typename: "conversations",
|
||||||
id: client.cache.identify({
|
id: conversationId
|
||||||
__typename: "conversations",
|
});
|
||||||
id: conversationId
|
|
||||||
}),
|
|
||||||
fields: {
|
|
||||||
messages_aggregate: () => ({ aggregate: { count: 0 } })
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Optionally, refetch queries if needed
|
if (!cacheId) {
|
||||||
// client.refetchQueries({
|
console.error(`Could not find conversation with id: ${conversationId}`);
|
||||||
// include: [CONVERSATION_LIST_QUERY, GET_CONVERSATION_DETAILS]
|
return;
|
||||||
// });
|
|
||||||
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}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
socket.on("new-message-summary", handleNewMessageSummary);
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Tag } from "antd";
|
import { Tag } from "antd";
|
||||||
import React from "react";
|
import React, { useContext } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
import { REMOVE_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||||
import OwnerNameDisplay from "../owner-name-display/owner-name-display.component";
|
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 [removeJobConversation] = useMutation(REMOVE_CONVERSATION_TAG);
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
const handleRemoveTag = (jobId) => {
|
const handleRemoveTag = (jobId) => {
|
||||||
const convId = jobConversations[0].conversationid;
|
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", {
|
logImEXEvent("messaging_remove_job_tag", {
|
||||||
conversationId: convId,
|
conversationId: convId,
|
||||||
|
|||||||
@@ -7,14 +7,17 @@ import ChatLabelComponent from "../chat-label/chat-label.component";
|
|||||||
import ChatPrintButton from "../chat-print-button/chat-print-button.component";
|
import ChatPrintButton from "../chat-print-button/chat-print-button.component";
|
||||||
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
|
import ChatTagRoContainer from "../chat-tag-ro/chat-tag-ro.container";
|
||||||
|
|
||||||
export default function ChatConversationTitle({ conversation }) {
|
export default function ChatConversationTitle({ conversation, bodyshop }) {
|
||||||
return (
|
return (
|
||||||
<Space wrap>
|
<Space wrap>
|
||||||
<PhoneNumberFormatter>{conversation && conversation.phone_num}</PhoneNumberFormatter>
|
<PhoneNumberFormatter>{conversation && conversation.phone_num}</PhoneNumberFormatter>
|
||||||
<ChatLabelComponent conversation={conversation} />
|
<ChatLabelComponent conversation={conversation} bodyshop={bodyshop} />
|
||||||
<ChatPrintButton conversation={conversation} />
|
<ChatPrintButton conversation={conversation} />
|
||||||
<ChatConversationTitleTags jobConversations={(conversation && conversation.job_conversations) || []} />
|
<ChatConversationTitleTags
|
||||||
<ChatTagRoContainer conversation={conversation || []} />
|
jobConversations={(conversation && conversation.job_conversations) || []}
|
||||||
|
bodyshop={bodyshop}
|
||||||
|
/>
|
||||||
|
<ChatTagRoContainer conversation={conversation || []} bodyshop={bodyshop} />
|
||||||
<ChatArchiveButton conversation={conversation} />
|
<ChatArchiveButton conversation={conversation} />
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ import ChatSendMessage from "../chat-send-message/chat-send-message.component";
|
|||||||
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx";
|
import LoadingSkeleton from "../loading-skeleton/loading-skeleton.component.jsx";
|
||||||
import "./chat-conversation.styles.scss";
|
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;
|
const [loading, error] = subState;
|
||||||
|
|
||||||
if (loading) return <LoadingSkeleton />;
|
if (loading) return <LoadingSkeleton />;
|
||||||
@@ -18,7 +24,7 @@ export default function ChatConversationComponent({ subState, conversation, mess
|
|||||||
onMouseDown={handleMarkConversationAsRead}
|
onMouseDown={handleMarkConversationAsRead}
|
||||||
onKeyDown={handleMarkConversationAsRead}
|
onKeyDown={handleMarkConversationAsRead}
|
||||||
>
|
>
|
||||||
<ChatConversationTitle conversation={conversation} />
|
<ChatConversationTitle conversation={conversation} bodyshop={bodyshop} />
|
||||||
<ChatMessageListComponent messages={messages} />
|
<ChatMessageListComponent messages={messages} />
|
||||||
<ChatSendMessage conversation={conversation} />
|
<ChatSendMessage conversation={conversation} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
|||||||
conversation={convoData ? convoData.conversations_by_pk : {}}
|
conversation={convoData ? convoData.conversations_by_pk : {}}
|
||||||
messages={convoData ? convoData.conversations_by_pk.messages : []}
|
messages={convoData ? convoData.conversations_by_pk.messages : []}
|
||||||
handleMarkConversationAsRead={handleMarkConversationAsRead}
|
handleMarkConversationAsRead={handleMarkConversationAsRead}
|
||||||
|
bodyshop={bodyshop}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { PlusOutlined } from "@ant-design/icons";
|
import { PlusOutlined } from "@ant-design/icons";
|
||||||
import { useMutation } from "@apollo/client";
|
import { useMutation } from "@apollo/client";
|
||||||
import { Input, notification, Spin, Tag, Tooltip } from "antd";
|
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 { useTranslation } from "react-i18next";
|
||||||
import { UPDATE_CONVERSATION_LABEL } from "../../graphql/conversations.queries";
|
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 [loading, setLoading] = useState(false);
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [value, setValue] = useState(conversation.label);
|
const [value, setValue] = useState(conversation.label);
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [updateLabel] = useMutation(UPDATE_CONVERSATION_LABEL);
|
const [updateLabel] = useMutation(UPDATE_CONVERSATION_LABEL);
|
||||||
@@ -26,6 +28,14 @@ export default function ChatLabel({ conversation }) {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
if (socket) {
|
||||||
|
socket.emit("conversation-modified", {
|
||||||
|
type: "label-updated",
|
||||||
|
conversationId: conversation.id,
|
||||||
|
bodyshopId: bodyshop.id,
|
||||||
|
label: value
|
||||||
|
});
|
||||||
|
}
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -2,16 +2,18 @@ import { PlusOutlined } from "@ant-design/icons";
|
|||||||
import { useLazyQuery, useMutation } from "@apollo/client";
|
import { useLazyQuery, useMutation } from "@apollo/client";
|
||||||
import { Tag } from "antd";
|
import { Tag } from "antd";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import React, { useState } from "react";
|
import React, { useContext, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { logImEXEvent } from "../../firebase/firebase.utils";
|
import { logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
import { INSERT_CONVERSATION_TAG } from "../../graphql/job-conversations.queries";
|
||||||
import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries";
|
import { SEARCH_FOR_JOBS } from "../../graphql/jobs.queries";
|
||||||
import ChatTagRo from "./chat-tag-ro.component";
|
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 { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const { socket } = useContext(SocketContext);
|
||||||
|
|
||||||
const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS);
|
const [loadRo, { loading, data }] = useLazyQuery(SEARCH_FOR_JOBS);
|
||||||
|
|
||||||
@@ -32,7 +34,31 @@ export default function ChatTagRoContainer({ conversation }) {
|
|||||||
|
|
||||||
const handleInsertTag = (value, option) => {
|
const handleInsertTag = (value, option) => {
|
||||||
logImEXEvent("messaging_add_job_tag");
|
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);
|
setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,23 @@ const cache = new InMemoryCache({
|
|||||||
fields: {
|
fields: {
|
||||||
conversations: offsetLimitPagination()
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -145,7 +145,6 @@ const redisSocketEvents = ({
|
|||||||
error: error.message,
|
error: error.message,
|
||||||
stack: error.stack
|
stack: error.stack
|
||||||
});
|
});
|
||||||
socket.emit("error", { message: "Failed to join conversation" });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const leaveConversationRoom = ({ bodyshopId, conversationId }) => {
|
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("join-bodyshop-conversation", joinConversationRoom);
|
||||||
socket.on("leave-bodyshop-conversation", leaveConversationRoom);
|
socket.on("leave-bodyshop-conversation", leaveConversationRoom);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user