({
virtuosoRef,
scrollContainerRef,
taskTs,
isStreaming,
isHidden,
hasTask,
}: UseScrollLifecycleOptions)
| 73 | // --------------------------------------------------------------------------- |
| 74 | |
| 75 | export function useScrollLifecycle({ |
| 76 | virtuosoRef, |
| 77 | scrollContainerRef, |
| 78 | taskTs, |
| 79 | isStreaming, |
| 80 | isHidden, |
| 81 | hasTask, |
| 82 | }: UseScrollLifecycleOptions): UseScrollLifecycleReturn { |
| 83 | // --- Mounted guard --- |
| 84 | const isMountedRef = useRef(true) |
| 85 | |
| 86 | // --- Phase state --- |
| 87 | const [scrollPhase, setScrollPhase] = useState<ScrollPhase>("USER_BROWSING_HISTORY") |
| 88 | const scrollPhaseRef = useRef<ScrollPhase>("USER_BROWSING_HISTORY") |
| 89 | |
| 90 | // --- Visibility state --- |
| 91 | const [showScrollToBottom, setShowScrollToBottom] = useState(false) |
| 92 | |
| 93 | // --- Bottom detection --- |
| 94 | const isAtBottomRef = useRef(false) |
| 95 | |
| 96 | // --- Hydration window --- |
| 97 | const isHydratingRef = useRef(false) |
| 98 | const hydrationTimeoutRef = useRef<number | null>(null) |
| 99 | const hydrationRetryUsedRef = useRef(false) |
| 100 | |
| 101 | // --- Pointer scroll tracking --- |
| 102 | const pointerScrollActiveRef = useRef(false) |
| 103 | const pointerScrollElementRef = useRef<HTMLElement | null>(null) |
| 104 | const pointerScrollLastTopRef = useRef<number | null>(null) |
| 105 | |
| 106 | // --- Re-anchor frame --- |
| 107 | const reanchorAnimationFrameRef = useRef<number | null>(null) |
| 108 | |
| 109 | // ----------------------------------------------------------------------- |
| 110 | // Phase transitions |
| 111 | // ----------------------------------------------------------------------- |
| 112 | |
| 113 | const transitionScrollPhase = useCallback((nextPhase: ScrollPhase) => { |
| 114 | if (scrollPhaseRef.current === nextPhase) { |
| 115 | return |
| 116 | } |
| 117 | scrollPhaseRef.current = nextPhase |
| 118 | setScrollPhase(nextPhase) |
| 119 | }, []) |
| 120 | |
| 121 | const enterAnchoredFollowing = useCallback(() => { |
| 122 | transitionScrollPhase("ANCHORED_FOLLOWING") |
| 123 | setShowScrollToBottom(false) |
| 124 | }, [transitionScrollPhase]) |
| 125 | |
| 126 | const enterUserBrowsingHistory = useCallback( |
| 127 | (_source: ScrollFollowDisengageSource) => { |
| 128 | transitionScrollPhase("USER_BROWSING_HISTORY") |
| 129 | // Always show the scroll-to-bottom CTA when the user explicitly |
| 130 | // disengages. If they happen to still be at the physical bottom, |
| 131 | // the next Virtuoso atBottomStateChange(true) will hide it. |
| 132 | setShowScrollToBottom(true) |
no test coverage detected