diff --git a/frontend/components/messenger/ChatPanel.tsx b/frontend/components/messenger/ChatPanel.tsx index 6500b8da..edf1d7db 100644 --- a/frontend/components/messenger/ChatPanel.tsx +++ b/frontend/components/messenger/ChatPanel.tsx @@ -26,6 +26,7 @@ export function ChatPanel({ room, messageInputRef, onCaptureClick }: ChatPanelPr const { emitTypingStart, emitTypingStop, typingUsers } = useMessengerSocket(); const scrollRef = useRef(null); const [isAtBottom, setIsAtBottom] = useState(true); + const [scrollReady, setScrollReady] = useState(false); const [isEditingName, setIsEditingName] = useState(false); const [editName, setEditName] = useState(""); const editInputRef = useRef(null); @@ -38,15 +39,25 @@ export function ChatPanel({ room, messageInputRef, onCaptureClick }: ChatPanelPr const lastMessageId = messages?.[messages.length - 1]?.id; + // Hide messages until scrolled to bottom (prevents visible jump on room open) + useEffect(() => { + setScrollReady(false); + }, [selectedRoomId]); + // Scroll to bottom when messages load or room changes // Two-pass: immediate rAF + 600ms delayed (for async image loads) useEffect(() => { - const scrollToBottom = () => { + requestAnimationFrame(() => { + const el = scrollRef.current; + if (el) { + el.scrollTop = el.scrollHeight; + setScrollReady(true); + } + }); + const t = setTimeout(() => { const el = scrollRef.current; if (el) el.scrollTop = el.scrollHeight; - }; - requestAnimationFrame(scrollToBottom); - const t = setTimeout(scrollToBottom, 600); + }, 600); return () => clearTimeout(t); }, [lastMessageId, selectedRoomId]); @@ -162,7 +173,7 @@ export function ChatPanel({ room, messageInputRef, onCaptureClick }: ChatPanelPr {/* Messages */} -
+
{messages?.map((msg, idx) => (