diff --git a/client/public/firebase-messaging-sw.js b/client/public/firebase-messaging-sw.js index 69bf1315f..1eb03072a 100644 --- a/client/public/firebase-messaging-sw.js +++ b/client/public/firebase-messaging-sw.js @@ -1,6 +1,6 @@ // Scripts for firebase and firebase messaging -importScripts("https://www.gstatic.com/firebasejs/10.14.1/firebase-app.js"); -importScripts("https://www.gstatic.com/firebasejs/10.14.1/firebase-messaging.js"); +importScripts("https://www.gstatic.com/firebasejs/10.14.1/firebase-app-compat.js"); +importScripts("https://www.gstatic.com/firebasejs/10.14.1/firebase-messaging-compat.js"); // Initialize the Firebase app in the service worker by passing the generated config let firebaseConfig; diff --git a/client/src/components/chat-conversation/chat-conversation.container.jsx b/client/src/components/chat-conversation/chat-conversation.container.jsx index b477b436e..ffaa3d807 100644 --- a/client/src/components/chat-conversation/chat-conversation.container.jsx +++ b/client/src/components/chat-conversation/chat-conversation.container.jsx @@ -1,10 +1,10 @@ -import { useApolloClient, useQuery } from "@apollo/client"; +import { gql, useApolloClient, useQuery, useSubscription } from "@apollo/client"; import axios from "axios"; import React, { useCallback, useContext, useEffect, useState } from "react"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import SocketContext from "../../contexts/SocketIO/socketContext"; -import { GET_CONVERSATION_DETAILS } from "../../graphql/conversations.queries"; +import { GET_CONVERSATION_DETAILS, CONVERSATION_SUBSCRIPTION_BY_PK } from "../../graphql/conversations.queries"; import { selectSelectedConversation } from "../../redux/messaging/messaging.selectors"; import { selectBodyshop } from "../../redux/user/user.selectors"; import ChatConversationComponent from "./chat-conversation.component"; @@ -14,11 +14,12 @@ const mapStateToProps = createStructuredSelector({ bodyshop: selectBodyshop }); -export function ChatConversationContainer({ bodyshop, selectedConversation }) { +function ChatConversationContainer({ bodyshop, selectedConversation }) { const client = useApolloClient(); const { socket } = useContext(SocketContext); const [markingAsReadInProgress, setMarkingAsReadInProgress] = useState(false); + // Fetch conversation details const { loading: convoLoading, error: convoError, @@ -29,49 +30,85 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) { nextFetchPolicy: "network-only" }); - const updateCacheWithReadMessages = useCallback( - (conversationId, messageIds) => { - if (!conversationId || !messageIds || messageIds.length === 0) return; + // Subscription for conversation updates + useSubscription(CONVERSATION_SUBSCRIPTION_BY_PK, { + skip: socket?.connected, + 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; + } - // Mark individual messages as read - messageIds.forEach((messageId) => { + messages.forEach((message) => { + const messageRef = client.cache.identify(message); + + // Write the new message to the cache + client.cache.writeFragment({ + id: messageRef, + fragment: gql` + fragment NewMessage on messages { + id + status + text + isoutbound + image + image_path + userid + created_at + read + } + `, + data: message + }); + + // Update the conversation cache to include the new message client.cache.modify({ - id: client.cache.identify({ __typename: "messages", id: messageId }), + id: client.cache.identify({ __typename: "conversations", id: selectedConversation }), fields: { - read() { - return true; // Mark message as read + messages(existingMessages = []) { + const alreadyExists = existingMessages.some((msg) => msg.__ref === messageRef); + if (alreadyExists) return existingMessages; + return [...existingMessages, { __ref: messageRef }]; } } }); }); + } + }); + + 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 + } + }); + }); - // Update aggregate unread count for the conversation client.cache.modify({ id: client.cache.identify({ __typename: "conversations", id: conversationId }), fields: { - messages_aggregate(existingAggregate) { - return { - ...existingAggregate, - aggregate: { - ...existingAggregate.aggregate, - count: 0 // No unread messages remaining - } - }; - } + unreadcnt: () => 0 } }); }, [client.cache] ); - // Handle WebSocket events + // WebSocket event handlers useEffect(() => { - if (!socket || !socket.connected) return; + if (!socket?.connected) return; const handleConversationChange = (data) => { if (data.type === "conversation-marked-read") { const { conversationId, messageIds } = data; - console.log("Conversation change received:", data); updateCacheWithReadMessages(conversationId, messageIds); } }; @@ -81,11 +118,11 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) { return () => { socket.off("conversation-changed", handleConversationChange); }; - }, [socket, client, updateCacheWithReadMessages]); + }, [socket, updateCacheWithReadMessages]); - // Handle joining/leaving conversation + // Join and leave conversation via WebSocket useEffect(() => { - if (!socket || !socket.connected) return; + if (!socket?.connected || !selectedConversation || !bodyshop?.id) return; socket.emit("join-bodyshop-conversation", { bodyshopId: bodyshop.id, @@ -98,17 +135,14 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) { conversationId: selectedConversation }); }; - }, [selectedConversation, bodyshop, socket]); + }, [socket, bodyshop, selectedConversation]); - // Handle marking conversation as read + // Mark conversation as read const handleMarkConversationAsRead = async () => { - if (!convoData || !selectedConversation || markingAsReadInProgress) return; + if (!convoData || markingAsReadInProgress) return; const conversation = convoData.conversations_by_pk; - if (!conversation) { - console.warn(`No data found for conversation ID: ${selectedConversation}`); - return; - } + if (!conversation) return; const unreadMessageIds = conversation.messages ?.filter((message) => !message.read && !message.isoutbound) @@ -116,22 +150,16 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) { if (unreadMessageIds?.length > 0) { setMarkingAsReadInProgress(true); - try { - const payload = { + await axios.post("/sms/markConversationRead", { conversation, imexshopid: bodyshop?.imexshopid, bodyshopid: bodyshop?.id - }; + }); - console.log("Marking conversation as read:", payload); - - await axios.post("/sms/markConversationRead", payload); - - // Update local cache updateCacheWithReadMessages(selectedConversation, unreadMessageIds); } catch (error) { - console.error("Error marking conversation as read:", error.response?.data || error.message); + console.error("Error marking conversation as read:", error.message); } finally { setMarkingAsReadInProgress(false); } @@ -141,11 +169,11 @@ export function ChatConversationContainer({ bodyshop, selectedConversation }) { return ( ); } -export default connect(mapStateToProps, null)(ChatConversationContainer); +export default connect(mapStateToProps)(ChatConversationContainer);