diff --git a/client/src/components/chat-popup/chat-popup.component.jsx b/client/src/components/chat-popup/chat-popup.component.jsx index 01c5b3e85..95513e09c 100644 --- a/client/src/components/chat-popup/chat-popup.component.jsx +++ b/client/src/components/chat-popup/chat-popup.component.jsx @@ -1,7 +1,7 @@ import { InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined } from "@ant-design/icons"; import { useApolloClient, useLazyQuery, useQuery } from "@apollo/client"; import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; @@ -27,32 +27,52 @@ const mapDispatchToProps = (dispatch) => ({ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleChatVisible }) { const { t } = useTranslation(); - const [pollInterval, setPollInterval] = useState(0); const { socket } = useSocket(); - const client = useApolloClient(); // Apollo Client instance for cache operations + const client = useApolloClient(); - // Lazy query for conversations - const [getConversations, { loading, data, refetch }] = useLazyQuery(CONVERSATION_LIST_QUERY, { + // When socket is connected, we do NOT poll (socket should push updates). + // When disconnected, we poll as a fallback. + const [pollInterval, setPollInterval] = useState(0); + + // Ensure conversations query runs once on initial page load (component mount). + const hasLoadedConversationsOnceRef = useRef(false); + + // Preserve the last known unread aggregate count so the badge doesn't "vanish" + // when UNREAD_CONVERSATION_COUNT gets skipped after socket connects. + const [unreadAggregateCount, setUnreadAggregateCount] = useState(0); + + // Lazy query for conversations (executed manually) + const [getConversations, { loading, data, refetch, called }] = useLazyQuery(CONVERSATION_LIST_QUERY, { fetchPolicy: "network-only", nextFetchPolicy: "network-only", - skip: !chatVisible, + notifyOnNetworkStatusChange: true, ...(pollInterval > 0 ? { pollInterval } : {}) }); - // Query for unread count when chat is not visible - const { data: unreadData } = useQuery(UNREAD_CONVERSATION_COUNT, { + // Query for unread count when chat is not visible and socket is not connected. + // (Once socket connects, we stop this query; we keep the last known value in state.) + useQuery(UNREAD_CONVERSATION_COUNT, { fetchPolicy: "network-only", nextFetchPolicy: "network-only", - pollInterval: 60 * 1000 // TODO: This is a fix for now, should be coming from sockets + skip: chatVisible || socket?.connected, + pollInterval: socket?.connected ? 0 : 60 * 1000, + onCompleted: (result) => { + const nextCount = result?.messages_aggregate?.aggregate?.count; + if (typeof nextCount === "number") setUnreadAggregateCount(nextCount); + }, + onError: (err) => { + // Keep last known count; do not force badge to zero on transient failures + console.warn("UNREAD_CONVERSATION_COUNT failed:", err?.message || err); + } }); - // Socket connection status + // Socket connection status -> polling strategy for CONVERSATION_LIST_QUERY useEffect(() => { const handleSocketStatus = () => { if (socket?.connected) { - setPollInterval(15 * 60 * 1000); // 15 minutes + setPollInterval(0); // skip polling if socket connected } else { - setPollInterval(60 * 1000); // 60 seconds + setPollInterval(60 * 1000); // fallback polling if disconnected } }; @@ -71,19 +91,32 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh }; }, [socket]); - // Fetch conversations when chat becomes visible + // Run conversations query exactly once on initial load (component mount) useEffect(() => { - if (chatVisible) - getConversations({ - variables: { - offset: 0 - } - }).catch((err) => { - console.error(`Error fetching conversations: ${(err, err.message || "")}`); - }); - }, [chatVisible, getConversations]); + if (hasLoadedConversationsOnceRef.current) return; - // Get unread count from the cache + hasLoadedConversationsOnceRef.current = true; + + getConversations({ + variables: { offset: 0 } + }).catch((err) => { + console.error(`Error fetching conversations: ${err?.message || ""}`, err); + }); + }, [getConversations]); + + const handleManualRefresh = async () => { + try { + if (called && typeof refetch === "function") { + await refetch({ offset: 0 }); + } else { + await getConversations({ variables: { offset: 0 } }); + } + } catch (err) { + console.error(`Error refreshing conversations: ${err?.message || ""}`, err); + } + }; + + // Get unread count from the cache (preferred). Fallback to preserved aggregate count. const unreadCount = (() => { try { const cachedData = client.readQuery({ @@ -91,18 +124,23 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh variables: { offset: 0 } }); - if (!cachedData?.conversations) { - return unreadData?.messages_aggregate?.aggregate?.count; + const conversations = cachedData?.conversations; + + if (!Array.isArray(conversations) || conversations.length === 0) { + return unreadAggregateCount; } - // Aggregate unread message count - return cachedData.conversations.reduce((total, conversation) => { - const unread = conversation.messages_aggregate?.aggregate?.count || 0; + const hasUnreadCounts = conversations.some((c) => c?.messages_aggregate?.aggregate?.count != null); + if (!hasUnreadCounts) { + return unreadAggregateCount; + } + + return conversations.reduce((total, conversation) => { + const unread = conversation?.messages_aggregate?.aggregate?.count || 0; return total + unread; }, 0); - } catch (error) { - console.warn("Unread count not found in cache:", error); - return 0; // Fallback if not in cache + } catch { + return unreadAggregateCount; } })(); @@ -117,9 +155,12 @@ export function ChatPopupComponent({ chatVisible, selectedConversation, toggleCh - refetch()} /> + + + {!socket?.connected && {t("messaging.labels.nopush")}} + toggleChatVisible()} style={{ position: "absolute", right: ".5rem", top: ".5rem" }}