264 lines
8.9 KiB
JavaScript
264 lines
8.9 KiB
JavaScript
import { gql, useApolloClient, useQuery, useSubscription } from "@apollo/client";
|
||
import axios from "axios";
|
||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||
import { connect } from "react-redux";
|
||
import { createStructuredSelector } from "reselect";
|
||
import { CONVERSATION_SUBSCRIPTION_BY_PK, GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries";
|
||
import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors";
|
||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||
import ChatConversationComponent from "./chat-conversation.component";
|
||
import { useSocket } from "../../contexts/SocketIO/useSocket.js";
|
||
|
||
const mapStateToProps = createStructuredSelector({
|
||
selectedConversation: selectSelectedConversation,
|
||
bodyshop: selectBodyshop
|
||
});
|
||
|
||
function ChatConversationContainer({ bodyshop, selectedConversation }) {
|
||
const client = useApolloClient();
|
||
const { socket } = useSocket();
|
||
const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
|
||
const [markingAsUnreadInProgress, setMarkingAsUnreadInProgress] = useState(false);
|
||
|
||
const {
|
||
loading: convoLoading,
|
||
error: convoError,
|
||
data: convoData
|
||
} = useQuery(GET_CONVERSATION_DETAILS, {
|
||
variables: { conversationId: selectedConversation },
|
||
fetchPolicy: "network-only",
|
||
nextFetchPolicy: "network-only",
|
||
skip: !selectedConversation
|
||
});
|
||
|
||
const conversation = convoData?.conversations_by_pk;
|
||
|
||
// Subscription for conversation updates (used when socket is NOT connected)
|
||
useSubscription(CONVERSATION_SUBSCRIPTION_BY_PK, {
|
||
skip: socket?.connected || !selectedConversation,
|
||
variables: { conversationId: selectedConversation },
|
||
onData: ({ data: subscriptionResult, client }) => {
|
||
const messages = subscriptionResult?.data?.messages;
|
||
if (!messages || messages.length === 0) return;
|
||
|
||
messages.forEach((message) => {
|
||
const messageRef = client.cache.identify(message);
|
||
|
||
client.cache.writeFragment({
|
||
id: messageRef,
|
||
fragment: gql`
|
||
fragment NewMessage on messages {
|
||
id
|
||
status
|
||
text
|
||
isoutbound
|
||
image
|
||
image_path
|
||
userid
|
||
created_at
|
||
read
|
||
is_system
|
||
}
|
||
`,
|
||
data: message
|
||
});
|
||
|
||
client.cache.modify({
|
||
id: client.cache.identify({ __typename: "conversations", id: selectedConversation }),
|
||
fields: {
|
||
messages(existingMessages = []) {
|
||
const alreadyExists = existingMessages.some((msg) => msg.__ref === messageRef);
|
||
if (alreadyExists) return existingMessages;
|
||
return [...existingMessages, { __ref: messageRef }];
|
||
},
|
||
updated_at() {
|
||
return message.created_at;
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
});
|
||
|
||
/**
|
||
* Best-effort badge update:
|
||
* This assumes your list query uses messages_aggregate.aggregate.count as UNREAD inbound count.
|
||
* If it’s total messages, rename/create a dedicated unread aggregate in the list query and update that field instead.
|
||
*/
|
||
const setConversationUnreadCountBestEffort = useCallback(
|
||
(conversationId, unreadCount) => {
|
||
if (!conversationId) return;
|
||
|
||
client.cache.modify({
|
||
id: client.cache.identify({ __typename: "conversations", id: conversationId }),
|
||
fields: {
|
||
messages_aggregate(existing) {
|
||
if (!existing?.aggregate) return existing;
|
||
return { ...existing, aggregate: { ...existing.aggregate, count: unreadCount } };
|
||
}
|
||
}
|
||
});
|
||
},
|
||
[client.cache]
|
||
);
|
||
|
||
const updateCacheWithReadMessages = useCallback(
|
||
(conversationId, messageIds) => {
|
||
if (!conversationId || !messageIds?.length) return;
|
||
|
||
messageIds.forEach((messageId) => {
|
||
client.cache.modify({
|
||
id: client.cache.identify({ __typename: "messages", id: messageId }),
|
||
fields: { read: () => true }
|
||
});
|
||
});
|
||
|
||
setConversationUnreadCountBestEffort(conversationId, 0);
|
||
},
|
||
[client.cache, setConversationUnreadCountBestEffort]
|
||
);
|
||
|
||
const applyUnreadStateWithMaxOneUnread = useCallback(
|
||
({ conversationId, lastUnreadMessageId, messageIdsMarkedRead = [], unreadCount = 1 }) => {
|
||
if (!conversationId || !lastUnreadMessageId) return;
|
||
|
||
messageIdsMarkedRead.forEach((id) => {
|
||
client.cache.modify({
|
||
id: client.cache.identify({ __typename: "messages", id }),
|
||
fields: { read: () => true }
|
||
});
|
||
});
|
||
|
||
client.cache.modify({
|
||
id: client.cache.identify({ __typename: "messages", id: lastUnreadMessageId }),
|
||
fields: { read: () => false }
|
||
});
|
||
|
||
setConversationUnreadCountBestEffort(conversationId, unreadCount);
|
||
},
|
||
[client.cache, setConversationUnreadCountBestEffort]
|
||
);
|
||
|
||
// WebSocket event handlers
|
||
useEffect(() => {
|
||
if (!socket?.connected) return;
|
||
|
||
const handleConversationChange = (data) => {
|
||
if (data?.type === "conversation-marked-read") {
|
||
updateCacheWithReadMessages(data.conversationId, data.messageIds);
|
||
}
|
||
|
||
if (data?.type === "conversation-marked-unread") {
|
||
applyUnreadStateWithMaxOneUnread({
|
||
conversationId: data.conversationId,
|
||
lastUnreadMessageId: data.lastUnreadMessageId,
|
||
messageIdsMarkedRead: data.messageIdsMarkedRead,
|
||
unreadCount: data.unreadCount
|
||
});
|
||
}
|
||
};
|
||
|
||
socket.on("conversation-changed", handleConversationChange);
|
||
return () => socket.off("conversation-changed", handleConversationChange);
|
||
}, [socket, updateCacheWithReadMessages, applyUnreadStateWithMaxOneUnread]);
|
||
|
||
// Join/leave conversation via WebSocket
|
||
useEffect(() => {
|
||
if (!socket?.connected || !selectedConversation || !bodyshop?.id) return;
|
||
|
||
socket.emit("join-bodyshop-conversation", {
|
||
bodyshopId: bodyshop.id,
|
||
conversationId: selectedConversation
|
||
});
|
||
|
||
return () => {
|
||
socket.emit("leave-bodyshop-conversation", {
|
||
bodyshopId: bodyshop.id,
|
||
conversationId: selectedConversation
|
||
});
|
||
};
|
||
}, [socket, bodyshop, selectedConversation]);
|
||
|
||
const inboundNonSystemMessages = useMemo(() => {
|
||
const msgs = conversation?.messages || [];
|
||
return msgs
|
||
.filter((m) => m && !m.isoutbound && !m.is_system)
|
||
.slice()
|
||
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
||
}, [conversation?.messages]);
|
||
|
||
const canMarkUnread = inboundNonSystemMessages.length > 0;
|
||
|
||
const handleMarkConversationAsRead = async () => {
|
||
if (!conversation || markingAsReadInProgress) return;
|
||
|
||
const unreadMessageIds = conversation.messages
|
||
?.filter((message) => !message.read && !message.isoutbound && !message.is_system)
|
||
.map((message) => message.id);
|
||
|
||
if (unreadMessageIds?.length > 0) {
|
||
setMarkingAsReadInProgress(true);
|
||
try {
|
||
await axios.post("/sms/markConversationRead", {
|
||
conversation,
|
||
imexshopid: bodyshop?.imexshopid,
|
||
bodyshopid: bodyshop?.id
|
||
});
|
||
|
||
updateCacheWithReadMessages(selectedConversation, unreadMessageIds);
|
||
} catch (error) {
|
||
console.error("Error marking conversation as read:", error.message);
|
||
} finally {
|
||
setMarkingAsReadInProgress(false);
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleMarkLastMessageAsUnread = async () => {
|
||
if (!conversation || markingAsUnreadInProgress) return;
|
||
if (!bodyshop?.id || !bodyshop?.imexshopid) return;
|
||
|
||
const lastInbound = inboundNonSystemMessages[inboundNonSystemMessages.length - 1];
|
||
if (!lastInbound?.id) return;
|
||
|
||
setMarkingAsUnreadInProgress(true);
|
||
try {
|
||
const res = await axios.post("/sms/markLastMessageUnread", {
|
||
conversationId: conversation.id,
|
||
imexshopid: bodyshop.imexshopid,
|
||
bodyshopid: bodyshop.id
|
||
});
|
||
|
||
const payload = res?.data || {};
|
||
if (payload.lastUnreadMessageId) {
|
||
applyUnreadStateWithMaxOneUnread({
|
||
conversationId: conversation.id,
|
||
lastUnreadMessageId: payload.lastUnreadMessageId,
|
||
messageIdsMarkedRead: payload.messageIdsMarkedRead || [],
|
||
unreadCount: typeof payload.unreadCount === "number" ? payload.unreadCount : 1
|
||
});
|
||
} else {
|
||
setConversationUnreadCountBestEffort(conversation.id, 0);
|
||
}
|
||
} catch (error) {
|
||
console.error("Error marking last message unread:", error.message);
|
||
} finally {
|
||
setMarkingAsUnreadInProgress(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<ChatConversationComponent
|
||
subState={[convoLoading, convoError]}
|
||
conversation={conversation || {}}
|
||
messages={conversation?.messages || []}
|
||
handleMarkConversationAsRead={handleMarkConversationAsRead}
|
||
handleMarkLastMessageAsUnread={handleMarkLastMessageAsUnread}
|
||
markingAsUnreadInProgress={markingAsUnreadInProgress}
|
||
canMarkUnread={canMarkUnread}
|
||
/>
|
||
);
|
||
}
|
||
|
||
export default connect(mapStateToProps)(ChatConversationContainer);
|