import { useCallback, useEffect, useLayoutEffect, useRef, type RefObject } from 'react';
import { decideScroll, type ScrollDirection } from '../utils/scrollDecision.ts';
/**
* Decide the restore target on a chat switch or load-resolve.
*
* Inputs:
* - nextChatId: chat we are ENTERING (or null if no chat selected)
* - isLoaded: is the next chat's content rendered now?
* - savedScrollTop: previously-saved scrollTop for nextChatId (or undefined)
*
* Output: { restore: 'saved' | 'bottom' | null }
* - restore: 'saved' = write scrollTop = the saved value; 'bottom' = scrollHeight;
* null = do nothing (still loading / deselected).
*
* NOTE: there is deliberately no "save the leaving chat's scrollTop" step here. A layout effect
* runs AFTER React has already swapped the container's content to the NEW chat, so a post-swap
* read captures the NEW content's (possibly clamped) scrollTop, not the leaving chat's position —
* saving then restores the returning chat to the TOP. Instead the scroll listener saves the live
* scrollTop continuously (see below), so the map always holds each chat's last REAL position.
*
* This is a pure function so it can be unit-tested without React.
*/
export interface RestoreDecision {
restore: 'saved' | 'bottom' | null;
}
export function decideRestoreTarget(
nextChatId: string | null,
isLoaded: boolean,
savedScrollTop: number | undefined,
): RestoreDecision {
const restore: 'saved' | 'bottom' | null =
nextChatId !== null && isLoaded ? (savedScrollTop !== undefined ? 'saved' : 'bottom') : null;
return { restore };
}
/**
* Per-chat scroll-position memory + auto-scroll heuristic.
*
* - On chat switch (and once content for the new chat has actually rendered):
* saves the leaving chat's scrollTop, restores the entering chat's saved
* scrollTop, or jumps to bottom on first visit. All synchronously, before
* paint, via useLayoutEffect — no visible "jump" or smooth-scroll animation.
* - The hook depends on BOTH activeChatId AND isLoaded so that a cold-open
* (spinner first, then data) correctly waits to restore until the messages
* list is mounted with non-zero scrollHeight.
* - On message append: `onMessageAppended(direction)` snapshots the geometry
* BEFORE the new message is committed, then defers the scroll-to-bottom (if
* any) to the next frame so the new message is already in the DOM.
* - Pinned-to-bottom: media (`
`/`