import { InfoCircleOutlined, MessageOutlined, ShrinkOutlined, SyncOutlined } from "@ant-design/icons"; import { useApolloClient, useLazyQuery, useQuery } from "@apollo/client/react"; import { Badge, Card, Col, Row, Space, Tag, Tooltip, Typography } from "antd"; import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { connect } from "react-redux"; import { createStructuredSelector } from "reselect"; import { CONVERSATION_LIST_QUERY, UNREAD_CONVERSATION_COUNT } from "../../graphql/conversations.queries"; import { toggleChatVisible } from "../../redux/messaging/messaging.actions"; import { selectChatVisible, selectSelectedConversation } from "../../redux/messaging/messaging.selectors"; import ChatConversationListComponent from "../chat-conversation-list/chat-conversation-list.component"; import ChatConversationContainer from "../chat-conversation/chat-conversation.container"; import ChatNewConversation from "../chat-new-conversation/chat-new-conversation.component"; import LoadingSpinner from "../loading-spinner/loading-spinner.component"; import "./chat-popup.styles.scss"; import { useSocket } from "../../contexts/SocketIO/useSocket.js"; import { selectDarkMode } from "../../redux/application/application.selectors.js"; const mapStateToProps = createStructuredSelector({ selectedConversation: selectSelectedConversation, chatVisible: selectChatVisible, isDarkMode: selectDarkMode }); const mapDispatchToProps = (dispatch) => ({ toggleChatVisible: () => dispatch(toggleChatVisible()) }); export function ChatPopupComponent({ chatVisible, selectedConversation, toggleChatVisible, isDarkMode }) { const { t } = useTranslation(); const { socket } = useSocket(); const client = useApolloClient(); // 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", ...(pollInterval > 0 ? { pollInterval } : {}) }); // 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.) const { data: unreadData, error: unreadError } = useQuery(UNREAD_CONVERSATION_COUNT, { fetchPolicy: "network-only", nextFetchPolicy: "network-only", skip: chatVisible || socket?.connected, pollInterval: socket?.connected ? 0 : 60 * 1000 }); // Handle unread count updates in useEffect useEffect(() => { if (unreadData) { const nextCount = unreadData?.messages_aggregate?.aggregate?.count; if (typeof nextCount === "number") setUnreadAggregateCount(nextCount); } }, [unreadData]); // Handle unread count errors in useEffect useEffect(() => { if (unreadError) { // Keep last known count; do not force badge to zero on transient failures console.warn("UNREAD_CONVERSATION_COUNT failed:", unreadError?.message || unreadError); } }, [unreadError]); // Socket connection status -> polling strategy for CONVERSATION_LIST_QUERY useEffect(() => { const handleSocketStatus = () => { if (socket?.connected) { setPollInterval(0); // skip polling if socket connected } else { setPollInterval(60 * 1000); // fallback polling if disconnected } }; handleSocketStatus(); if (socket) { socket.on("connect", handleSocketStatus); socket.on("disconnect", handleSocketStatus); } return () => { if (socket) { socket.off("connect", handleSocketStatus); socket.off("disconnect", handleSocketStatus); } }; }, [socket]); // Run conversations query exactly once on initial load (component mount) useEffect(() => { if (hasLoadedConversationsOnceRef.current) return; hasLoadedConversationsOnceRef.current = true; getConversations({ variables: { offset: 0 } }).catch((err) => { // Ignore abort errors (they're expected when component unmounts) if (err?.name !== "AbortError") { console.error(`Error fetching conversations: ${err?.message || ""}`, err); } }); }, []); 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({ query: CONVERSATION_LIST_QUERY, variables: { offset: 0 } }); const conversations = cachedData?.conversations; if (!Array.isArray(conversations) || conversations.length === 0) { return unreadAggregateCount; } 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 { return unreadAggregateCount; } })(); return ( {chatVisible ? (
{t("messaging.labels.messaging")} {!socket?.connected && {t("messaging.labels.nopush")}} toggleChatVisible()} style={{ position: "absolute", right: ".5rem", top: ".5rem" }} /> {loading ? ( ) : ( )} {selectedConversation ? : null}
) : (
toggleChatVisible()} style={{ cursor: "pointer" }}> {t("messaging.labels.messaging")}
)}
); } export default connect(mapStateToProps, mapDispatchToProps)(ChatPopupComponent);