diff --git a/apps/desktop/src/pages/ConversationPage.tsx b/apps/desktop/src/pages/ConversationPage.tsx index 9d7351d..e0342f6 100644 --- a/apps/desktop/src/pages/ConversationPage.tsx +++ b/apps/desktop/src/pages/ConversationPage.tsx @@ -1,6 +1,6 @@ import { parseMessagePayload } from '@chat-app/shared/chat'; import { extractErrorCode } from '@chat-app/shared/i18n'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'react-router-dom'; @@ -406,7 +406,14 @@ export function ConversationPage() { if (id && messages.length > 0) markRead(id); }, [id, messages.length, markRead]); - useEffect(() => { + // useLayoutEffect: run synchronously after DOM commit, before the + // browser paints. Using useEffect here let one frame of "scrollTop = 0 + // (top of list)" paint between message-list mount and the auto-scroll, + // which is exactly the "flickers to a different position, then jumps" + // glitch users saw when re-entering a chat. Layout-effect fires while + // the message list is in the DOM but before paint, so the first frame + // already shows the correct scroll position. + useLayoutEffect(() => { const el = scrollRef.current; if (!el || !stickToBottom) return; el.scrollTop = el.scrollHeight; @@ -423,7 +430,13 @@ export function ConversationPage() { const restoredForRef = useRef(null); const isRestoringRef = useRef(false); - useEffect(() => { + // useLayoutEffect, same reason as above: writing scrollTop here happens + // before the first paint of the freshly-mounted chat, so the user + // doesn't see a frame at scrollTop=0 before the jump to the saved + // position. Combined with the messages.length gate this means the + // re-entry shows the message list AT the saved scroll location in one + // single paint — no "loaded then jumped" effect. + useLayoutEffect(() => { const el = scrollRef.current; if (!el || !id) return; if (restoredForRef.current === id) return;