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 ( ); } export default connect(mapStateToProps)(ChatConversationContainer);