// State to track the cursor for pagination const [nextCursor, setNextCursor] = React.useState(undefined); // Reset cursor when chat changes React.useEffect(() => { setNextCursor(undefined); }, [selected?.chatId]); console.log("[show.tsx] Current pagination cursor:", nextCursor); // Messages fetching with infinite scroll const { query: { fetchNextPage: fetchNextMsgs, refetch: refetchMsgs }, result: { data: msgsPages, hasNextPage: hasMoreMsgs } } = useInfiniteList({ resource: selected?.chatId && !selected?.chatId?.startsWith("playground_") ? `whatsapp/chats/${selected?.chatId}/messages` : undefined!, meta: { cursor: nextCursor, // Pass the cursor through meta }, liveMode: selected?.chatId && !selected?.chatId?.startsWith("playground_") ? "manual" : "off", liveParams: { channel: user?.company.id, types: ["message", "message_error", "message_status", "message_cancel", "chatUpsert"], }, onLiveEvent: useMemo( () => throttle((event: any) => { console.log("[show.tsx] SSE Event received:", { type: event.type, payload: event.payload, currentChatId: selected?.chatId || chatId, }); // Only process relevant events // Note: liveProvider converts "chatUpsert" to "updated" if ( ![ "message", "message_error", "message_status", "message_cancel", "chatUpsert", "updated", // liveProvider converts chatUpsert to updated ].includes(event.type) ) { console.log("[show.tsx] Event type not in allowed list, skipping:", event.type); return; } // Process live event const eventData = event.payload.data; if (!eventData) { console.log("[show.tsx] No event data, skipping"); return; } // Handle both formats: chatId with or without "CHAT#" prefix const eventChatId = eventData.chatId || eventData.from; const cleanEventChatId = getChatNumber(eventChatId); const currentChatId = selected?.chatId || chatId; console.log("[show.tsx] Chat ID comparison:", { eventChatId, cleanEventChatId, currentChatId, matches: cleanEventChatId === currentChatId, }); // For chatUpsert events, we still want to process them even if they're for different chats // This ensures the chat list gets updated properly if (event.type === "chatUpsert" || event.type === "updated") { if (cleanEventChatId !== currentChatId) { console.log("[show.tsx] ChatUpsert for different chat, processing for session update"); // Still check if session reopened for notification purposes if (eventData.sessionOpen === true) { const chatName = eventData.name || cleanEventChatId; console.log(`[show.tsx] Session reopened for ${chatName}`); // Invalidate chat list to update session status queryClient.invalidateQueries({ queryKey: ["whatsapp", "chats"] }); // Show notification if it's a session reopening const wasClosedBefore = eventData.sessionOpen === false; if (wasClosedBefore) { message.info(`Sessão de 24h reaberta para ${chatName}`); } } return; } } else if (cleanEventChatId !== currentChatId) { // For non-chatUpsert events, skip if not for current chat console.log("[show.tsx] Chat ID mismatch for non-chatUpsert event, skipping"); return; } // Process matching chat event // Handle different event types // Note: "updated" is actually "chatUpsert" converted by liveProvider if ( event.type === "message" || ((event.type === "chatUpsert" || event.type === "updated") && eventData.messageId) ) { console.log("[show.tsx] Processing message event"); // Extract or create message data const messageData = event.type === "message" ? eventData : { messageId: eventData.messageId, text: eventData.lastMessage, timestamp: eventData.timestamp, fromMe: eventData.fromMe, chatId: cleanEventChatId, type: "text", status: "sent", from: eventData.fromMe ? null : cleanEventChatId, ...(eventData.extra || {}), }; console.log("[show.tsx] Message data extracted:", messageData); // Validate message data if (!messageData.messageId) { console.log("[show.tsx] No messageId, skipping"); return; } // Update live messages map (efficient duplicate prevention) setLiveMessages((prev) => { const next = new Map(prev); // Double-check that this message belongs to current chat before adding const msgChatId = getChatNumber( messageData.chatId || messageData.from || "" ); const currentChatId = selected?.chatId || chatId; if (msgChatId !== currentChatId) { console.warn( `[show.tsx] Attempted to add message from wrong chat: ${msgChatId} !== ${currentChatId}` ); return prev; // Don't add the message } console.log("[show.tsx] Adding message to liveMessages:", { messageId: messageData.messageId, text: messageData.text, previousSize: prev.size, newSize: next.size + 1, }); // Check if this message should replace a loading bubble if (messageData.replaceMessageId) { // Remove the loading bubble with the same ID next.delete(messageData.replaceMessageId); } // Limit map size to prevent memory issues (keep last 100 messages) if (next.size >= 100 && !next.has(messageData.messageId)) { const firstKey = next.keys().next().value; if (firstKey) { next.delete(firstKey); } } next.set(messageData.messageId, messageData); return next; }); // Force refetch messages to update the list with new message // This ensures the message appears immediately in the UI console.log("[show.tsx] Forcing refetch after new message"); refetchMsgs(); // Play sound for incoming messages, trigger tab blinking, and mark as read if (!messageData.fromMe) { playSound(); const senderName = selected?.name || getChatNumber(messageData.from || messageData.chatId || ""); startBlinking(`💬 Nova mensagem de ${senderName}`); if (messageData.messageId && chatId) { markMessageAsRead(messageData.messageId); } } // Only refetch if it's been more than 5 seconds since last refetch // This prevents excessive API calls const now = Date.now(); if (!window.lastRefetch || now - window.lastRefetch > 5000) { window.lastRefetch = now; refetchMsgs(); } } else if (event.type === "message_status") { // Update message status in live messages setLiveMessages((prev) => { const next = new Map(prev); const msg = next.get(eventData.messageId); if (msg) { next.set(eventData.messageId, { ...msg, status: eventData.status, }); } return next; }); } else if (event.type === "message_error") { // Handle message errors console.error("[show.tsx] Message error:", eventData); // Could show a toast notification here } else if (event.type === "message_cancel") { // Handle message cancellation console.log("[show.tsx] Message canceled:", eventData.messageId); // Remove the canceled message from live messages setLiveMessages((prev) => { const next = new Map(prev); if (eventData.messageId) { next.delete(eventData.messageId); } return next; }); } else if (event.type === "chatUpsert" || event.type === "updated") { // Handle chatUpsert to update session status and other chat properties console.log("[show.tsx] ChatUpsert received, full eventData:", eventData); console.log("[show.tsx] Current selected state before update:", { chatId: selected?.chatId, sessionOpen: selected?.sessionOpen, sessionExpires: (selected as any)?.sessionExpires, }); // Check if this is for the current chat const eventChatId = getChatNumber(eventData.chatId || ""); const currentChatId = selected?.chatId; if (!selected || eventChatId !== currentChatId) { console.log("[show.tsx] ChatUpsert for different/no chat, ignoring:", { eventChatId, currentChatId, }); return; } // Check if session is reopening BEFORE updating state const wasSessionClosed = !selected.sessionOpen; const isSessionOpening = eventData.sessionOpen === true; console.log("[show.tsx] Session state change check:", { wasSessionClosed, isSessionOpening, prevSessionOpen: selected.sessionOpen, newSessionOpen: eventData.sessionOpen, }); // Update selected chat with new session info setSelected((prev) => { if (!prev) return prev; const updatedChat = { ...prev, sessionOpen: eventData.sessionOpen !== undefined ? eventData.sessionOpen : prev.sessionOpen, sessionExpires: eventData.sessionExpires || (prev as any).sessionExpires, // Also update other fields that might change rating: eventData.rating !== undefined ? eventData.rating : prev.rating, aiEnabled: eventData.aiEnabled !== undefined ? eventData.aiEnabled : prev.aiEnabled, assignedToId: eventData.assignedToId !== undefined ? eventData.assignedToId : prev.assignedToId, assignedTo: eventData.assignedTo !== undefined ? eventData.assignedTo : prev.assignedTo, lastMessage: eventData.lastMessage || prev.lastMessage, unreadCount: eventData.unreadCount !== undefined ? eventData.unreadCount : prev.unreadCount, } as WhatsappBusinessProfile; console.log("[show.tsx] Updated selected chat state:", { chatId: updatedChat.chatId, sessionOpen: updatedChat.sessionOpen, sessionExpires: (updatedChat as any).sessionExpires, }); return updatedChat; }); // Handle session reopening OUTSIDE state update callback if (wasSessionClosed && isSessionOpening) { console.log("[show.tsx] 🎉 Session reopened! Chat is now active for 24h"); // Hide template box since we can now send regular messages setTemplateBoxVisible(false); // Show notification message.success("Sessão de 24h reaberta! Você pode enviar mensagens de texto novamente."); } // Also refetch to ensure consistency refetchMsgs(); } else { // For other unknown events, just refetch console.log("[show.tsx] Unknown event type:", event.type); refetchMsgs(); } }, 100), // Throttle to 100ms [ selected?.chatId, chatId, selected?.name, playSound, startBlinking, markMessageAsRead, ] ), pagination: { pageSize: 25 }, queryOptions: { enabled: !!selected?.chatId, getNextPageParam: (lastPage) => { const next = lastPage?.cursor?.next; console.log("[show.tsx] getNextPageParam called, lastPage cursor:", lastPage); if (next) { setNextCursor(next); // Update the cursor state return JSON.stringify(next); } return undefined; }, }, });