diff --git a/frontend/components/messenger/ChatPanel.tsx b/frontend/components/messenger/ChatPanel.tsx index 8b7543ec..b223ba3a 100644 --- a/frontend/components/messenger/ChatPanel.tsx +++ b/frontend/components/messenger/ChatPanel.tsx @@ -37,23 +37,40 @@ export function ChatPanel({ room }: ChatPanelProps) { const lastMessageId = messages?.[messages.length - 1]?.id; // Scroll to bottom: sentinel scrollIntoView before paint (no visible jump) + // Scroll to bottom on room open / new message useLayoutEffect(() => { if (isOpen) bottomRef.current?.scrollIntoView(); }, [selectedRoomId, lastMessageId, isOpen]); - // Second pass for async image loads + // ResizeObserver: re-scroll whenever content height changes (images loading, etc.) + const shouldAutoScrollRef = useRef(true); useEffect(() => { - if (!isOpen) return; - const t = setTimeout(() => { bottomRef.current?.scrollIntoView(); }, 600); - return () => clearTimeout(t); - }, [lastMessageId, selectedRoomId, isOpen]); + const el = scrollRef.current; + if (!el) return; + const inner = el.firstElementChild as HTMLElement | null; + if (!inner) return; + const ro = new ResizeObserver(() => { + if (shouldAutoScrollRef.current) { + bottomRef.current?.scrollIntoView(); + } + }); + ro.observe(inner); + return () => ro.disconnect(); + }, [selectedRoomId]); + + // Track whether user has scrolled up (disable auto-scroll while reading old messages) + useEffect(() => { + shouldAutoScrollRef.current = true; + }, [selectedRoomId, lastMessageId]); // Re-attach scroll listener whenever room changes (scrollRef mounts after room is set) useEffect(() => { const el = scrollRef.current; if (!el) return; const onScroll = () => { - setIsAtBottom(el.scrollHeight - el.scrollTop - el.clientHeight < 60); + const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 60; + setIsAtBottom(atBottom); + shouldAutoScrollRef.current = atBottom; }; el.addEventListener("scroll", onScroll); return () => el.removeEventListener("scroll", onScroll);