import { useDialog } from "@/app/providers/DialogProvider/useDialog"; import { useRosettaColors } from "@/app/hooks/useRosettaColors"; import { Message, MessageSystem } from "./Message"; import React, { useCallback, useEffect, useRef, useState } from "react"; import { usePublicKey } from "@/app/providers/AccountProvider/usePublicKey"; import { MessageSkeleton } from "../MessageSkeleton/MessageSkeleton"; import { ScrollArea } from "@mantine/core"; import { MESSAGE_AVATAR_NO_RENDER_TIME_DIFF_S, SCROLL_TOP_IN_MESSAGES_TO_VIEW_AFFIX } from "@/app/constants"; import { DialogAffix } from "../DialogAffix/DialogAffix"; import { useReplyMessages } from "@/app/providers/DialogProvider/useReplyMessages"; import { useSetting } from "@/app/providers/SettingsProvider/useSetting"; export function Messages() { const colors = useRosettaColors(); const publicKey = usePublicKey(); const { messages, dialog, loadMessagesToTop, loading } = useDialog(); const { replyMessages, isSelectionStarted } = useReplyMessages(); const viewportRef = useRef(null); const lastMessageRef = useRef(null); const contentRef = useRef(null); const shouldAutoScrollRef = useRef(true); const isFirstRenderRef = useRef(true); const previousScrollHeightRef = useRef(0); const [affix, setAffix] = useState(false); const [wallpaper] = useSetting ('wallpaper', ''); const scrollToBottom = useCallback((smooth: boolean = false) => { if (!viewportRef.current) return; requestAnimationFrame(() => { if (!viewportRef.current) return; viewportRef.current.scrollTo({ top: viewportRef.current.scrollHeight, behavior: smooth ? 'smooth' : 'auto' }); }); }, []); // Сброс состояния при смене диалога useEffect(() => { isFirstRenderRef.current = true; shouldAutoScrollRef.current = true; previousScrollHeightRef.current = 0; setAffix(false); }, [dialog]); // IntersectionObserver - отслеживаем видимость последнего сообщения useEffect(() => { if (!lastMessageRef.current || !viewportRef.current || loading) return; const observer = new IntersectionObserver( (entries) => { const entry = entries[0]; //console.info("IntersectionObserver triggered ", entry.isIntersecting); //shouldAutoScrollRef.current = entry.isIntersecting; // Если последнее сообщение видно, скрываем кнопку "вниз" if (entry.isIntersecting) { setAffix(false); } }, { root: viewportRef.current, threshold: 0.1 } ); observer.observe(lastMessageRef.current); return () => observer.disconnect(); }, [messages.length, loading]); // MutationObserver - отслеживаем изменения контента (загрузка картинок, видео) useEffect(() => { if (!contentRef.current) return; const observer = new MutationObserver(() => { // Скроллим только если нужен авто-скролл if (shouldAutoScrollRef.current) { scrollToBottom(true); } }); observer.observe(contentRef.current, { childList: true, subtree: true, attributes: true, attributeFilter: ['src', 'style', 'class'] }); return () => observer.disconnect(); }, [scrollToBottom]); // Первый рендер - скроллим вниз моментально useEffect(() => { if (loading || messages.length === 0) return; if (isFirstRenderRef.current) { scrollToBottom(false); isFirstRenderRef.current = false; } }, [loading, messages.length, scrollToBottom]); useEffect(() => { if(affix){ shouldAutoScrollRef.current = false; } else { shouldAutoScrollRef.current = true; } }, [affix]); // Новое сообщение - скроллим если пользователь внизу или это его сообщение useEffect(() => { if (loading || messages.length === 0 || isFirstRenderRef.current) return; const lastMessage = messages[messages.length - 1]; // Скроллим если пользователь внизу или это его собственное сообщение if ((shouldAutoScrollRef.current || lastMessage.from_me) && !affix) { /** * Скролл только если пользователь не читает сейчас старую переписку * (!affix)) */ //console.info("Scroll because", shouldAutoScrollRef.current); scrollToBottom(true); } }, [messages.length, loading, affix, scrollToBottom]); // Восстановление позиции после загрузки старых сообщений useEffect(() => { if (!viewportRef.current || previousScrollHeightRef.current === 0) return; const scrollDiff = viewportRef.current.scrollHeight - previousScrollHeightRef.current; if (scrollDiff > 0) { viewportRef.current.scrollTop = scrollDiff; previousScrollHeightRef.current = 0; } }, [messages.length]); // Скролл при отправке reply сообщения useEffect(() => { if (replyMessages.messages.length === 0 || isSelectionStarted()) return; scrollToBottom(true); }, [replyMessages.messages.length]); const loadMessagesToScrollAreaTop = async () => { if (!viewportRef.current) return; previousScrollHeightRef.current = viewportRef.current.scrollHeight; await loadMessagesToTop(); }; const onAffixClick = () => { shouldAutoScrollRef.current = true; scrollToBottom(true); }; return ( { if (!viewportRef.current) return; // Загружаем старые сообщения при достижении верха if (scroll.y === 0 && !loading && messages.length >= 20) { loadMessagesToScrollAreaTop(); } // Показываем/скрываем кнопку "вниз" const distanceFromBottom = (viewportRef.current.scrollHeight - viewportRef.current.clientHeight) - scroll.y; setAffix(distanceFromBottom > SCROLL_TOP_IN_MESSAGES_TO_VIEW_AFFIX); }} >
{loading && <> } {!loading && messages.map((message, index) => { const prevMessage = messages[index - 1]; const currentDate = new Date(message.timestamp).toDateString(); const prevDate = prevMessage ? new Date(prevMessage.timestamp).toDateString() : null; const showSystem = prevDate !== currentDate; const isLastMessage = index === messages.length - 1; const isLastMessageInStack = isLastMessage || messages[index + 1].from_public_key !== message.from_public_key || (new Date(messages[index + 1].timestamp).toDateString() !== new Date(message.timestamp).toDateString()) || (messages[index + 1].timestamp - message.timestamp) >= (MESSAGE_AVATAR_NO_RENDER_TIME_DIFF_S * 1000) || (messages[index + 1].plain_message == "$a=Group created" || messages[index + 1].plain_message == "$a=Group joined"); return ( {showSystem && ( { const messageDate = new Date(message.timestamp); const today = new Date(); const yesterday = new Date(); yesterday.setDate(today.getDate() - 1); const isToday = messageDate.toDateString() === today.toDateString(); const isYesterday = messageDate.toDateString() === yesterday.toDateString(); if (isToday) return "today"; if (isYesterday) return "yesterday"; return messageDate.toLocaleDateString('en-EN', { day: 'numeric', month: 'long', year: 'numeric' }); })() } /> )} {index > 0 && messages[index - 1].readed == 1 && message.readed == 0 && publicKey != message.from_public_key && ( ) }
{message.plain_message != "$a=Group created" && message.plain_message != "$a=Group joined" && ( 0 && messages[index - 1].from_public_key == message.from_public_key && (new Date(messages[index - 1].timestamp).toDateString() == new Date(message.timestamp).toDateString()) && (message.timestamp - messages[index - 1].timestamp) < (MESSAGE_AVATAR_NO_RENDER_TIME_DIFF_S * 1000) && (messages[index - 1].plain_message != "$a=Group created" && messages[index - 1].plain_message != "$a=Group joined") } /> )} {message.plain_message == "$a=Group created" && ( )} {message.plain_message == "$a=Group joined" && ( )}
); })}
) }