/* eslint-disable @typescript-eslint/no-explicit-any */ import { useEffect, useLayoutEffect, useRef } from "react"; import Message from "./Message"; import { useChatContext } from "../../providers/ChatProvider"; import useChatUIStore from "../../stores/Zustant"; import { useInfiniteQuery } from "@tanstack/react-query"; import { fetchMessages } from "../../service/messageService"; import { useInView } from "react-intersection-observer"; import Loader from "../Loader"; import SystemNotice from "../common/SystemNotice"; const Messages = () => { const { selectedConversation, setMessages, messages } = useChatUIStore(); const { userId, socket } = useChatContext(); const { ref, inView } = useInView(); const chatContainerRef = useRef(null); const prevScrollHeightRef = useRef(0); const prevIsFetchingRef = useRef(false); const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({ queryKey: ["messages", selectedConversation?._id, userId], queryFn: ({ pageParam = 1 }) => fetchMessages(selectedConversation?._id, userId, pageParam), getNextPageParam: (lastPage) => lastPage.nextPage, initialPageParam: 1, enabled: !!selectedConversation?._id, // Prevent fetching if no conversation is selected }); // Handle infinite scroll useEffect(() => { if (inView) fetchNextPage(); }, [fetchNextPage, inView]); // Save scroll height before older messages are fetched useEffect(() => { if (isFetchingNextPage && !prevIsFetchingRef.current) { prevScrollHeightRef.current = chatContainerRef.current?.scrollHeight ?? 0; } prevIsFetchingRef.current = isFetchingNextPage; }, [isFetchingNextPage]); // Restore scroll position after older messages are rendered useLayoutEffect(() => { if (!isFetchingNextPage && prevScrollHeightRef.current > 0) { const container = chatContainerRef.current; if (container && container.scrollHeight > prevScrollHeightRef.current) { container.scrollTop = container.scrollHeight - prevScrollHeightRef.current; prevScrollHeightRef.current = 0; } } }, [messages, isFetchingNextPage]); // Update messages when data changes useEffect(() => { if (data) { const allMessages = data.pages.flatMap((page) => page.messages).reverse(); setMessages(allMessages); } }, [data, setMessages]); // Listen for WebSocket messages useEffect(() => { if (!socket || !selectedConversation?._id) return; const handleMessage = (event: MessageEvent) => { try { const parsed = JSON.parse(event.data); // if (parsed.type === "newMessage" || parsed.event === "newMessage") { // const newMessage = parsed.data; // if (!newMessage) { // // console.warn( // // "Received newMessage event without a message payload", // // parsed // // ); // return; // } // newMessage.shouldShake = true; // setMessages((prevMessages) => { // const isDuplicate = prevMessages.some( // (msg) => // msg._id === newMessage._id || // (msg.isOptimistic && // msg.sender.senderId === userId && // msg.message === newMessage.message) // ); // return isDuplicate ? prevMessages : [...prevMessages, newMessage]; // }); // } if (parsed.event === "newMessage" || parsed.type === "newMessage") { const newMessage = parsed?.data?.data; if (!newMessage) return; const incomingConversationId = newMessage.conversationId; const currentConversationId = selectedConversation?._id; // ✅ if message is for another conversation, DO NOT append here if ( !currentConversationId || incomingConversationId !== currentConversationId ) { return; } newMessage.shouldShake = true; setMessages((prevMessages) => { const isDuplicate = prevMessages.some( (msg: any) => msg._id === newMessage._id || (msg.isOptimistic && msg.sender?.senderId === userId && msg.message === newMessage.message), ); return isDuplicate ? prevMessages : [...prevMessages, newMessage]; }); return; } const statusOrder = ["sent", "delivered", "read", "edited", "deleted"]; if (parsed.event === "messageStatusUpdated") { const { messageId, status } = parsed.data || {}; if (!messageId) { // console.error("Missing messageId in status update", parsed); return; } setMessages((prev) => prev.map((msg) => { if (msg._id !== messageId) return msg; const currentIdx = statusOrder.indexOf(msg.status); const newIdx = statusOrder.indexOf(status); if (newIdx === -1 || currentIdx === -1 || newIdx <= currentIdx) return msg; if (newIdx > currentIdx) { // console.log(`Updating status for ${messageId} to ${status}`); return { ...msg, status }; } return msg; // No update if new status isn't higher }), ); } if (parsed.event === "messageEdited") { // console.log("Received messageEdited event:", parsed); const updatedMessage = parsed.data; if (!updatedMessage || !updatedMessage.messageId) return; setMessages((prevMessages) => prevMessages.map((msg) => msg._id === updatedMessage.messageId ? { ...msg, message: updatedMessage.message, status: "edited" } : msg, ), ); } if (parsed.event === "messageDeleted") { // console.log("Received messageDeleted event:", parsed); const { messageId } = parsed.data || {}; if (!messageId) return; setMessages((prevMessages) => prevMessages.map((msg) => msg._id === messageId ? { ...msg, message: "This message was deleted", status: "deleted", } : msg, ), ); } } catch (error) { console.error("Error parsing WebSocket message:", error); } }; socket.addEventListener("message", handleMessage); return () => { socket.removeEventListener("message", handleMessage); }; }, [socket, selectedConversation?._id, setMessages, userId]); // Scroll to bottom when a new message is appended (sender or receiver) const lastMessageId = messages[messages.length - 1]?._id; useEffect(() => { if (!lastMessageId) return; const timer = setTimeout(() => { const container = chatContainerRef.current; if (container) { container.scrollTo({ top: container.scrollHeight, behavior: "smooth", }); } }, 100); return () => clearTimeout(timer); }, [lastMessageId]); // Track message visibility for read receipts // useEffect(() => { // if (!socket || !messages.length) return; // const observer = new IntersectionObserver( // (entries) => { // entries.forEach((entry) => { // if (entry.isIntersecting) { // const messageId = entry.target.getAttribute("data-message-id"); // if (messageId) { // sendDeliveryConfirmation(messageId); // } // } // }); // }, // { threshold: 0.5 } // ); // const messageElements = document.querySelectorAll("[data-message-id]"); // messageElements.forEach((el) => observer.observe(el)); // return () => observer.disconnect(); // }, [messages, socket]); return (
{isFetchingNextPage ? : null}
{messages?.length > 0 ? ( messages.map((message: any, idx: number) => { if (!message) return null; const divider = dividerLabel(message, messages[idx - 1]); return (
{divider &&
{divider}
}
); }) ) : (

Send a message to start the conversation

)}
); }; const isSameDay = (a: Date, b: Date) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); /** * Label for the divider above a message, or null when it shares a day with * the message before it. */ const dividerLabel = (message: any, previous: any): string | null => { const at = new Date(message?.createdAt); if (Number.isNaN(at.getTime())) return null; const prevAt = previous ? new Date(previous.createdAt) : null; if (prevAt && !Number.isNaN(prevAt.getTime()) && isSameDay(at, prevAt)) return null; const now = new Date(); if (isSameDay(at, now)) return "Today"; const yesterday = new Date(now); yesterday.setDate(now.getDate() - 1); if (isSameDay(at, yesterday)) return "Yesterday"; return at.toLocaleDateString([], { month: "short", day: "numeric", year: "numeric" }); }; export default Messages;