This commit is contained in:
Dave
2025-12-30 13:41:26 -05:00
parent 4a7bb07345
commit 2c7b328596
10 changed files with 292 additions and 80 deletions

View File

@@ -19,7 +19,9 @@ export function ChatConversationComponent({
conversation,
messages,
handleMarkConversationAsRead,
bodyshop
handleMarkLastMessageAsUnread,
markingAsUnreadInProgress,
canMarkUnread
}) {
const [loading, error] = subState;
@@ -33,7 +35,12 @@ export function ChatConversationComponent({
onMouseDown={handleMarkConversationAsRead}
onKeyDown={handleMarkConversationAsRead}
>
<ChatConversationTitle conversation={conversation} bodyshop={bodyshop} />
<ChatConversationTitle
conversation={conversation}
onMarkUnread={handleMarkLastMessageAsUnread}
markUnreadDisabled={!canMarkUnread}
markUnreadLoading={markingAsUnreadInProgress}
/>
<ChatMessageListComponent messages={messages} />
<ChatSendMessage conversation={conversation} />
</div>

View File

@@ -1,6 +1,6 @@
import { gql, useApolloClient, useQuery, useSubscription } from "@apollo/client";
import axios from "axios";
import { useCallback, useEffect, useState } from "react";
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";
@@ -18,8 +18,8 @@ function ChatConversationContainer({ bodyshop, selectedConversation }) {
const client = useApolloClient();
const { socket } = useSocket();
const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false);
const [markingAsUnreadInProgress, setMarkingAsUnreadInProgress] = useState(false);
// Fetch conversation details
const {
loading: convoLoading,
error: convoError,
@@ -27,24 +27,23 @@ function ChatConversationContainer({ bodyshop, selectedConversation }) {
} = useQuery(GET_CONVERSATION_DETAILS, {
variables: { conversationId: selectedConversation },
fetchPolicy: "network-only",
nextFetchPolicy: "network-only"
nextFetchPolicy: "network-only",
skip: !selectedConversation
});
// Subscription for conversation updates
const conversation = convoData?.conversations_by_pk;
// Subscription for conversation updates (used when socket is NOT connected)
useSubscription(CONVERSATION_SUBSCRIPTION_BY_PK, {
skip: socket?.connected,
skip: socket?.connected || !selectedConversation,
variables: { conversationId: selectedConversation },
onData: ({ data: subscriptionResult, client }) => {
// Extract the messages array from the result
const messages = subscriptionResult?.data?.messages;
if (!messages || messages.length === 0) {
console.warn("No messages found in subscription result.");
return;
}
if (!messages || messages.length === 0) return;
messages.forEach((message) => {
const messageRef = client.cache.identify(message);
// Write the new message to the cache
client.cache.writeFragment({
id: messageRef,
fragment: gql`
@@ -64,7 +63,6 @@ function ChatConversationContainer({ bodyshop, selectedConversation }) {
data: message
});
// Update the conversation cache to include the new message
client.cache.modify({
id: client.cache.identify({ __typename: "conversations", id: selectedConversation }),
fields: {
@@ -82,6 +80,28 @@ function ChatConversationContainer({ bodyshop, selectedConversation }) {
}
});
/**
* Best-effort badge update:
* This assumes your list query uses messages_aggregate.aggregate.count as UNREAD inbound count.
* If its 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;
@@ -89,13 +109,34 @@ function ChatConversationContainer({ bodyshop, selectedConversation }) {
messageIds.forEach((messageId) => {
client.cache.modify({
id: client.cache.identify({ __typename: "messages", id: messageId }),
fields: {
read: () => true
}
fields: { read: () => true }
});
});
setConversationUnreadCountBestEffort(conversationId, 0);
},
[client.cache]
[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
@@ -103,20 +144,25 @@ function ChatConversationContainer({ bodyshop, selectedConversation }) {
if (!socket?.connected) return;
const handleConversationChange = (data) => {
if (data.type === "conversation-marked-read") {
const { conversationId, messageIds } = data;
updateCacheWithReadMessages(conversationId, messageIds);
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]);
return () => {
socket.off("conversation-changed", handleConversationChange);
};
}, [socket, updateCacheWithReadMessages]);
// Join and leave conversation via WebSocket
// Join/leave conversation via WebSocket
useEffect(() => {
if (!socket?.connected || !selectedConversation || !bodyshop?.id) return;
@@ -133,15 +179,21 @@ function ChatConversationContainer({ bodyshop, selectedConversation }) {
};
}, [socket, bodyshop, selectedConversation]);
// Mark conversation as read
const handleMarkConversationAsRead = async () => {
if (!convoData || markingAsReadInProgress) return;
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 conversation = convoData.conversations_by_pk;
if (!conversation) return;
const canMarkUnread = inboundNonSystemMessages.length > 0;
const handleMarkConversationAsRead = async () => {
if (!conversation || markingAsReadInProgress) return;
const unreadMessageIds = conversation.messages
?.filter((message) => !message.read && !message.isoutbound)
?.filter((message) => !message.read && !message.isoutbound && !message.is_system)
.map((message) => message.id);
if (unreadMessageIds?.length > 0) {
@@ -162,12 +214,48 @@ function ChatConversationContainer({ bodyshop, selectedConversation }) {
}
};
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={convoData?.conversations_by_pk || {}}
messages={convoData?.conversations_by_pk?.messages || []}
conversation={conversation || {}}
messages={conversation?.messages || []}
handleMarkConversationAsRead={handleMarkConversationAsRead}
handleMarkLastMessageAsUnread={handleMarkLastMessageAsUnread}
markingAsUnreadInProgress={markingAsUnreadInProgress}
canMarkUnread={canMarkUnread}
/>
);
}