/* eslint-disable @typescript-eslint/no-explicit-any */ import { useCallback, useEffect, useState } from "react"; import { useGetConversations } from "../../hooks/queries/useChatApi"; import { useChatContext } from "../../providers/ChatProvider"; import { Conversation as ConversationType, ParticipantGroup, } from "../../types/type"; import CollapsibleSection from "../common/CollapsibleSection"; import useChatUIStore from "../../stores/Zustant"; import VirtualizedChatList from "../common/VirtualizedChatList"; type GroupedServiceChats = { [serviceId: string]: { serviceTitle: string; conversations: ConversationType[]; }; }; type TabType = "personal" | "service"; const Conversations = () => { const { userId, socket } = useChatContext(); const { data: participantGroups } = useGetConversations(userId); const { searchTerm, lastSentMessage, setLastSentMessage } = useChatUIStore(); const [activeTab, setActiveTab] = useState("personal"); const [conversations, setConversations] = useState<{ personalChats: ConversationType[]; groupedServiceChats: GroupedServiceChats; }>({ personalChats: [], groupedServiceChats: {} }); // Process fetched data useEffect(() => { if (!participantGroups) return; const processConversations = (groups: ParticipantGroup[]) => { const allPersonalChats: ConversationType[] = []; const allServiceChatsMap = new Map(); groups.forEach((group) => { if (group.personalConversation) { allPersonalChats.push(group.personalConversation); } group.serviceConversations.forEach((serviceGroup) => { if (!allServiceChatsMap.has(serviceGroup.serviceId)) { allServiceChatsMap.set(serviceGroup.serviceId, { serviceTitle: serviceGroup.serviceTitle, conversations: [], }); } const existing = allServiceChatsMap.get(serviceGroup.serviceId)!; existing.conversations.push(...serviceGroup.conversations); }); }); const sortConversations = (convos: ConversationType[]) => convos.sort( (a, b) => new Date(b.lastMessage?.createdAt || b.updatedAt).getTime() - new Date(a.lastMessage?.createdAt || a.updatedAt).getTime() ); sortConversations(allPersonalChats); allServiceChatsMap.forEach((value) => { sortConversations(value.conversations); }); const result = { personalChats: allPersonalChats, groupedServiceChats: Object.fromEntries(allServiceChatsMap), }; return result; }; setConversations(processConversations(participantGroups)); }, [participantGroups]); // ── Hoisted to component scope so both the socket listener AND the // lastSentMessage effect can call it ───────────────────────────── const handleNewMessage = useCallback((newMessage: any) => { if (!newMessage?.conversationId) return; setConversations((prev) => { const personalChats = [...prev.personalChats]; const groupedServiceChats = { ...prev.groupedServiceChats }; const updateConversation = (convo: ConversationType) => ({ ...convo, lastMessage: newMessage, updatedAt: new Date().toISOString(), unreadMessageIds: userId !== newMessage.senderId ? [...(convo.unreadMessageIds || []), newMessage._id] : convo.unreadMessageIds || [], unreadMessageCount: userId !== newMessage.senderId ? (convo.unreadMessageCount || 0) + 1 : convo.unreadMessageCount || 0, }); const personalIndex = personalChats.findIndex( (c) => c._id === newMessage.conversationId ); if (personalIndex >= 0) { personalChats[personalIndex] = updateConversation(personalChats[personalIndex]); return { personalChats, groupedServiceChats }; } for (const serviceId in groupedServiceChats) { const serviceIndex = groupedServiceChats[serviceId].conversations.findIndex( (c) => c._id === newMessage.conversationId ); if (serviceIndex >= 0) { const updatedConversations = [...groupedServiceChats[serviceId].conversations]; updatedConversations[serviceIndex] = updateConversation(updatedConversations[serviceIndex]); groupedServiceChats[serviceId] = { ...groupedServiceChats[serviceId], conversations: updatedConversations, }; return { personalChats, groupedServiceChats }; } } // Fallback: brand-new conversation not yet in local state personalChats.push({ _id: newMessage.conversationId, participants: [newMessage.senderId, newMessage.receiverId], lastMessage: newMessage, updatedAt: new Date().toISOString(), unreadMessageIds: userId !== newMessage.senderId ? [newMessage._id] : [], unreadMessageCount: userId !== newMessage.senderId ? 1 : 0, type: "personal", createdAt: new Date().toISOString(), } as any); return { personalChats, groupedServiceChats }; }); }, [userId]); // Real-time update listeners useEffect(() => { if (!socket) return; const handleMessageReadAck = (data: { messageIds: string[]; chatId: string; }) => { const { chatId, messageIds } = data; setConversations((prev) => { const personalChats = [...prev.personalChats]; const groupedServiceChats = { ...prev.groupedServiceChats }; const updateRead = (convo: ConversationType) => { if (convo._id !== chatId) return convo; const updatedUnread = (convo.unreadMessageIds || []).filter( (id) => !messageIds.includes(id) ); return { ...convo, unreadMessageIds: updatedUnread, unreadMessageCount: updatedUnread.length, }; }; const personalIndex = personalChats.findIndex((c) => c._id === chatId); if (personalIndex >= 0) { personalChats[personalIndex] = updateRead( personalChats[personalIndex] ); return { personalChats, groupedServiceChats }; } for (const serviceId in groupedServiceChats) { const convos = groupedServiceChats[serviceId].conversations; const convoIndex = convos.findIndex((c) => c._id === chatId); if (convoIndex >= 0) { const updatedConvos = [...convos]; updatedConvos[convoIndex] = updateRead(updatedConvos[convoIndex]); groupedServiceChats[serviceId] = { ...groupedServiceChats[serviceId], conversations: updatedConvos, }; return { personalChats, groupedServiceChats }; } } return prev; }); }; const messageListener = (event: MessageEvent) => { try { const parsed = JSON.parse(event.data); if (parsed.event === "newMessage") { // Actual message is nested at parsed.data.data — same structure as Messages.tsx handleNewMessage(parsed?.data?.data); } else if (parsed.event === "messageStatusUpdated" && parsed.data?.status === "read") { const rawId = parsed.data.messageId ?? parsed.data.messageIds; handleMessageReadAck({ messageIds: Array.isArray(rawId) ? rawId : [rawId].filter(Boolean), chatId: parsed.data.chatId, }); } } catch (e) { console.error("Error parsing socket message:", e); } }; socket.addEventListener("message", messageListener); return () => socket.removeEventListener("message", messageListener); }, [socket, handleNewMessage]); // React to messages sent by the current user (no server echo needed) useEffect(() => { if (!lastSentMessage) return; handleNewMessage(lastSentMessage); setLastSentMessage(null); }, [lastSentMessage, handleNewMessage, setLastSentMessage]); // const isEmpty = // activeTab === "personal" // ? conversations.personalChats.length === 0 // : Object.keys(conversations.groupedServiceChats).length === 0; const lowerSearch = searchTerm?.toLowerCase().trim(); // Filter personal chats const filteredPersonalChats = !lowerSearch ? conversations.personalChats : conversations.personalChats.filter((convo) => { const details = Array.isArray(convo.participantDetails) ? convo.participantDetails : [convo.participantDetails].filter(Boolean); return details.some((p: any) => p?.name?.toLowerCase().includes(lowerSearch)); }); // Filter service chats const filteredGroupedServiceChats: GroupedServiceChats = !lowerSearch ? conversations.groupedServiceChats : Object.fromEntries( Object.entries(conversations.groupedServiceChats) .map(([serviceId, group]) => { const details = (convo: ConversationType) => { const d = Array.isArray(convo.participantDetails) ? convo.participantDetails : [convo.participantDetails].filter(Boolean); return d.some((p: any) => p?.name?.toLowerCase().includes(lowerSearch)); }; const filteredConvos = group.conversations.filter( (convo) => details(convo) || group.serviceTitle?.toLowerCase().includes(lowerSearch) || convo.serviceTitle?.toLowerCase().includes(lowerSearch) ); return [serviceId, { ...group, conversations: filteredConvos }] as [string, GroupedServiceChats[string]]; }) .filter(([, group]) => group.conversations.length > 0) ); // Improved empty state logic const showPersonalTab = activeTab === "personal" && filteredPersonalChats.length > 0; const showServiceTab = activeTab === "service" && Object.keys(filteredGroupedServiceChats).length > 0; const isEmpty = !showPersonalTab && !showServiceTab; // Calculate unread counts for tabs const personalUnreadCount = conversations.personalChats.reduce( (total, convo) => total + (convo.unreadMessageCount || 0), 0 ); const serviceUnreadCount = Object.values(conversations.groupedServiceChats) .flatMap((group) => group.conversations) .reduce((total, convo) => total + (convo.unreadMessageCount || 0), 0); return (
{isEmpty ? (

No conversations

{activeTab === "personal" ? "You have no personal messages yet." : "You have no service messages yet."}

) : ( <> {activeTab === "personal" && filteredPersonalChats.length > 0 && ( )} {activeTab === "service" && Object.entries(filteredGroupedServiceChats).length > 0 && Object.entries(filteredGroupedServiceChats).map( ([serviceId, { serviceTitle, conversations: serviceConvos }]) => ( ) )} )}
); }; export default Conversations;