diff --git a/apps/desktop/src/pages/ConversationPage.tsx b/apps/desktop/src/pages/ConversationPage.tsx index 72aa4bf..44b4977 100644 --- a/apps/desktop/src/pages/ConversationPage.tsx +++ b/apps/desktop/src/pages/ConversationPage.tsx @@ -84,7 +84,7 @@ import { clearDraft, getDraftSync, setDraft } from '../lib/composerDraftStore'; // pending bubbles and the "load older" tile inside the same Virtuoso // instance means scroll-to-bottom / followOutput stay coherent across both // (we don't need a sibling scroll container for pending items). -type VirtuosoRow = +export type VirtuosoRow = | { kind: 'loader'; key: string } | { kind: 'message'; key: string; message: DecryptedMessage; idx: number } | { kind: 'pending'; key: string; item: OutboxItem }; @@ -110,6 +110,21 @@ const EMPTY_REACTIONS: AggregatedReaction[] = []; // reading position even if some rows above re-render at different heights. const scrollPositions = new Map(); +// ── TEMP scroll-jank instrumentation ────────────────────────────────────── +// Flip SCROLL_DEBUG to false or delete this block once the chat-switch flicker +// is root-caused. Logs go to the devtools console AND window.__scrolllog (copy +// the whole run with: copy(window.__scrolllog.join('\n')) ). +const SCROLL_DEBUG = true; +const dbgNow = (): number => Math.round(performance.now()); +const dbgLog = (line: string): void => { + if (!SCROLL_DEBUG) return; + const w = window as unknown as { __scrolllog?: string[] }; + (w.__scrolllog ??= []).push(line); + // eslint-disable-next-line no-console + console.log(line); +}; +// ────────────────────────────────────────────────────────────────────────── + /** Pending composer attachment: the raw File plus the per-attachment * view-once flag the user can toggle from the thumb hover button (P7.T4). * Lives only in composer state — the flag is forwarded into @@ -496,6 +511,18 @@ export function ConversationPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [virtuosoRows.length > 0]); + // SCROLL_DEBUG: log every render with the height-affecting inputs so the + // post-mount cascade (reactions/pins/receipts/divider/refresh) is visible. + useEffect(() => { + dbgLog( + `[scroll] render t=${dbgNow()} id=${(id ?? '').slice(0, 6)} loading=${loading}` + + ` msgs=${messages.length} rows=${virtuosoRows.length}` + + ` reactions=${reactionsByMessage.size} pins=${pins.length}` + + ` firstUnread=${firstUnreadId ? 'set' : '-'}` + + ` displayCount=${displayCount} initIdx=${JSON.stringify(initialTopMostIndex)}`, + ); + }); + const jumpToMessage = useCallback( (targetId: string) => { const msgIdx = messages.findIndex((m) => m.id === targetId); @@ -702,6 +729,7 @@ export function ConversationPage() { // away counter when the user actually reaches the bottom. const handleAtBottomStateChange = useCallback( (atBottom: boolean) => { + dbgLog(`[scroll] atBottom=${atBottom} t=${dbgNow()}`); setStickToBottom(atBottom); if (atBottom) setNewMessagesWhileAway(0); if (id) { @@ -720,6 +748,7 @@ export function ConversationPage() { // we only care about the start of the range here. const handleRangeChanged = useCallback( (range: { startIndex: number; endIndex: number }) => { + dbgLog(`[scroll] range t=${dbgNow()} start=${range.startIndex} end=${range.endIndex}`); topmostIndexRef.current = range.startIndex; if (id) { const prev = scrollPositions.get(id);