import React, { useCallback, useState, useEffect, useRef, useMemo, } from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; import { MessageItem } from "./MessageItem"; import { subscribeToEvent, unsubscribeFromEvent } from "../../utils/events"; import { useInView } from "react-intersection-observer"; import { Box, Typography } from "@mui/material"; import { ChatOptions } from "./ChatOptions"; import ErrorBoundary from "../../common/ErrorBoundary"; export const ChatList = ({ initialMessages, myAddress, tempMessages, chatId, onReply, onEdit, handleReaction, chatReferences, tempChatReferences, members, myName, selectedGroup, enableMentions, openQManager, hasSecretKey, isPrivate }) => { const parentRef = useRef(); const [messages, setMessages] = useState(initialMessages); const [showScrollButton, setShowScrollButton] = useState(false); const [showScrollDownButton, setShowScrollDownButton] = useState(false); const hasLoadedInitialRef = useRef(false); const scrollingIntervalRef = useRef(null); const lastSeenUnreadMessageTimestamp = useRef(null); // Initialize the virtualizer const rowVirtualizer = useVirtualizer({ count: messages.length, getItemKey: (index) => messages[index]?.tempSignature || messages[index].signature, getScrollElement: () => parentRef?.current, estimateSize: useCallback(() => 80, []), // Provide an estimated height of items, adjust this as needed overscan: 10, // Number of items to render outside the visible area to improve smoothness }); const isAtBottom = useMemo(()=> { if (parentRef.current && rowVirtualizer?.isScrolling !== undefined) { const { scrollTop, scrollHeight, clientHeight } = parentRef.current; const atBottom = scrollTop + clientHeight >= scrollHeight - 10; // Adjust threshold as needed return atBottom } return false }, [rowVirtualizer?.isScrolling]) useEffect(() => { if (!parentRef.current || rowVirtualizer?.isScrolling === undefined) return; if(isAtBottom){ if (scrollingIntervalRef.current) { clearTimeout(scrollingIntervalRef.current); } setShowScrollDownButton(false); return; } else if (rowVirtualizer?.isScrolling) { if (scrollingIntervalRef.current) { clearTimeout(scrollingIntervalRef.current); } setShowScrollDownButton(false); return; } const { scrollTop, scrollHeight, clientHeight } = parentRef.current; const atBottom = scrollHeight - scrollTop - clientHeight <= 300; if (!atBottom) { scrollingIntervalRef.current = setTimeout(() => { setShowScrollDownButton(true); }, 250); } else { setShowScrollDownButton(false); } }, [rowVirtualizer?.isScrolling, isAtBottom]); // Update message list with unique signatures and tempMessages useEffect(() => { let uniqueInitialMessagesMap = new Map(); // Only add a message if it doesn't already exist in the Map initialMessages.forEach((message) => { if (!uniqueInitialMessagesMap.has(message.signature)) { uniqueInitialMessagesMap.set(message.signature, message); } }); const uniqueInitialMessages = Array.from( uniqueInitialMessagesMap.values() ).sort((a, b) => a.timestamp - b.timestamp); const totalMessages = [...uniqueInitialMessages, ...(tempMessages || [])]; if (totalMessages.length === 0) return; setMessages(totalMessages); setTimeout(() => { const hasUnreadMessages = totalMessages.some( (msg) => msg.unread && !msg?.chatReference && !msg?.isTemp && (!msg?.chatReference && msg?.timestamp > lastSeenUnreadMessageTimestamp.current || 0) ); if (parentRef.current) { const { scrollTop, scrollHeight, clientHeight } = parentRef.current; const atBottom = scrollTop + clientHeight >= scrollHeight - 10; // Adjust threshold as needed if (!atBottom && hasUnreadMessages) { setShowScrollButton(hasUnreadMessages); setShowScrollDownButton(false); } else { handleMessageSeen(); } } if (!hasLoadedInitialRef.current) { const findDivideIndex = totalMessages.findIndex( (item) => !!item?.divide ); const divideIndex = findDivideIndex !== -1 ? findDivideIndex : undefined; scrollToBottom(totalMessages, divideIndex); hasLoadedInitialRef.current = true; } }, 500); }, [initialMessages, tempMessages]); const scrollToBottom = (initialMsgs, divideIndex) => { const index = initialMsgs ? initialMsgs.length - 1 : messages.length - 1; if (rowVirtualizer) { if (divideIndex) { rowVirtualizer.scrollToIndex(divideIndex, { align: "start" }); } else { rowVirtualizer.scrollToIndex(index, { align: "end" }); } } handleMessageSeen(); }; const handleMessageSeen = useCallback(() => { setMessages((prevMessages) => prevMessages.map((msg) => ({ ...msg, unread: false, })) ); setShowScrollButton(false); lastSeenUnreadMessageTimestamp.current = Date.now() }, []); const sentNewMessageGroupFunc = useCallback(() => { const { scrollHeight, scrollTop, clientHeight } = parentRef.current; // Check if the user is within 200px from the bottom const distanceFromBottom = scrollHeight - scrollTop - clientHeight; if (distanceFromBottom <= 700) { scrollToBottom(); } }, [messages]); useEffect(() => { subscribeToEvent("sent-new-message-group", sentNewMessageGroupFunc); return () => { unsubscribeFromEvent("sent-new-message-group", sentNewMessageGroupFunc); }; }, [sentNewMessageGroupFunc]); const lastSignature = useMemo(() => { if (!messages || messages?.length === 0) return null; const lastIndex = messages.length - 1; return messages[lastIndex]?.signature; }, [messages]); const goToMessage = useCallback((idx) => { rowVirtualizer.scrollToIndex(idx); }, []); return (
{rowVirtualizer.getVirtualItems().map((virtualRow) => { const index = virtualRow.index; let message = messages[index] || null; // Safeguard against undefined let replyIndex = -1; let reply = null; let reactions = null; let isUpdating = false; try { // Safeguard for message existence if (message) { // Check for repliedTo logic replyIndex = messages.findIndex( (msg) => msg?.signature === message?.repliedTo ); if (message?.repliedTo && replyIndex !== -1) { reply = { ...(messages[replyIndex] || {}) }; if (chatReferences?.[reply?.signature]?.edit) { reply.decryptedData = chatReferences[reply?.signature]?.edit; reply.text = chatReferences[reply?.signature]?.edit?.message; } } // GroupDirectId logic if (message?.message && message?.groupDirectId) { replyIndex = messages.findIndex( (msg) => msg?.signature === message?.message?.repliedTo ); if (message?.message?.repliedTo && replyIndex !== -1) { reply = messages[replyIndex] || null; } message = { ...(message?.message || {}), isTemp: true, unread: false, status: message?.status, }; } // Check for reactions and edits if (chatReferences?.[message.signature]) { reactions = chatReferences[message.signature]?.reactions || null; if (chatReferences[message.signature]?.edit?.message && message?.text) { message.text = chatReferences[message.signature]?.edit?.message; message.isEdit = true message.editTimestamp = chatReferences[message.signature]?.edit?.timestamp } if (chatReferences[message.signature]?.edit?.messageText && message?.messageText) { message.messageText = chatReferences[message.signature]?.edit?.messageText; message.isEdit = true message.editTimestamp = chatReferences[message.signature]?.edit?.timestamp } if (chatReferences[message.signature]?.edit?.images) { message.images = chatReferences[message.signature]?.edit?.images; message.isEdit = true; } } // Check if message is updating if ( tempChatReferences?.some( (item) => item?.chatReference === message?.signature ) ) { isUpdating = true; } } } catch (error) { console.error("Error processing message:", error, { index, message }); // Gracefully handle the error by providing fallback data message = null; reply = null; reactions = null; } // Render fallback if message is null if (!message) { return (
Error loading message.
); } return (
Error loading content: Invalid Data } >
); })}
{showScrollButton && ( )} {showScrollDownButton && !showScrollButton && ( )}
{enableMentions && (hasSecretKey || isPrivate === false) && ( )}
); };