'use client'; import { type RefObject, useEffect, useRef } from 'react'; export interface UseChatHistoryOptions { enabled?: boolean; containerRef: RefObject; topSentinelRef: RefObject; hasMore: boolean; isLoadingMore: boolean; loadMore: () => Promise; } /** Triggers `loadMore` when the top sentinel enters the container's viewport. * Preserves scroll anchor: if the container's height grows after load, we * bump scrollTop by the delta so the previously-visible message stays put. */ export function useChatHistory(options: UseChatHistoryOptions): void { const { enabled = true, containerRef, topSentinelRef, hasMore, isLoadingMore, loadMore } = options; const heightBeforeRef = useRef(null); // Restore anchor after content prepends. useEffect(() => { if (heightBeforeRef.current == null) return; const el = containerRef.current; if (!el) { heightBeforeRef.current = null; return; } if (!isLoadingMore) { const delta = el.scrollHeight - heightBeforeRef.current; if (delta > 0) { el.scrollTop += delta; } heightBeforeRef.current = null; } }, [containerRef, isLoadingMore]); useEffect(() => { if (!enabled || !hasMore) return; const sentinel = topSentinelRef.current; const root = containerRef.current; if (!sentinel || !root) return; const observer = new IntersectionObserver( (entries) => { const entry = entries[0]; if (!entry?.isIntersecting) return; if (isLoadingMore) return; const el = containerRef.current; if (el) heightBeforeRef.current = el.scrollHeight; void loadMore(); }, { root, threshold: 0, rootMargin: '200px 0px 0px 0px' }, ); observer.observe(sentinel); return () => observer.disconnect(); }, [enabled, hasMore, isLoadingMore, containerRef, topSentinelRef, loadMore]); }