From dd2d3cc3f6122735f299e97c04265ac7abc78cf1 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Thu, 9 Jul 2026 15:22:40 -0400 Subject: [PATCH 01/38] feat(chat): re-add native LegendList for the message thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the inverted FlatList + KeyboardChatScrollView native thread with a non-inverted KeyboardAwareLegendList, on top of the list-agnostic row fixes in the parent commit. - non-inverted list: rows render in natural order, so the sent-message animation, separators, and header/footer components swap ends. Bottom clearance for the input bar is reserved statically via contentContainerStyle, with the keyboard composer inset seeded to 0 so the two don't stack. - pagination: onStartReached/onEndReached replace the viewability-window heuristic (useNativeSafeOnViewableItemsChanged is deleted). - prepend jump: maintainScrollAtEnd's dataChange trigger re-pins on ANY data change within maintainScrollAtEndThreshold of the end, so on short threads a load-older prepend yanked the view to the bottom. The threshold stays wide (0.5) because initialScrollAtEnd positions from estimatedItemSize and lands short on our tall rows; instead the re-pin is suspended while a prepend is in flight (prependActive). - initial position: the list is not mounted until the thread is loaded, so its first render always has data and initialScrollAtEnd lands at the newest message. Previously, arriving from the inbox mounted the list empty and the initial scroll ran against no data and never re-fired. - centering: scrollToItem(viewPosition: 0.5) lands accurately here, so the closed-loop offset corrector (viewable-range feedback, damped item-delta scrolls) is deleted in favor of re-asserting across the pagination settle. onScrollToIndexFailed goes with it — LegendList has no such prop. - maintainVisibleContentPosition is on from mount so prepends hold position. - fling cost: experimental_adaptiveRender lets rows shed their swipe pan handlers during fast scroll, via useAdaptiveRender in long-pressable driving the SwipeableRow `enabled` prop added in the parent commit. useAdaptiveRender reads LegendList's state context and throws outside a LegendList, so it can only be wired up here. The Swipeable stays mounted — toggling its tree would remount children and flash images. - stable renderItem: NativeRow reads the centered highlight itself, so renderItem identity never changes and a highlight change doesn't re-render every visible row. Includes a temporary [LISTDBG] dump for the initial-load settle; remove before merging. --- shared/chat/conversation/list-area/index.tsx | 599 ++++++++---------- .../messages/wrapper/long-pressable/index.tsx | 17 +- .../messages/wrapper/sent.native.tsx | 10 +- 3 files changed, 287 insertions(+), 339 deletions(-) diff --git a/shared/chat/conversation/list-area/index.tsx b/shared/chat/conversation/list-area/index.tsx index dda21755fdf5..ec3ad4812e58 100644 --- a/shared/chat/conversation/list-area/index.tsx +++ b/shared/chat/conversation/list-area/index.tsx @@ -30,21 +30,29 @@ import {copyToClipboard} from '@/util/storeless-actions' import noop from 'lodash/noop' import {LegendList} from '@legendapp/list/react' import type {LegendListRef} from '@/common-adapters' -import {FlatList} from 'react-native' -import type {ScrollViewProps} from 'react-native' +import type {View} from 'react-native' import {mobileTypingContainerHeight} from '../input-area/normal/typing' import { - KeyboardChatScrollView, - useKeyboardState, - useReanimatedKeyboardAnimation, -} from 'react-native-keyboard-controller' -import Animated, {interpolate, useAnimatedStyle} from 'react-native-reanimated' + KeyboardAwareLegendList, + useKeyboardChatComposerInset, + useKeyboardScrollToEnd, +} from '@legendapp/list/keyboard' +import {useReanimatedKeyboardAnimation} from 'react-native-keyboard-controller' +import Animated, {interpolate, useAnimatedReaction, useAnimatedStyle} from 'react-native-reanimated' +import {scheduleOnRN} from 'react-native-worklets' import {ThreadSearchOverlayContext} from '../thread-search-overlay-context' import {useSafeAreaInsets} from 'react-native-safe-area-context' type ItemType = T.Chat.Ordinal const noOrdinals: ReadonlyArray = [] +// Stable config so it doesn't churn props each render. Empty = enable adaptive render with defaults. +const adaptiveRenderConfig = {} + +// Stable MVCP config (anchor visible rows across data prepends). Referenced by native; desktop +// inlines an equivalent. +const mvcpData = {data: true} as const + const keyExtractor = (ordinal: ItemType) => String(ordinal) // Item type for list recycling pool separation. A message that leads its author group renders an @@ -95,6 +103,7 @@ const useThreadListData = () => containsLatestMessage: !s.moreToLoadForward, loaded: s.loaded, messageOrdinals: s.messageOrdinals ?? noOrdinals, + moreToLoadBack: s.moreToLoadBack, })) ) @@ -621,37 +630,19 @@ const DesktopThreadWrapperWithProfiler = () => ( // ==================== NATIVE ==================== -type RNFlatListRef = { - scrollToOffset: (opts: {animated: boolean; offset: number}) => void - scrollToItem: (opts: {animated: boolean; item: unknown; viewPosition?: number}) => void -} - -const useInvertedMessageOrdinals = (messageOrdinals?: ReadonlyArray) => { - const source = messageOrdinals ?? noOrdinals - return React.useMemo(() => (source.length > 1 ? [...source].reverse() : source), [source]) -} - const useNativeScrolling = (p: { centeredOrdinal: T.Chat.Ordinal - messageOrdinals: ReadonlyArray - listRef: React.RefObject + listRef: React.RefObject + scrollMessageToEnd: (o: {animated: boolean; closeKeyboard: boolean}) => Promise }) => { - const {listRef, centeredOrdinal, messageOrdinals} = p - const numOrdinals = messageOrdinals.length - const loadOlderMessages = useConversationThreadLoadOlderMessagesDueToScroll() - const getThreadLoadStatusOptions = useThreadLoadStatusOptionsGetter() + const {listRef, centeredOrdinal, scrollMessageToEnd} = p - // KeyboardChatScrollView sets contentInset.top = K - insets.bottom and - // contentOffset.y = -(K - insets.bottom) when keyboard is open. Scrolling to - // offset=0 would place content K-insets.bottom pixels lower (behind the keyboard). - // We compute the correct resting offset: keyboardHeight.value (negative) + insets.bottom. - // When keyboard is closed keyboardHeight.value = 0 so the result is clamped to 0. - const {height: keyboardAnimHeight} = useReanimatedKeyboardAnimation() - const {bottom: insetsBottom} = useSafeAreaInsets() + // scrollMessageToEnd freezes the keyboard-aware scroll view, scrolls to the end, + // then unfreezes — so the newest message stays pinned above the input bar even + // while the keyboard is open. const scrollToBottom = React.useCallback(() => { - const offset = Math.min(keyboardAnimHeight.value + insetsBottom, 0) - listRef.current?.scrollToOffset({animated: false, offset}) - }, [insetsBottom, keyboardAnimHeight, listRef]) + void scrollMessageToEnd({animated: false, closeKeyboard: false}) + }, [scrollMessageToEnd]) const {setScrollRef} = React.useContext(ThreadRefsContext) React.useEffect(() => { @@ -667,128 +658,84 @@ const useNativeScrolling = (p: { }, [centeredOrdinal]) const centeredOrdinalRef = React.useRef(centeredOrdinal) - // reset per centered target so each new search hit gets a fresh batch of retries - const scrollFailRetryRef = React.useRef(0) React.useEffect(() => { centeredOrdinalRef.current = centeredOrdinal - scrollFailRetryRef.current = 0 }, [centeredOrdinal]) const [scrollToCentered] = React.useState(() => () => { - const co = centeredOrdinalRef.current - if (lastScrollToCentered.current === co) { - return - } - lastScrollToCentered.current = co - // coarse: scrollToItem lands at the wrong offset for tall variable-height rows, - // but it gets the target area rendered. The closed-loop corrector in the - // component refines from there using the real viewable index range. - const reassert = (delay: number) => - setTimeout(() => { - const list = listRef.current - const cur = centeredOrdinalRef.current - if (!list || cur !== co || T.Chat.ordinalToNumber(cur) <= 0) { - return - } - list.scrollToItem({animated: false, item: cur, viewPosition: 0.5}) - }, delay) - ;[50, 250].forEach(reassert) - }) - - // The centered hit may be outside the rendered window, so scrollToItem fails - // silently. Wait for more rows to render and retry centering (capped) until it lands. - const [onScrollToIndexFailed] = React.useState(() => () => { - if (scrollFailRetryRef.current > 5) { - return - } - scrollFailRetryRef.current += 1 setTimeout(() => { + const list = listRef.current + if (!list) { + return + } const co = centeredOrdinalRef.current - if (T.Chat.ordinalToNumber(co) > 0) { - listRef.current?.scrollToItem({animated: false, item: co, viewPosition: 0.5}) + if (lastScrollToCentered.current === co) { + return } - }, 200) - }) - const onEndReached = () => { - loadOlderMessages(numOrdinals, getThreadLoadStatusOptions()) - } + lastScrollToCentered.current = co + void list.scrollToItem({animated: false, item: co, viewPosition: 0.5}) + }, 100) + }) return { - onEndReached, - onScrollToIndexFailed, scrollToBottom, scrollToCentered, } } -// The maintainVisibleContentPosition prop must ALWAYS be set (never toggled to undefined): -// RN Fabric only re-snapshots the MVP anchor while the prop is set, so an unset->set -// transition adjusts contentOffset against a stale anchor frame from before the prop was -// unset — a spurious jump + autoscroll animation of the whole list (seen after dismissing -// the keyboard following a send). Instead we swap between two configs: -// - closed (keyboard hidden): autoscrollToTopThreshold=1 so new messages at the bottom -// auto-reveal when the user is pinned there. -// - noAutoscroll (keyboard open, or centered on a search hit, or empty list): MVP still -// anchors content, but autoscroll-to-top is off because: -// 1. with the keyboard open contentOffset.y = -(K-insets.bottom) <= 1, so the threshold -// would fire on insert and scroll to y=0, hiding new messages behind the keyboard. -// 2. while centered on a search hit, autoscroll yanks the centered row. -// With the keyboard open, MVP's insert adjustment briefly holds old content in place; -// the deferred scrollToBottom layout effect below re-pins the newest message. -const maintainVisibleContentPositionClosed = { - autoscrollToTopThreshold: 1, - minIndexForVisible: 0, -} -const maintainVisibleContentPositionNoAutoscroll = { - minIndexForVisible: 0, -} +// Reads the centered highlight itself (like desktop's HighlightableRow) so renderItem stays +// referentially stable — a renderItem identity change re-renders every visible row at once. +const NativeRow = React.memo(function NativeRow({ordinal}: {ordinal: T.Chat.Ordinal}) { + const {centeredHighlightOrdinal} = useConversationCenter() + return ( + <> + + + + ) +}) + +const nativeRenderItem = ({item: ordinal}: {item: T.Chat.Ordinal}) => const NativeConversationList = function NativeConversationList() { const nativeStyles = useNativeStyles() - const List = FlatList as unknown as React.ComponentType< - Record & {ref?: React.Ref} - > - const conversationIDKey = useConversationThreadID() - const listData = useConversationThreadSelector( - C.useShallow(s => ({ - loaded: s.loaded, - messageOrdinals: s.messageOrdinals, - })) - ) - const {centeredHighlightOrdinal, centeredOrdinal} = useConversationCenter() + const listData = useThreadListData() + const {centeredOrdinal} = useConversationCenter() const noCenteredOrdinal = T.Chat.numberToOrdinal(-1) const centeredOrdinalOrNone = centeredOrdinal ?? noCenteredOrdinal - const centeredHighlightOrdinalOrNone = centeredHighlightOrdinal ?? noCenteredOrdinal - const {loaded} = listData + const {loaded, containsLatestMessage, messageOrdinals, moreToLoadBack} = listData + const hasCentered = centeredOrdinal !== undefined - const messageOrdinals = useInvertedMessageOrdinals(listData.messageOrdinals) + // initialScrollAtEnd only positions the FIRST render that has data. Coming from the inbox the + // thread loads async after mount, so if the list mounted empty the initial scroll would run on + // an empty list and never re-fire once data streamed in (cold-start has data at mount, which is + // why only the inbox path was broken). Gate the list mount on loaded so its first render always + // has data and initialScrollAtEnd lands at the newest message on both paths. + const listReady = loaded || hasCentered - const listRef = React.useRef(null) + const listRef = React.useRef(null) const markInitiallyLoadedThreadAsRead = useConversationThreadMarkThreadAsRead() - const keyExtractor = (ordinal: ItemType) => { - return String(ordinal) - } - - const renderItem = (info?: {item?: ItemType}) => { - const ordinal = info?.item - if (!ordinal) { - return null - } - return - } - - const numOrdinals = messageOrdinals.length - const getItemType = useGetItemType() const insets = useSafeAreaInsets() - const isKeyboardVisible = useKeyboardState((s: {isVisible: boolean}) => s.isVisible) - // While the thread-search bar is open it overlays the bottom of the list. Reserve - // that height as extra content padding so centered/newest messages clear it. + // While the thread-search bar is open it overlays the bottom of the list. Reserve that height + // as extra content padding (so the newest message clears it) and lift the jump-to-recent button + // above both the keyboard and the bar. searchOverlayHeight is a reanimated SharedValue set by + // the search bar's onLayout; mirror it to state for the (static) content padding. const searchOverlayHeight = React.useContext(ThreadSearchOverlayContext) + const [searchPad, setSearchPad] = React.useState(0) + useAnimatedReaction( + () => searchOverlayHeight?.value ?? 0, + (h, prev) => { + if (h !== prev) { + scheduleOnRN(setSearchPad, h) + } + }, + [searchOverlayHeight] + ) const {height: keyboardAnimHeight, progress: keyboardProgress} = useReanimatedKeyboardAnimation() const insetsBottom = insets.bottom // The input/search bar lives in a KeyboardStickyView with offset @@ -804,119 +751,101 @@ const NativeConversationList = function NativeConversationList() { ], })) - const {scrollToCentered, scrollToBottom, onEndReached, onScrollToIndexFailed} = useNativeScrolling({ + const {onStartReached: onStartReachedRaw, onEndReached} = usePagination({ + containsLatestMessage, + messageOrdinals, + }) + + // Suspend the bottom re-pin while a load-older prepend is in flight. maintainScrollAtEnd's + // dataChange trigger scroll-to-ends ANY data change while within maintainScrollAtEndThreshold + // (0.5 viewport) of the end — on short threads the top is inside that window, so the prepend + // yanks the view to the bottom. The guard is set when we request older messages and cleared via + // timeout (never synchronously in render) so the prepend's data change is still processed with + // the re-pin off; 0ms once it lands (first ordinal changed) or 3s fallback if it never does. + const [prependPending, setPrependPending] = React.useState< + {conv: string; firstOrdinal: T.Chat.Ordinal | undefined} | undefined + >(undefined) + const firstOrdinal = messageOrdinals[0] + const prependActive = prependPending?.conv === conversationIDKey + React.useEffect(() => { + if (!prependPending) return undefined + const landedOrStale = + prependPending.conv !== conversationIDKey || prependPending.firstOrdinal !== firstOrdinal + const id = setTimeout(() => setPrependPending(undefined), landedOrStale ? 0 : 3000) + return () => clearTimeout(id) + }, [prependPending, conversationIDKey, firstOrdinal]) + + const firstOrdinalRef = React.useRef(firstOrdinal) + React.useEffect(() => { + firstOrdinalRef.current = firstOrdinal + }, [firstOrdinal]) + const moreToLoadBackRef = React.useRef(moreToLoadBack) + React.useEffect(() => { + moreToLoadBackRef.current = moreToLoadBack + }, [moreToLoadBack]) + + const onStartReached = React.useCallback(() => { + if (moreToLoadBackRef.current) { + setPrependPending({conv: conversationIDKey, firstOrdinal: firstOrdinalRef.current}) + } + onStartReachedRaw() + }, [conversationIDKey, onStartReachedRaw]) + + // The bottom clearance for the input bar is reserved statically via contentContainerStyle + // (listContentStyle) below, so this composer inset is seeded to 0 — otherwise the two stack + // and leave a large empty gap below the newest message on cold start. composerRef is null + // (the composer lives in a sibling subtree, not this list) so measure() is never called. + const composerRef = React.useRef(null) + const {contentInsetEndAdjustment} = useKeyboardChatComposerInset(listRef, composerRef, 0) + const {freeze, scrollMessageToEnd} = useKeyboardScrollToEnd({listRef}) + + const {scrollToCentered, scrollToBottom} = useNativeScrolling({ centeredOrdinal: centeredOrdinalOrNone, listRef, - messageOrdinals, + scrollMessageToEnd, }) - // Closed-loop centering corrector. scrollToItem/scrollToIndex lands at the wrong - // offset here (inverted list + custom keyboard scrollview + tall variable-height - // image rows), so instead we read the actual viewable index range each frame and - // scrollToOffset by the item-delta until the target sits at viewport center. - const scrollOffsetRef = React.useRef(0) - const contentHeightRef = React.useRef(0) + // Latest centered target, read inside the stable re-assert callback. const centeredRef = React.useRef(centeredOrdinalOrNone) React.useEffect(() => { centeredRef.current = centeredOrdinalOrNone }, [centeredOrdinalOrNone]) - const ordsRef = React.useRef(messageOrdinals) - React.useEffect(() => { - ordsRef.current = messageOrdinals - }, [messageOrdinals]) - // {active, iters}: correcting toward a centered hit and how many steps taken - const correctRef = React.useRef({active: false, iters: 0}) - const vFirstRef = React.useRef(undefined) - const vLastRef = React.useRef(undefined) - const [correctCenter] = React.useState( - () => (first: number | null | undefined, last: number | null | undefined) => { - const st = correctRef.current - if (!st.active) return - const co = centeredRef.current - const ords = ordsRef.current - const num = ords.length - if (co <= 0 || !num || first == null || last == null) return - const targetIdx = ords.indexOf(co) - if (targetIdx < 0) return - const centerIdx = (first + last) / 2 - const diff = targetIdx - centerIdx - if (Math.abs(diff) <= 0.5 || st.iters > 12) { - st.active = false - return - } - st.iters += 1 - const avgH = contentHeightRef.current / num - // damp by 0.9 to avoid overshoot/oscillation; higher index = older = higher offset - const newOffset = Math.max(0, scrollOffsetRef.current + diff * avgH * 0.9) - listRef.current?.scrollToOffset({animated: false, offset: newOffset}) - } - ) - const [onScrollNative] = React.useState( - () => - (e: {nativeEvent: {contentOffset: {y: number}; contentSize: {height: number}}}) => { - scrollOffsetRef.current = e.nativeEvent.contentOffset.y - contentHeightRef.current = e.nativeEvent.contentSize.height - } - ) - const [onContentSizeChangeNative] = React.useState(() => (_w: number, h: number) => { - contentHeightRef.current = h - }) - // user touched the list: stop fighting them - const [onScrollBeginDrag] = React.useState(() => () => { - correctRef.current.active = false - }) const jumpToRecent = useJumpToRecent(scrollToBottom, messageOrdinals.length) - // When keyboard is open, maintainVisibleContentPosition adjusts contentOffset by the new - // message height when a message is added, undoing the scrollToBottom from onSubmit. - // Defer the re-scroll past the native MPV adjustment (which runs on the UI thread after - // React's commit) so the newest message stays visible. - const prevNumOrdinalsRef = React.useRef(numOrdinals) - // Tracks which conversation prevNumOrdinalsRef's baseline belongs to so the - // baseline resets on a real conversation switch (value compare) rather than on - // a react-native-screens freeze/thaw, which re-mounts effects. - const numBaselineConvRef = React.useRef(conversationIDKey) - const isKeyboardVisibleRef = React.useRef(isKeyboardVisible) - React.useLayoutEffect(() => { - isKeyboardVisibleRef.current = isKeyboardVisible + // Re-assert native centering on the current target. scrollToItem(viewPosition: 0.5) lands + // accurately on its own, but the centered load streams older messages in afterward (pagination + // prepends), so we re-call it across a few frames; maintainVisibleContentPosition keeps the row + // steady between asserts. + const [reassertCentered] = React.useState(() => () => { + const co = centeredRef.current + if (co <= 0) return + void listRef.current?.scrollToItem({animated: false, item: co, viewPosition: 0.5}) }) - React.useLayoutEffect(() => { - const sameConv = numBaselineConvRef.current === conversationIDKey - numBaselineConvRef.current = conversationIDKey - const prev = prevNumOrdinalsRef.current - prevNumOrdinalsRef.current = numOrdinals - if (sameConv && numOrdinals > prev && isKeyboardVisibleRef.current) { - const id = setTimeout(() => { - if (isKeyboardVisibleRef.current) { - scrollToBottom() - } - }, 0) - return () => clearTimeout(id) - } - return undefined - }, [conversationIDKey, numOrdinals, scrollToBottom]) - - // Center on the search hit once it actually appears in the loaded list. Centering - // on the raw centeredOrdinal change is unreliable: navigating to a hit reloads the - // thread centered on it, so messageOrdinals is briefly empty (idx -1) when the - // ordinal changes. Wait for the target to load, then scroll (scrollToCentered - // guards against repeats and re-asserts across frames). + + // Center on the search hit once it actually appears in the loaded list. Centering on the raw + // centeredOrdinal change is unreliable: navigating to a hit reloads the thread centered on it, + // so messageOrdinals is briefly empty (the target not yet present) when the ordinal changes. + // Wait for the target to load, then coarse-scroll and re-assert across the pagination settle. + const lastCenteredOrdinal = React.useRef(0) React.useEffect(() => { - if (!(centeredOrdinalOrNone > 0 && messageOrdinals.includes(centeredOrdinalOrNone))) { + if (centeredOrdinalOrNone <= 0) { + lastCenteredOrdinal.current = 0 + return undefined + } + if (!messageOrdinals.includes(centeredOrdinalOrNone)) { + return undefined + } + if (lastCenteredOrdinal.current === centeredOrdinalOrNone) { return undefined } - // coarse scroll to get the target area rendered, then run the closed-loop - // corrector which refines via the real viewable index range + lastCenteredOrdinal.current = centeredOrdinalOrNone scrollToCentered() - correctRef.current = {active: true, iters: 0} - const ids = [50, 250, 500, 900].map(d => - setTimeout(() => correctCenter(vFirstRef.current, vLastRef.current), d) - ) + const ids = [50, 250, 500, 900, 1400].map(d => setTimeout(reassertCentered, d)) return () => { ids.forEach(clearTimeout) } - }, [centeredOrdinalOrNone, messageOrdinals, scrollToCentered, correctCenter]) + }, [centeredOrdinalOrNone, messageOrdinals, scrollToCentered, reassertCentered]) // These refs store the conversation they last applied to (not a boolean) so a // freeze/thaw of this screen — which re-mounts effects without a real @@ -938,99 +867,147 @@ const NativeConversationList = function NativeConversationList() { markInitiallyLoadedThreadAsRead() } + // Initial bottom position is handled declaratively by initialScrollAtEnd (the list is not + // mounted until data is loaded, so its first render has data). Centered navigation still + // needs an imperative nudge for the case where loaded flips true after centeredOrdinal set. if (centeredOrdinalOrNone > 0) { scrollToCentered() setTimeout(() => { scrollToCentered() }, 100) - } else if (numOrdinals > 0) { - scrollToBottom() - setTimeout(() => { - scrollToBottom() - }, 100) - } - }, [ - conversationIDKey, - centeredOrdinalOrNone, - loaded, - markInitiallyLoadedThreadAsRead, - numOrdinals, - scrollToBottom, - scrollToCentered, - ]) - - const onViewableItemsChanged = useNativeSafeOnViewableItemsChanged(onEndReached, messageOrdinals.length) - const [onViewableItemsChangedNative] = React.useState( - () => (info: {viewableItems: Array<{index: number | null}>}) => { - onViewableItemsChanged.current(info) - const first = info.viewableItems.at(0)?.index - const last = info.viewableItems.at(-1)?.index - vFirstRef.current = first - vLastRef.current = last - correctCenter(first, last) } + }, [conversationIDKey, centeredOrdinalOrNone, loaded, markInitiallyLoadedThreadAsRead, scrollToCentered]) + + // [LISTDBG] TEMP: diagnose initial-load not landing at bottom on tall-row threads. Dumps list + // state across the load settle: whether it lands short (gap>0) or drifts as rows measure, plus + // where the newest row actually sits (belowVp>0 = parked above) and real per-type avgs vs 120. + const dbgDump = React.useCallback( + (tag: string) => { + const s = listRef.current?.getState() as + | { + isAtEnd?: boolean + scroll?: number + scrollLength?: number + contentLength?: number + end?: number + endBuffered?: number + isWithinMaintainScrollAtEndThreshold?: boolean + getAverageItemSizes?: () => Record + positionAtIndex?: (i: number) => number + sizeAtIndex?: (i: number) => number + } + | undefined + let avgs = '' + try { + const a = s?.getAverageItemSizes?.() + if (a) { + avgs = Object.entries(a) + .map(([k, v]) => `${k}:${Math.round(v.average)}(${v.count})`) + .join(' ') + } + } catch {} + const gap = Math.round((s?.contentLength ?? 0) - (s?.scroll ?? 0) - (s?.scrollLength ?? 0)) + const lastIdx = messageOrdinals.length - 1 + let lastInfo = '' + try { + const posLast = Math.round(s?.positionAtIndex?.(lastIdx) ?? -1) + const sizeLast = Math.round(s?.sizeAtIndex?.(lastIdx) ?? -1) + const vpBottom = Math.round((s?.scroll ?? 0) + (s?.scrollLength ?? 0)) + lastInfo = `lastIdx=${lastIdx} posLast=${posLast} sizeLast=${sizeLast} lastBottom=${posLast + sizeLast} vpBottom=${vpBottom} belowVp=${posLast + sizeLast - vpBottom}` + } catch {} + console.log( + `[LISTDBG] ${tag} conv=${conversationIDKey.slice(0, 6)} num=${messageOrdinals.length} ` + + `isAtEnd=${s?.isAtEnd} withinThresh=${s?.isWithinMaintainScrollAtEndThreshold} ` + + `end=${s?.end} endBuf=${s?.endBuffered} ` + + `scroll=${Math.round(s?.scroll ?? -1)} scrollLen=${Math.round(s?.scrollLength ?? -1)} ` + + `contentLen=${Math.round(s?.contentLength ?? -1)} gap=${gap} ${lastInfo} avgs=[${avgs}]` + ) + }, + [conversationIDKey, messageOrdinals] ) + const dbgLoadedRef = React.useRef(undefined) + React.useEffect(() => { + if (!loaded) return undefined + if (dbgLoadedRef.current === conversationIDKey) return undefined + dbgLoadedRef.current = conversationIDKey + const ids = [0, 100, 300, 600, 1200, 2000, 3500].map(d => setTimeout(() => dbgDump(`t+${d}`), d)) + return () => { + ids.forEach(clearTimeout) + } + }, [loaded, conversationIDKey, dbgDump]) - const renderScrollComponent = React.useCallback( - (props: ScrollViewProps) => ( - - ), - [insets.bottom, searchOverlayHeight] - ) - - const mvpAutoscroll = !(centeredOrdinalOrNone > 0 || !numOrdinals || isKeyboardVisible) + const initialScrollIndex = useInitialScrollIndex(messageOrdinals, centeredOrdinal) - const nativeContentContainerStyle = React.useMemo( - () => ({ - paddingBottom: 0, - paddingTop: mobileTypingContainerHeight + insets.bottom, - }), - [insets.bottom] + // Reserve bottom space so the newest message clears the sticky input bar, which is pulled up + // over the list bottom (KeyboardStickyView offset -insets.bottom) plus the floating typing + // indicator. Without this the list scrolls to its content end but the newest row sits behind + // the input bar. + const listContentStyle = React.useMemo( + () => ({paddingBottom: mobileTypingContainerHeight + insets.bottom + searchPad}), + [insets.bottom, searchPad] ) + // The input bar (KeyboardStickyView, closed offset -insets.bottom) overlaps the bottom of the + // list by insets.bottom, so without this the scroll indicator runs down behind it. Inset the + // indicator by exactly that overlap (NOT the full content padding, which also reserves space for + // the floating typing indicator that the scrollbar doesn't need to clear). + const scrollIndicatorInsets = React.useMemo(() => ({bottom: insets.bottom}), [insets.bottom]) + return ( - + ) : null} {jumpToRecent && ( {jumpToRecent} @@ -1054,42 +1031,4 @@ const useNativeStyles = Kb.Styles.createStyleHook( }) as const ) -const minTimeDelta = 1000 -const minDistanceFromEnd = 10 - -const useNativeSafeOnViewableItemsChanged = (onEndReached: () => void, numOrdinals: number) => { - const nextCallbackRef = React.useRef(new Date().getTime()) - const onEndReachedRef = React.useRef(onEndReached) - React.useEffect(() => { - onEndReachedRef.current = onEndReached - }, [onEndReached]) - const numOrdinalsRef = React.useRef(numOrdinals) - React.useEffect(() => { - numOrdinalsRef.current = numOrdinals - nextCallbackRef.current = new Date().getTime() + minTimeDelta - }, [numOrdinals]) - - // this can't change ever, so we have to use refs to keep in sync - const onViewableItemsChanged = React.useRef( - ({viewableItems}: {viewableItems: Array<{index: number | null}>}) => { - const idx = viewableItems.at(-1)?.index ?? 0 - const lastIdx = numOrdinalsRef.current - 1 - const offset = numOrdinalsRef.current > 50 ? minDistanceFromEnd : 1 - const deltaIdx = idx - lastIdx + offset - // not far enough from the end - if (deltaIdx < 0) { - return - } - const t = new Date().getTime() - const deltaT = t - nextCallbackRef.current - // enough time elapsed? - if (deltaT > 0) { - nextCallbackRef.current = t + minTimeDelta - onEndReachedRef.current() - } - } - ) - return onViewableItemsChanged -} - export default isMobile ? NativeConversationList : DesktopThreadWrapperWithProfiler diff --git a/shared/chat/conversation/messages/wrapper/long-pressable/index.tsx b/shared/chat/conversation/messages/wrapper/long-pressable/index.tsx index 0e1f2dee7469..cfcc77593336 100644 --- a/shared/chat/conversation/messages/wrapper/long-pressable/index.tsx +++ b/shared/chat/conversation/messages/wrapper/long-pressable/index.tsx @@ -16,6 +16,7 @@ type Props = { import {useConversationThreadToggleSearch} from '../../../thread-context' import Swipeable, {type SwipeableMethods} from '@/common-adapters/swipeable-row' import {ThreadRefsContext} from '@/chat/conversation/normal/context' +import {useAdaptiveRender} from '@legendapp/list/react-native' function ReplyIcon({progress}: {progress: Animated.Value}) { const styles = useStyles() @@ -28,16 +29,23 @@ function ReplyIcon({progress}: {progress: Animated.Value}) { } function LongPressable(props: Props & {ref?: React.Ref}) { + if (!isMobile) { + return + } + return +} + +function LongPressableMobile(props: Props & {ref?: React.Ref}) { const styles = useStyles() const toggleThreadSearch = useConversationThreadToggleSearch() const setReplyTo = InputState.useConversationInputDispatch(s => s.setReplyTo) const ordinal = useOrdinal() const {focusInput} = React.useContext(ThreadRefsContext) const swipeRef = React.useRef(null) - - if (!isMobile) { - return - } + // Velocity-driven signal from LegendList: during fast scroll it flips to "light". We keep the + // Swipeable mounted (toggling its tree would remount children and flash images) and instead just + // disable its pan handlers in light mode, shedding the per-row touch evaluation during the fling. + const adaptiveMode = useAdaptiveRender() const {children, onLongPress, style} = props @@ -68,6 +76,7 @@ function LongPressable(props: Props & {ref?: React.Ref}) { return ( diff --git a/shared/chat/conversation/messages/wrapper/sent.native.tsx b/shared/chat/conversation/messages/wrapper/sent.native.tsx index 4629e2b1f6dc..785adfd9965d 100644 --- a/shared/chat/conversation/messages/wrapper/sent.native.tsx +++ b/shared/chat/conversation/messages/wrapper/sent.native.tsx @@ -1,11 +1,11 @@ import type * as React from 'react' import Animated, {FadeInDown} from 'react-native-reanimated' -// Slide-up + fade for a message you just sent. The thread list is an inverted -// FlatList (cells are flipped with scaleY: -1), so FadeInDown renders on screen -// as sliding up from below. Runs entirely on the UI thread with no re-renders. -// The entering animation only plays when this Animated.View MOUNTS — callers must -// key it per message (recycled containers reuse instances). +// Slide-up + fade for a message you just sent. The thread list (LegendList) is +// NOT inverted, so FadeInDown (enters from 25px below, sliding up into place) +// reads as the row rising from the input bar. Runs entirely on the UI thread +// with no re-renders. The entering animation only plays when this Animated.View +// MOUNTS — callers must key it per message (recycled containers reuse instances). export function Sent(p: {children: React.ReactNode}) { return ( From 9712f15919bb865c8780b2f9fb8789a99773204d Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Fri, 17 Jul 2026 16:56:30 -0400 Subject: [PATCH 02/38] fix(chat): keep native thread pinned to bottom through keyboard dismissal The keyboard-hide contentOffset unwind shifts LegendList's draw window, so estimate-only rows measure in at their real sizes mid-dismiss; LegendList's JS MVCP compensates against that moving target and settles short of the end, leaving the newest message below the fold. Re-pin frame-by-frame from keyboardWillHide until shortly after keyboardDidHide when we were at the end when the hide started. --- shared/chat/conversation/list-area/index.tsx | 58 +++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/shared/chat/conversation/list-area/index.tsx b/shared/chat/conversation/list-area/index.tsx index ec3ad4812e58..1f92bb3c8d68 100644 --- a/shared/chat/conversation/list-area/index.tsx +++ b/shared/chat/conversation/list-area/index.tsx @@ -37,7 +37,7 @@ import { useKeyboardChatComposerInset, useKeyboardScrollToEnd, } from '@legendapp/list/keyboard' -import {useReanimatedKeyboardAnimation} from 'react-native-keyboard-controller' +import {KeyboardEvents, useReanimatedKeyboardAnimation} from 'react-native-keyboard-controller' import Animated, {interpolate, useAnimatedReaction, useAnimatedStyle} from 'react-native-reanimated' import {scheduleOnRN} from 'react-native-worklets' import {ThreadSearchOverlayContext} from '../thread-search-overlay-context' @@ -925,6 +925,62 @@ const NativeConversationList = function NativeConversationList() { }, [conversationIDKey, messageOrdinals] ) + // Keyboard-dismiss re-pin: the hide unwind (KeyboardChatScrollView writes the compensating + // contentOffset) shifts LegendList's draw window, so rows that only had estimatedItemSize + // measure in at their real (smaller) sizes mid-dismiss. LegendList's JS MVCP compensates + // against that moving target and settles short of the end, with nothing to re-pin it + // (maintainScrollAtEnd only fires on data changes). If we were pinned at the end when the + // hide started, keep re-pinning frame-by-frame while the keyboard animates down. The + // still-near-end guard keeps an on-drag dismissal that continues scrolling up into history + // from being yanked back down. + // The loop runs from hide start until shortly after the keyboard is fully down (sizes have + // settled by then), so the list visually stays pinned through the whole dismissal instead of + // snapping once at the end. It only issues a scroll when a gap has actually opened. + React.useEffect(() => { + let raf = 0 + let stopId: ReturnType | undefined + const cancel = () => { + cancelAnimationFrame(raf) + raf = 0 + clearTimeout(stopId) + stopId = undefined + } + const getListState = () => + listRef.current?.getState() as + | {isAtEnd?: boolean; scroll?: number; scrollLength?: number; contentLength?: number} + | undefined + const tick = () => { + const s = getListState() + if (s) { + const gap = (s.contentLength ?? 0) - (s.scroll ?? 0) - (s.scrollLength ?? 0) + const stillNearEnd = gap > 1 && gap <= (s.scrollLength ?? 0) + if (stillNearEnd) { + void listRef.current?.scrollToEnd({animated: false}) + } + } + raf = requestAnimationFrame(tick) + } + const subs = [ + KeyboardEvents.addListener('keyboardWillHide', () => { + if (!getListState()?.isAtEnd) return + cancel() + // 1s cap in case keyboardDidHide never fires (e.g. interrupted dismissal) + stopId = setTimeout(cancel, 1000) + raf = requestAnimationFrame(tick) + }), + KeyboardEvents.addListener('keyboardDidHide', () => { + if (!raf) return + clearTimeout(stopId) + // let the last post-dismissal measurements land, then stop tracking + stopId = setTimeout(cancel, 150) + }), + ] + return () => { + cancel() + subs.forEach(sub => sub.remove()) + } + }, []) + const dbgLoadedRef = React.useRef(undefined) React.useEffect(() => { if (!loaded) return undefined From b1616d9c0dba1b6a61ade06c949bf1529e4e25b7 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 11 Aug 2026 11:50:16 -0400 Subject: [PATCH 03/38] fix(chat): land search hits reliably via patched legend-list Two defects kept a search hit from landing where it was asked to, one per platform, both fixed in the library rather than worked around here. On desktop the scroll extent legend-list clamps against is measured from the DOM, which lags a React commit. The jump to a hit runs on the tick the thread data changes, so the requested offset was clamped to a stale extent - usually 0 - and the thread sat at the top. On native the scroll landed against estimated row heights and nothing re-aimed once the rows above measured taller, so hits drifted off screen. With the library fixed, the elaborate app-side compensation is unnecessary: the desktop centering loop, the native re-assert timers, the prepend guard and the keyboard re-pin loop all come out, leaving a single scrollToIndex per target. maintainVisibleContentPosition on native now stays mounted with one config instead of being switched off while centered. It is what holds the hit in place while the centered load streams older messages in, and toggling the prop makes the list jump. Patch regenerated from the fork build; both fixes are proposed upstream. --- shared/chat/conversation/list-area/index.tsx | 366 +- shared/patches/@legendapp+list+3.3.5.patch | 9246 ++++++++++++++++++ 2 files changed, 9284 insertions(+), 328 deletions(-) create mode 100644 shared/patches/@legendapp+list+3.3.5.patch diff --git a/shared/chat/conversation/list-area/index.tsx b/shared/chat/conversation/list-area/index.tsx index 1f92bb3c8d68..9b5f2f7d64ca 100644 --- a/shared/chat/conversation/list-area/index.tsx +++ b/shared/chat/conversation/list-area/index.tsx @@ -37,7 +37,7 @@ import { useKeyboardChatComposerInset, useKeyboardScrollToEnd, } from '@legendapp/list/keyboard' -import {KeyboardEvents, useReanimatedKeyboardAnimation} from 'react-native-keyboard-controller' +import {useReanimatedKeyboardAnimation} from 'react-native-keyboard-controller' import Animated, {interpolate, useAnimatedReaction, useAnimatedStyle} from 'react-native-reanimated' import {scheduleOnRN} from 'react-native-worklets' import {ThreadSearchOverlayContext} from '../thread-search-overlay-context' @@ -103,7 +103,6 @@ const useThreadListData = () => containsLatestMessage: !s.moreToLoadForward, loaded: s.loaded, messageOrdinals: s.messageOrdinals ?? noOrdinals, - moreToLoadBack: s.moreToLoadBack, })) ) @@ -147,7 +146,6 @@ const usePagination = (p: { return {onEndReached, onStartReached} } -const centerTolerancePx = 8 // A scroller within this many pixels of its end counts as at the end. const endTolerancePx = 2 @@ -333,106 +331,35 @@ const DesktopThreadWrapper = function DesktopThreadWrapper() { // scroll when loaded becomes true after centeredOrdinal was already set. // Reset per dataset, not per conversation: re-centering on the ordinal we are already parked // on still reloads the thread, so the list has to scroll to it again. - const lastScrolledCenteredRef = React.useRef(undefined) + // Records the index the target sat at when we last scrolled to it, not just the ordinal: a + // load-older prepend moves the target down by however many messages arrived above it, so the + // scroll we already issued no longer points at it and has to be re-issued. + const lastScrolledCenteredRef = React.useRef<{index: number; ordinal: T.Chat.Ordinal} | undefined>( + undefined + ) React.useLayoutEffect(() => { lastScrolledCenteredRef.current = undefined }, [datasetKey]) - // Owns the in-flight centering loop. It has to outlive re-renders: the messages that make - // centering accurate arrive after it starts, so the loop must not be torn down by an effect - // cleanup when messageOrdinals changes. Only a new target or unmount stops it. - const centerLoopRef = React.useRef<{cancelled: boolean} | undefined>(undefined) - // The loop re-centers for up to ~3s; a user scrolling in that window must win. - const abortCentering = React.useCallback(() => { - if (centerLoopRef.current) centerLoopRef.current.cancelled = true - }, []) - React.useEffect(() => abortCentering, [abortCentering]) - - // Closed loop, not one shot: rows enter at estimatedItemSize and only settle as they measure, so - // the first scroll lands off by however wrong the estimates above the target were. Measure the - // row's real offset from the viewport center and correct until it holds still, then get out of - // the way: maintainVisibleContentPosition owns the offset from then on. Two controllers fighting - // over the same scroll offset would oscillate. - // - // Correct via LegendList's own scrollToOffset, never scrollIntoView: touching scrollTop directly - // desyncs LegendList's internal scroll state, and the next time it recomputes item positions it - // snaps somewhere unrelated. - const scrollToCentered = React.useEffectEvent((target: T.Chat.Ordinal) => { - abortCentering() - const loop = {cancelled: false} - centerLoopRef.current = loop - const run = async () => { - let settled = 0 - let pinnedChecks = 0 - let scrollAtLastRequest: number | undefined - for (let elapsed = 0; elapsed < 3000 && !loop.cancelled; ) { - const wrapper = wrapperRef.current as unknown as { - getBoundingClientRect: () => {height: number; top: number} - querySelector: (s: string) => {getBoundingClientRect: () => {height: number; top: number}} | null - } | null - const el = wrapper ? wrapper.querySelector(`[data-ordinal="${target}"]`) : null - if (!wrapper || !el) { - // Target is outside the rendered window; get it mounted first. - const idx = sortedIndexOf( - messageOrdinalsRef.current as unknown as number[], - target as unknown as number - ) - if (idx >= 0) { - void listRef.current?.scrollToIndex({animated: false, index: idx, viewPosition: 0.5}) - } - settled = 0 - pinnedChecks = 0 - await new Promise(resolve => setTimeout(resolve, 100)) - elapsed += 100 - continue - } - const elRect = el.getBoundingClientRect() - const wrapRect = wrapper.getBoundingClientRect() - const offBy = elRect.top + elRect.height / 2 - (wrapRect.top + wrapRect.height / 2) - const scroll = listRef.current?.getState().scroll - // Deadband, not exact centering: below this the row reads as centered, and chasing the - // remainder only fights maintainVisibleContentPosition's own sub-pixel adjustments. - if (Math.abs(offBy) <= centerTolerancePx || scroll === undefined) { - pinnedChecks = 0 - // Only the iteration right after a correction can diagnose a clamp. - scrollAtLastRequest = undefined - if (++settled >= 3) return - } else if (scroll === scrollAtLastRequest) { - // A hit near either end of the thread cannot be centered: the offset we ask for gets - // clamped and the row never reaches the middle. Our last correction moved the scroll - // position not at all, so we are pinned against an edge — stop rather than spin. - if (++pinnedChecks >= 3) return - } else { - pinnedChecks = 0 - scrollAtLastRequest = scroll - void listRef.current?.scrollToOffset({animated: false, offset: scroll + offBy}) - } - await new Promise(resolve => setTimeout(resolve, 50)) - elapsed += 50 - } - } - void run() - }) - React.useEffect(() => { if (!loaded) return if (centeredOrdinal !== undefined) { - if (lastScrolledCenteredRef.current === centeredOrdinal) return const idx = sortedIndexOf( messageOrdinalsRef.current as unknown as number[], centeredOrdinal as unknown as number ) if (idx < 0) return - lastScrolledCenteredRef.current = centeredOrdinal - scrollToCentered(centeredOrdinal) + const last = lastScrolledCenteredRef.current + if (last?.ordinal === centeredOrdinal && last.index === idx) return + lastScrolledCenteredRef.current = {index: idx, ordinal: centeredOrdinal} + void listRef.current?.scrollToIndex({animated: false, index: idx, viewPosition: 0.5}) } else if (lastScrolledCenteredRef.current !== undefined) { lastScrolledCenteredRef.current = undefined - abortCentering() if (containsLatestMessage) { void listRef.current?.scrollToEnd({animated: false}) } } - }, [abortCentering, centeredOrdinal, loaded, containsLatestMessage, messageOrdinals]) + }, [centeredOrdinal, loaded, containsLatestMessage, messageOrdinals]) // Scroll to the message being edited const lastEditingOrdinalRef = React.useRef(undefined) @@ -536,12 +463,6 @@ const DesktopThreadWrapper = function DesktopThreadWrapper() { const initialScrollIndex = useInitialScrollIndex(messageOrdinals, centeredOrdinal) - // A wheel means the user took over: stop centering so we don't scroll them away from where - // they landed. - const onWheel = React.useCallback(() => { - abortCentering() - }, [abortCentering]) - return (
{ - if (T.Chat.ordinalToNumber(centeredOrdinal) < 0) { - lastScrollToCentered.current = -1 - } - }, [centeredOrdinal]) - const centeredOrdinalRef = React.useRef(centeredOrdinal) React.useEffect(() => { centeredOrdinalRef.current = centeredOrdinal }, [centeredOrdinal]) + // Stable so the effect that calls it does not re-run on every centeredOrdinal change. const [scrollToCentered] = React.useState(() => () => { - setTimeout(() => { - const list = listRef.current - if (!list) { - return - } - const co = centeredOrdinalRef.current - if (lastScrollToCentered.current === co) { - return - } - - lastScrollToCentered.current = co - void list.scrollToItem({animated: false, item: co, viewPosition: 0.5}) - }, 100) + const co = centeredOrdinalRef.current + if (T.Chat.ordinalToNumber(co) < 0) return + void listRef.current?.scrollToItem({animated: false, item: co, viewPosition: 0.5}) }) return { @@ -704,16 +607,9 @@ const NativeConversationList = function NativeConversationList() { const {centeredOrdinal} = useConversationCenter() const noCenteredOrdinal = T.Chat.numberToOrdinal(-1) const centeredOrdinalOrNone = centeredOrdinal ?? noCenteredOrdinal - const {loaded, containsLatestMessage, messageOrdinals, moreToLoadBack} = listData + const {loaded, containsLatestMessage, messageOrdinals} = listData const hasCentered = centeredOrdinal !== undefined - // initialScrollAtEnd only positions the FIRST render that has data. Coming from the inbox the - // thread loads async after mount, so if the list mounted empty the initial scroll would run on - // an empty list and never re-fire once data streamed in (cold-start has data at mount, which is - // why only the inbox path was broken). Gate the list mount on loaded so its first render always - // has data and initialScrollAtEnd lands at the newest message on both paths. - const listReady = loaded || hasCentered - const listRef = React.useRef(null) const markInitiallyLoadedThreadAsRead = useConversationThreadMarkThreadAsRead() @@ -751,46 +647,11 @@ const NativeConversationList = function NativeConversationList() { ], })) - const {onStartReached: onStartReachedRaw, onEndReached} = usePagination({ + const {onStartReached, onEndReached} = usePagination({ containsLatestMessage, messageOrdinals, }) - // Suspend the bottom re-pin while a load-older prepend is in flight. maintainScrollAtEnd's - // dataChange trigger scroll-to-ends ANY data change while within maintainScrollAtEndThreshold - // (0.5 viewport) of the end — on short threads the top is inside that window, so the prepend - // yanks the view to the bottom. The guard is set when we request older messages and cleared via - // timeout (never synchronously in render) so the prepend's data change is still processed with - // the re-pin off; 0ms once it lands (first ordinal changed) or 3s fallback if it never does. - const [prependPending, setPrependPending] = React.useState< - {conv: string; firstOrdinal: T.Chat.Ordinal | undefined} | undefined - >(undefined) - const firstOrdinal = messageOrdinals[0] - const prependActive = prependPending?.conv === conversationIDKey - React.useEffect(() => { - if (!prependPending) return undefined - const landedOrStale = - prependPending.conv !== conversationIDKey || prependPending.firstOrdinal !== firstOrdinal - const id = setTimeout(() => setPrependPending(undefined), landedOrStale ? 0 : 3000) - return () => clearTimeout(id) - }, [prependPending, conversationIDKey, firstOrdinal]) - - const firstOrdinalRef = React.useRef(firstOrdinal) - React.useEffect(() => { - firstOrdinalRef.current = firstOrdinal - }, [firstOrdinal]) - const moreToLoadBackRef = React.useRef(moreToLoadBack) - React.useEffect(() => { - moreToLoadBackRef.current = moreToLoadBack - }, [moreToLoadBack]) - - const onStartReached = React.useCallback(() => { - if (moreToLoadBackRef.current) { - setPrependPending({conv: conversationIDKey, firstOrdinal: firstOrdinalRef.current}) - } - onStartReachedRaw() - }, [conversationIDKey, onStartReachedRaw]) - // The bottom clearance for the input bar is reserved statically via contentContainerStyle // (listContentStyle) below, so this composer inset is seeded to 0 — otherwise the two stack // and leave a large empty gap below the newest message on cold start. composerRef is null @@ -805,47 +666,30 @@ const NativeConversationList = function NativeConversationList() { scrollMessageToEnd, }) - // Latest centered target, read inside the stable re-assert callback. - const centeredRef = React.useRef(centeredOrdinalOrNone) - React.useEffect(() => { - centeredRef.current = centeredOrdinalOrNone - }, [centeredOrdinalOrNone]) - const jumpToRecent = useJumpToRecent(scrollToBottom, messageOrdinals.length) - // Re-assert native centering on the current target. scrollToItem(viewPosition: 0.5) lands - // accurately on its own, but the centered load streams older messages in afterward (pagination - // prepends), so we re-call it across a few frames; maintainVisibleContentPosition keeps the row - // steady between asserts. - const [reassertCentered] = React.useState(() => () => { - const co = centeredRef.current - if (co <= 0) return - void listRef.current?.scrollToItem({animated: false, item: co, viewPosition: 0.5}) - }) - // Center on the search hit once it actually appears in the loaded list. Centering on the raw // centeredOrdinal change is unreliable: navigating to a hit reloads the thread centered on it, // so messageOrdinals is briefly empty (the target not yet present) when the ordinal changes. - // Wait for the target to load, then coarse-scroll and re-assert across the pagination settle. - const lastCenteredOrdinal = React.useRef(0) + // Tracks the index the target sat at, not just the ordinal: the centered load streams older + // messages in above it afterwards, which moves it out from under the scroll we already issued. + const lastCentered = React.useRef<{index: number; ordinal: T.Chat.Ordinal} | undefined>(undefined) React.useEffect(() => { if (centeredOrdinalOrNone <= 0) { - lastCenteredOrdinal.current = 0 - return undefined + lastCentered.current = undefined + return } - if (!messageOrdinals.includes(centeredOrdinalOrNone)) { - return undefined + const index = messageOrdinals.indexOf(centeredOrdinalOrNone) + if (index < 0) { + return } - if (lastCenteredOrdinal.current === centeredOrdinalOrNone) { - return undefined + const last = lastCentered.current + if (last?.ordinal === centeredOrdinalOrNone && last.index === index) { + return } - lastCenteredOrdinal.current = centeredOrdinalOrNone + lastCentered.current = {index, ordinal: centeredOrdinalOrNone} scrollToCentered() - const ids = [50, 250, 500, 900, 1400].map(d => setTimeout(reassertCentered, d)) - return () => { - ids.forEach(clearTimeout) - } - }, [centeredOrdinalOrNone, messageOrdinals, scrollToCentered, reassertCentered]) + }, [centeredOrdinalOrNone, messageOrdinals, scrollToCentered]) // These refs store the conversation they last applied to (not a boolean) so a // freeze/thaw of this screen — which re-mounts effects without a real @@ -866,131 +710,7 @@ const NativeConversationList = function NativeConversationList() { markedConvRef.current = conversationIDKey markInitiallyLoadedThreadAsRead() } - - // Initial bottom position is handled declaratively by initialScrollAtEnd (the list is not - // mounted until data is loaded, so its first render has data). Centered navigation still - // needs an imperative nudge for the case where loaded flips true after centeredOrdinal set. - if (centeredOrdinalOrNone > 0) { - scrollToCentered() - setTimeout(() => { - scrollToCentered() - }, 100) - } - }, [conversationIDKey, centeredOrdinalOrNone, loaded, markInitiallyLoadedThreadAsRead, scrollToCentered]) - - // [LISTDBG] TEMP: diagnose initial-load not landing at bottom on tall-row threads. Dumps list - // state across the load settle: whether it lands short (gap>0) or drifts as rows measure, plus - // where the newest row actually sits (belowVp>0 = parked above) and real per-type avgs vs 120. - const dbgDump = React.useCallback( - (tag: string) => { - const s = listRef.current?.getState() as - | { - isAtEnd?: boolean - scroll?: number - scrollLength?: number - contentLength?: number - end?: number - endBuffered?: number - isWithinMaintainScrollAtEndThreshold?: boolean - getAverageItemSizes?: () => Record - positionAtIndex?: (i: number) => number - sizeAtIndex?: (i: number) => number - } - | undefined - let avgs = '' - try { - const a = s?.getAverageItemSizes?.() - if (a) { - avgs = Object.entries(a) - .map(([k, v]) => `${k}:${Math.round(v.average)}(${v.count})`) - .join(' ') - } - } catch {} - const gap = Math.round((s?.contentLength ?? 0) - (s?.scroll ?? 0) - (s?.scrollLength ?? 0)) - const lastIdx = messageOrdinals.length - 1 - let lastInfo = '' - try { - const posLast = Math.round(s?.positionAtIndex?.(lastIdx) ?? -1) - const sizeLast = Math.round(s?.sizeAtIndex?.(lastIdx) ?? -1) - const vpBottom = Math.round((s?.scroll ?? 0) + (s?.scrollLength ?? 0)) - lastInfo = `lastIdx=${lastIdx} posLast=${posLast} sizeLast=${sizeLast} lastBottom=${posLast + sizeLast} vpBottom=${vpBottom} belowVp=${posLast + sizeLast - vpBottom}` - } catch {} - console.log( - `[LISTDBG] ${tag} conv=${conversationIDKey.slice(0, 6)} num=${messageOrdinals.length} ` + - `isAtEnd=${s?.isAtEnd} withinThresh=${s?.isWithinMaintainScrollAtEndThreshold} ` + - `end=${s?.end} endBuf=${s?.endBuffered} ` + - `scroll=${Math.round(s?.scroll ?? -1)} scrollLen=${Math.round(s?.scrollLength ?? -1)} ` + - `contentLen=${Math.round(s?.contentLength ?? -1)} gap=${gap} ${lastInfo} avgs=[${avgs}]` - ) - }, - [conversationIDKey, messageOrdinals] - ) - // Keyboard-dismiss re-pin: the hide unwind (KeyboardChatScrollView writes the compensating - // contentOffset) shifts LegendList's draw window, so rows that only had estimatedItemSize - // measure in at their real (smaller) sizes mid-dismiss. LegendList's JS MVCP compensates - // against that moving target and settles short of the end, with nothing to re-pin it - // (maintainScrollAtEnd only fires on data changes). If we were pinned at the end when the - // hide started, keep re-pinning frame-by-frame while the keyboard animates down. The - // still-near-end guard keeps an on-drag dismissal that continues scrolling up into history - // from being yanked back down. - // The loop runs from hide start until shortly after the keyboard is fully down (sizes have - // settled by then), so the list visually stays pinned through the whole dismissal instead of - // snapping once at the end. It only issues a scroll when a gap has actually opened. - React.useEffect(() => { - let raf = 0 - let stopId: ReturnType | undefined - const cancel = () => { - cancelAnimationFrame(raf) - raf = 0 - clearTimeout(stopId) - stopId = undefined - } - const getListState = () => - listRef.current?.getState() as - | {isAtEnd?: boolean; scroll?: number; scrollLength?: number; contentLength?: number} - | undefined - const tick = () => { - const s = getListState() - if (s) { - const gap = (s.contentLength ?? 0) - (s.scroll ?? 0) - (s.scrollLength ?? 0) - const stillNearEnd = gap > 1 && gap <= (s.scrollLength ?? 0) - if (stillNearEnd) { - void listRef.current?.scrollToEnd({animated: false}) - } - } - raf = requestAnimationFrame(tick) - } - const subs = [ - KeyboardEvents.addListener('keyboardWillHide', () => { - if (!getListState()?.isAtEnd) return - cancel() - // 1s cap in case keyboardDidHide never fires (e.g. interrupted dismissal) - stopId = setTimeout(cancel, 1000) - raf = requestAnimationFrame(tick) - }), - KeyboardEvents.addListener('keyboardDidHide', () => { - if (!raf) return - clearTimeout(stopId) - // let the last post-dismissal measurements land, then stop tracking - stopId = setTimeout(cancel, 150) - }), - ] - return () => { - cancel() - subs.forEach(sub => sub.remove()) - } - }, []) - - const dbgLoadedRef = React.useRef(undefined) - React.useEffect(() => { - if (!loaded) return undefined - if (dbgLoadedRef.current === conversationIDKey) return undefined - dbgLoadedRef.current = conversationIDKey - const ids = [0, 100, 300, 600, 1200, 2000, 3500].map(d => setTimeout(() => dbgDump(`t+${d}`), d)) - return () => { - ids.forEach(clearTimeout) - } - }, [loaded, conversationIDKey, dbgDump]) + }, [conversationIDKey, loaded, markInitiallyLoadedThreadAsRead]) const initialScrollIndex = useInitialScrollIndex(messageOrdinals, centeredOrdinal) @@ -1013,7 +733,6 @@ const NativeConversationList = function NativeConversationList() { - {listReady ? ( - ) : null} {jumpToRecent && ( {jumpToRecent} diff --git a/shared/patches/@legendapp+list+3.3.5.patch b/shared/patches/@legendapp+list+3.3.5.patch new file mode 100644 index 000000000000..dbbe69f5ec97 --- /dev/null +++ b/shared/patches/@legendapp+list+3.3.5.patch @@ -0,0 +1,9246 @@ +diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js +index b3c5a30..d256c30 100644 +--- a/node_modules/@legendapp/list/react-native.js ++++ b/node_modules/@legendapp/list/react-native.js +@@ -457,611 +457,6 @@ var EDGE_POSITION_EPSILON = 1; + var ENABLE_DEVMODE = IS_DEV && false; + var ENABLE_DEBUG_VIEW = IS_DEV && false; + +-// src/core/cancelImperativeScroll.ts +-function cancelScrollCompletionChecks({ scheduledWork }) { +- scheduledWork.cancel("checkFinishedScrollFrame"); +- scheduledWork.cancel("checkFinishedScrollRetryFrame"); +- scheduledWork.cancel("checkFinishedScrollFallback"); +- scheduledWork.cancel("platformScrollCompletion"); +-} +-function settlePendingImperativeScroll(state) { +- var _a3, _b; +- const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; +- state.pendingScrollResolve = void 0; +- state.pendingScrollToEnd = void 0; +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +-} +-function cancelImperativeScroll(state) { +- cancelScrollCompletionChecks(state); +- state.scheduledWork.cancel("imperativeScrollReady"); +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- settlePendingImperativeScroll(state); +-} +- +-// src/core/deferredPublicOnScroll.ts +-function withResolvedContentOffset(state, event, resolvedOffset) { +- return { +- ...event, +- nativeEvent: { +- ...event.nativeEvent, +- contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } +- } +- }; +-} +-function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { +- var _a3, _b, _c, _d; +- const state = ctx.state; +- const deferredEvent = state.deferredPublicOnScrollEvent; +- state.deferredPublicOnScrollEvent = void 0; +- if (deferredEvent) { +- (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( +- _c, +- withResolvedContentOffset( +- state, +- deferredEvent, +- (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 +- ) +- ); +- } +-} +- +-// src/core/initialScrollSession.ts +-var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +-function hasInitialScrollSessionCompletion(completion) { +- return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); +-} +-function clearInitialScrollSession(state) { +- state.initialScrollSession = void 0; +- return void 0; +-} +-function createInitialScrollSession(options) { +- const { bootstrap, completion, kind, previousDataLength } = options; +- return kind === "offset" ? { +- completion, +- kind, +- previousDataLength +- } : { +- bootstrap, +- completion, +- kind, +- previousDataLength +- }; +-} +-function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { +- var _a4, _b2; +- if (!state.initialScrollSession) { +- state.initialScrollSession = createInitialScrollSession({ +- completion: {}, +- kind, +- previousDataLength: 0 +- }); +- } else if (state.initialScrollSession.kind !== kind) { +- state.initialScrollSession = createInitialScrollSession({ +- bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, +- completion: state.initialScrollSession.completion, +- kind, +- previousDataLength: state.initialScrollSession.previousDataLength +- }); +- } +- (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; +- return state.initialScrollSession.completion; +-} +-var initialScrollCompletion = { +- didDispatchNativeScroll(state) { +- var _a3, _b; +- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); +- }, +- didRetrySilentInitialScroll(state) { +- var _a3, _b; +- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); +- }, +- markInitialScrollNativeDispatch(state) { +- ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; +- }, +- markSilentInitialScrollRetry(state) { +- ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; +- }, +- resetFlags(state) { +- if (!state.initialScrollSession) { +- return; +- } +- const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); +- completion.didDispatchNativeScroll = void 0; +- completion.didRetrySilentInitialScroll = void 0; +- } +-}; +-var initialScrollWatchdog = { +- clear(state) { +- initialScrollWatchdog.set(state, void 0); +- }, +- didReachTarget(newScroll, watchdog) { +- const nextDistance = Math.abs(newScroll - watchdog.targetOffset); +- return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; +- }, +- get(state) { +- var _a3, _b; +- return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; +- }, +- hasNonZeroTargetOffset(targetOffset) { +- return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; +- }, +- isAtZeroTargetOffset(targetOffset) { +- return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; +- }, +- set(state, watchdog) { +- var _a3, _b; +- if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { +- return; +- } +- const completion = ensureInitialScrollSessionCompletion(state); +- completion.watchdog = watchdog ? { +- startScroll: watchdog.startScroll, +- targetOffset: watchdog.targetOffset +- } : void 0; +- } +-}; +-function setInitialScrollSession(state, options = {}) { +- var _a3, _b, _c, _d; +- const existingSession = state.initialScrollSession; +- const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; +- const completion = existingSession == null ? void 0 : existingSession.completion; +- const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; +- const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; +- if (!kind) { +- return clearInitialScrollSession(state); +- } +- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { +- return clearInitialScrollSession(state); +- } +- const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; +- state.initialScrollSession = createInitialScrollSession({ +- bootstrap, +- completion, +- kind, +- previousDataLength +- }); +- return state.initialScrollSession; +-} +- +-// src/utils/checkThreshold.ts +-var HYSTERESIS_MULTIPLIER = 1.3; +-function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { +- const absDistance = Math.abs(distance); +- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; +-} +-var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { +- const absDistance = Math.abs(distance); +- const within = atThreshold || threshold > 0 && absDistance <= threshold; +- const updateSnapshot = () => { +- setSnapshot({ +- atThreshold, +- contentSize: context.contentSize, +- dataLength: context.dataLength, +- scrollPosition: context.scrollPosition +- }); +- }; +- if (!wasReached) { +- if (!within) { +- return false; +- } +- onReached(distance); +- updateSnapshot(); +- return true; +- } +- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); +- if (reset) { +- setSnapshot(void 0); +- return false; +- } +- if (within) { +- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; +- if (changed) { +- updateSnapshot(); +- } +- } +- return true; +-}; +- +-// src/utils/edgeReachedGate.ts +-function resetEdgeLatch(ctx, edge) { +- const state = ctx.state; +- if (edge === "start") { +- state.isStartReached = false; +- state.startReachedSnapshot = void 0; +- } else { +- state.isEndReached = false; +- state.endReachedSnapshot = void 0; +- } +-} +-function resetSharedEdgeGateIfOutsideHysteresis(ctx) { +- const state = ctx.state; +- if (!state.edgeReachedGate) { +- return; +- } +- const contentSize = getContentSize(ctx); +- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); +- const isContentLess = contentSize < state.scrollLength; +- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; +- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; +- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); +- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); +- if (isOutsideStart && isOutsideEnd) { +- state.edgeReachedGate = void 0; +- } +-} +-function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { +- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; +-} +-function markReachedEdge(ctx) { +- ctx.state.edgeReachedGate = "closed"; +-} +-function prepareReachedEdgeForNextUserScroll(ctx) { +- if (ctx.state.edgeReachedGate) { +- ctx.state.edgeReachedGate = "prepared"; +- } +-} +-function beginReachedEdgeUserScroll(ctx, scrollDelta) { +- const state = ctx.state; +- if (state.edgeReachedGate !== "prepared") { +- return void 0; +- } +- const allowedEdge = scrollDelta < 0 ? "start" : "end"; +- state.edgeReachedGate = "closed"; +- resetEdgeLatch(ctx, allowedEdge); +- return allowedEdge; +-} +- +-// src/utils/hasActiveInitialScroll.ts +-function hasActiveInitialScroll(state) { +- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; +-} +- +-// src/utils/checkAtBottom.ts +-function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { +- var _a3; +- const state = ctx.state; +- if (!state) { +- return; +- } +- const { +- queuedInitialLayout, +- scrollLength, +- scroll, +- maintainingScrollAtEnd, +- props: { maintainScrollAtEndThreshold, onEndReachedThreshold } +- } = state; +- const contentSize = getContentSize(ctx); +- resetSharedEdgeGateIfOutsideHysteresis(ctx); +- if (contentSize > 0 && queuedInitialLayout) { +- const insetEnd = getContentInsetEnd(ctx); +- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; +- const isContentLess = contentSize < scrollLength; +- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); +- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); +- set$( +- ctx, +- "isWithinMaintainScrollAtEndThreshold", +- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength +- ); +- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; +- if (!shouldSkipThresholdChecks) { +- state.isEndReached = checkThreshold( +- distanceFromEnd, +- isContentLess, +- onEndReachedThreshold * scrollLength, +- state.isEndReached, +- state.endReachedSnapshot, +- { +- contentSize, +- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, +- scrollPosition: scroll +- }, +- (distance) => { +- var _a4, _b; +- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { +- markReachedEdge(ctx); +- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); +- } +- }, +- (snapshot) => { +- state.endReachedSnapshot = snapshot; +- } +- ); +- } +- } +-} +- +-// src/utils/checkAtTop.ts +-function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { +- const state = ctx == null ? void 0 : ctx.state; +- if (!state) { +- return; +- } +- const { +- isStartReached, +- props: { data, onStartReachedThreshold }, +- scroll, +- scrollLength, +- startReachedSnapshot, +- totalSize +- } = state; +- const dataLength = data.length; +- const threshold = onStartReachedThreshold * scrollLength; +- resetSharedEdgeGateIfOutsideHysteresis(ctx); +- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { +- state.isStartReached = false; +- state.startReachedSnapshot = void 0; +- } +- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); +- set$(ctx, "isNearStart", scroll <= threshold); +- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; +- if (!shouldSkipThresholdChecks) { +- state.isStartReached = checkThreshold( +- scroll, +- false, +- threshold, +- state.isStartReached, +- startReachedSnapshot, +- { +- contentSize: totalSize, +- dataLength, +- scrollPosition: scroll +- }, +- (distance) => { +- var _a3, _b; +- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { +- markReachedEdge(ctx); +- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); +- } +- }, +- (snapshot) => { +- state.startReachedSnapshot = snapshot; +- } +- ); +- } +-} +- +-// src/utils/checkThresholds.ts +-function checkThresholds(ctx, allowedEdge) { +- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; +- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); +- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); +-} +- +-// src/core/recalculateSettledScroll.ts +-function recalculateSettledScroll(ctx) { +- var _a3, _b; +- const state = ctx.state; +- if ((_a3 = state.props) == null ? void 0 : _a3.data) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); +- } +- checkThresholds(ctx); +-} +- +-// src/core/adaptiveRender.ts +-var DEFAULT_ADAPTIVE_RENDER_ENTER_VELOCITY = 3; +-var DEFAULT_ADAPTIVE_RENDER_EXIT_VELOCITY = 1; +-var DEFAULT_ADAPTIVE_RENDER_EXIT_DELAY = 250; +-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; +-function clearAdaptiveRenderExitTimeout(ctx) { +- ctx.state.scheduledWork.cancel("adaptiveRender"); +-} +-function scheduleAdaptiveRenderExit(ctx, exitDelay) { +- const state = ctx.state; +- clearAdaptiveRenderExitTimeout(ctx); +- if (exitDelay <= 0) { +- setAdaptiveRender(ctx, "normal", "scroll"); +- } else { +- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); +- } +-} +-function setAdaptiveRender(ctx, mode, reason) { +- var _a3, _b; +- const previousMode = peek$(ctx, "adaptiveRender"); +- if (previousMode !== mode) { +- set$(ctx, "adaptiveRender", mode); +- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); +- } +-} +-function resetAdaptiveRender(ctx) { +- var _a3, _b; +- clearAdaptiveRenderExitTimeout(ctx); +- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; +- if (peek$(ctx, "adaptiveRender") !== mode) { +- setAdaptiveRender(ctx, mode, "initial"); +- } +-} +-function updateAdaptiveRender(ctx, scrollVelocity, options) { +- var _a3, _b, _c; +- const state = ctx.state; +- const adaptiveRender = state.props.adaptiveRender; +- const currentMode = peek$(ctx, "adaptiveRender"); +- if (peek$(ctx, "readyToRender")) { +- if (adaptiveRender) { +- const isWeb = Platform.OS === "web"; +- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY : DEFAULT_ADAPTIVE_RENDER_ENTER_VELOCITY; +- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY : DEFAULT_ADAPTIVE_RENDER_EXIT_VELOCITY; +- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY : DEFAULT_ADAPTIVE_RENDER_EXIT_DELAY; +- const threshold = currentMode === "light" ? exitVelocity : enterVelocity; +- const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; +- const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; +- if (nextMode !== previousMode) { +- if (nextMode === "light") { +- setAdaptiveRender(ctx, "light", "scroll"); +- scheduleAdaptiveRenderExit(ctx, exitDelay); +- } else if (currentMode === "light") { +- scheduleAdaptiveRenderExit(ctx, exitDelay); +- } +- } +- } else { +- resetAdaptiveRender(ctx); +- } +- } +-} +- +-// src/utils/getEffectiveDrawDistance.ts +-var INITIAL_DRAW_DISTANCE = 50; +-function getEffectiveDrawDistance(ctx, mode) { +- var _a3; +- const drawDistance = ctx.state.props.drawDistance; +- const initialScroll = ctx.state.initialScroll; +- const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; +- const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; +- return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; +-} +-function scheduleFullDrawDistancePrewarm(ctx) { +- const { state } = ctx; +- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { +- return; +- } +- state.scheduledWork.frame(() => { +- var _a3; +- return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +- }, "fullDrawDistancePrewarm"); +-} +- +-// src/utils/setInitialRenderState.ts +-function resetInitialRenderState(ctx, { +- resetLayout, +- resetInitialScroll +-}) { +- const { state } = ctx; +- if (resetLayout) { +- state.didContainersLayout = false; +- state.queuedInitialLayout = false; +- } +- if (resetInitialScroll) { +- state.didFinishInitialScroll = false; +- } +- set$(ctx, "readyToRender", false); +- resetAdaptiveRender(ctx); +-} +-function setInitialRenderState(ctx, { +- didLayout, +- didInitialScroll +-}) { +- const { state } = ctx; +- const { +- loadStartTime, +- props: { onLoad } +- } = state; +- if (didLayout) { +- state.didContainersLayout = true; +- } +- if (didInitialScroll) { +- state.didFinishInitialScroll = true; +- } +- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); +- if (isReadyToRender && !peek$(ctx, "readyToRender")) { +- set$(ctx, "readyToRender", true); +- setAdaptiveRender(ctx, "normal", "ready"); +- if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { +- scheduleFullDrawDistancePrewarm(ctx); +- } +- if (!state.didLoad) { +- state.didLoad = true; +- if (onLoad) { +- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); +- } +- } +- } +-} +- +-// src/core/finishInitialScroll.ts +-var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; +-function syncInitialScrollOffset(state, offset) { +- state.scroll = offset; +- state.scrollPending = offset; +- state.scrollPrev = offset; +-} +-function clearPreservedInitialScrollTargetTimeout(state) { +- state.scheduledWork.cancel("preservedInitialScroll"); +-} +-function clearPreservedInitialScrollTarget(state) { +- clearPreservedInitialScrollTargetTimeout(state); +- state.clearPreservedInitialScrollOnNextFinish = void 0; +- state.initialScroll = void 0; +- setInitialScrollSession(state); +-} +-function supersedeInitialScroll(ctx) { +- var _a3, _b, _c; +- const state = ctx.state; +- const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; +- if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { +- if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { +- cancelAnimationFrame(bootstrapInitialScroll.frameHandle); +- } +- if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { +- cancelScrollCompletionChecks(state); +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- } +- initialScrollCompletion.resetFlags(state); +- setInitialScrollSession(state, { bootstrap: null }); +- finishInitialScroll(ctx); +- } +-} +-function finishInitialScroll(ctx, options) { +- var _a3, _b, _c; +- const state = ctx.state; +- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { +- syncInitialScrollOffset(state, options.resolvedOffset); +- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { +- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); +- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { +- syncInitialScrollOffset(state, observedOffset); +- } +- } +- const complete = () => { +- var _a4, _b2, _c2, _d, _e; +- const shouldReleaseDeferredPublicOnScroll = Platform.OS === "web" && ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; +- const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0; +- initialScrollWatchdog.clear(state); +- if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) { +- state.clearPreservedInitialScrollOnNextFinish = void 0; +- setInitialScrollSession(state); +- clearPreservedInitialScrollTargetTimeout(state); +- if (options == null ? void 0 : options.schedulePreservedTargetClear) { +- state.scheduledWork.timeout( +- () => { +- var _a5; +- if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) { +- return; +- } +- clearPreservedInitialScrollTarget(state); +- }, +- PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, +- "preservedInitialScroll" +- ); +- } +- } else { +- clearPreservedInitialScrollTarget(state); +- } +- if (options == null ? void 0 : options.recalculateItems) { +- recalculateSettledScroll(ctx); +- } +- setInitialRenderState(ctx, { didInitialScroll: true }); +- if (shouldReleaseDeferredPublicOnScroll) { +- releaseDeferredPublicOnScroll(ctx, finalScrollOffset); +- } +- (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); +- }; +- if (options == null ? void 0 : options.waitForCompletionFrame) { +- requestAnimationFrame(complete); +- return; +- } +- complete(); +-} +- +-// src/core/calculateOffsetForIndex.ts +-function calculateOffsetForIndex(ctx, index) { +- const state = ctx.state; +- return index !== void 0 ? state.positions[index] || 0 : 0; +-} +- + // src/core/getStartOffsetAdjustment.ts + function getStartOffsetAdjustment(ctx) { + const { state } = ctx; +@@ -1265,58 +660,279 @@ function getItemSizeAtIndex(ctx, index) { + if (index === void 0 || index < 0) { + return void 0; + } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; ++ } ++ if (index !== void 0) { ++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); ++ if (startOffsetAdjustment) { ++ offset += startOffsetAdjustment; ++ } ++ } ++ if (viewPosition !== void 0 && index !== void 0) { ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return offset; ++ } ++ const isOutOfBounds = index < 0 || index >= dataLength; ++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; ++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); ++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); ++ const trailingInset = getContentInsetEnd(ctx); ++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); ++ if (!isOutOfBounds && index === state.props.data.length - 1) { ++ const footerSize = peek$(ctx, "footerSize") || 0; ++ offset += footerSize; ++ } ++ } ++ return offset; ++} ++ ++// src/core/clampScrollOffset.ts ++function clampScrollOffset(ctx, offset, scrollTarget) { ++ const state = ctx.state; ++ const contentSize = getContentSize(ctx); ++ let clampedOffset = offset; ++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android" || state.lastLayout)) { ++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); ++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; ++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; ++ const maxOffset = baseMaxOffset + extraEndOffset; ++ clampedOffset = Math.min(offset, maxOffset); ++ } ++ clampedOffset = Math.max(0, clampedOffset); ++ return clampedOffset; ++} ++ ++// src/core/calculateOffsetForIndex.ts ++function calculateOffsetForIndex(ctx, index) { ++ const state = ctx.state; ++ return index !== void 0 ? state.positions[index] || 0 : 0; ++} ++ ++// src/utils/checkThreshold.ts ++var HYSTERESIS_MULTIPLIER = 1.3; ++function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { ++ const absDistance = Math.abs(distance); ++ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; ++} ++var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { ++ const absDistance = Math.abs(distance); ++ const within = atThreshold || threshold > 0 && absDistance <= threshold; ++ const updateSnapshot = () => { ++ setSnapshot({ ++ atThreshold, ++ contentSize: context.contentSize, ++ dataLength: context.dataLength, ++ scrollPosition: context.scrollPosition ++ }); ++ }; ++ if (!wasReached) { ++ if (!within) { ++ return false; ++ } ++ onReached(distance); ++ updateSnapshot(); ++ return true; ++ } ++ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); ++ if (reset) { ++ setSnapshot(void 0); ++ return false; ++ } ++ if (within) { ++ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; ++ if (changed) { ++ updateSnapshot(); ++ } ++ } ++ return true; ++}; ++ ++// src/utils/edgeReachedGate.ts ++function resetEdgeLatch(ctx, edge) { ++ const state = ctx.state; ++ if (edge === "start") { ++ state.isStartReached = false; ++ state.startReachedSnapshot = void 0; ++ } else { ++ state.isEndReached = false; ++ state.endReachedSnapshot = void 0; ++ } ++} ++function resetSharedEdgeGateIfOutsideHysteresis(ctx) { ++ const state = ctx.state; ++ if (!state.edgeReachedGate) { ++ return; ++ } ++ const contentSize = getContentSize(ctx); ++ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); ++ const isContentLess = contentSize < state.scrollLength; ++ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; ++ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; ++ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); ++ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); ++ if (isOutsideStart && isOutsideEnd) { ++ state.edgeReachedGate = void 0; ++ } ++} ++function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { ++ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; ++} ++function markReachedEdge(ctx) { ++ ctx.state.edgeReachedGate = "closed"; ++} ++function prepareReachedEdgeForNextUserScroll(ctx) { ++ if (ctx.state.edgeReachedGate) { ++ ctx.state.edgeReachedGate = "prepared"; ++ } ++} ++function beginReachedEdgeUserScroll(ctx, scrollDelta) { ++ const state = ctx.state; ++ if (state.edgeReachedGate !== "prepared") { ++ return void 0; ++ } ++ const allowedEdge = scrollDelta < 0 ? "start" : "end"; ++ state.edgeReachedGate = "closed"; ++ resetEdgeLatch(ctx, allowedEdge); ++ return allowedEdge; ++} ++ ++// src/utils/hasActiveInitialScroll.ts ++function hasActiveInitialScroll(state) { ++ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; ++} ++ ++// src/utils/checkAtBottom.ts ++function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { ++ var _a3; ++ const state = ctx.state; ++ if (!state) { ++ return; ++ } ++ const { ++ queuedInitialLayout, ++ scrollLength, ++ scroll, ++ maintainingScrollAtEnd, ++ props: { maintainScrollAtEndThreshold, onEndReachedThreshold } ++ } = state; ++ const contentSize = getContentSize(ctx); ++ resetSharedEdgeGateIfOutsideHysteresis(ctx); ++ if (contentSize > 0 && queuedInitialLayout) { ++ const insetEnd = getContentInsetEnd(ctx); ++ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; ++ const isContentLess = contentSize < scrollLength; ++ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); ++ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); ++ set$( ++ ctx, ++ "isWithinMaintainScrollAtEndThreshold", ++ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength ++ ); ++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; ++ if (!shouldSkipThresholdChecks) { ++ state.isEndReached = checkThreshold( ++ distanceFromEnd, ++ isContentLess, ++ onEndReachedThreshold * scrollLength, ++ state.isEndReached, ++ state.endReachedSnapshot, ++ { ++ contentSize, ++ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, ++ scrollPosition: scroll ++ }, ++ (distance) => { ++ var _a4, _b; ++ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { ++ markReachedEdge(ctx); ++ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); ++ } ++ }, ++ (snapshot) => { ++ state.endReachedSnapshot = snapshot; ++ } ++ ); ++ } ++ } ++} ++ ++// src/utils/checkAtTop.ts ++function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { ++ const state = ctx == null ? void 0 : ctx.state; ++ if (!state) { ++ return; ++ } ++ const { ++ isStartReached, ++ props: { data, onStartReachedThreshold }, ++ scroll, ++ scrollLength, ++ startReachedSnapshot, ++ totalSize ++ } = state; ++ const dataLength = data.length; ++ const threshold = onStartReachedThreshold * scrollLength; ++ resetSharedEdgeGateIfOutsideHysteresis(ctx); ++ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { ++ state.isStartReached = false; ++ state.startReachedSnapshot = void 0; ++ } ++ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); ++ set$(ctx, "isNearStart", scroll <= threshold); ++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; ++ if (!shouldSkipThresholdChecks) { ++ state.isStartReached = checkThreshold( ++ scroll, ++ false, ++ threshold, ++ state.isStartReached, ++ startReachedSnapshot, ++ { ++ contentSize: totalSize, ++ dataLength, ++ scrollPosition: scroll ++ }, ++ (distance) => { ++ var _a3, _b; ++ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { ++ markReachedEdge(ctx); ++ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); ++ } ++ }, ++ (snapshot) => { ++ state.startReachedSnapshot = snapshot; ++ } ++ ); ++ } + } + +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +- } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } +- } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } +- } +- return offset; ++// src/utils/checkThresholds.ts ++function checkThresholds(ctx, allowedEdge) { ++ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; ++ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); ++ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + } + +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { ++// src/core/recalculateSettledScroll.ts ++function recalculateSettledScroll(ctx) { ++ var _a3, _b; + const state = ctx.state; +- const contentSize = getContentSize(ctx); +- let clampedOffset = offset; +- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android" || state.lastLayout)) { +- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); +- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; +- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; +- const maxOffset = baseMaxOffset + extraEndOffset; +- clampedOffset = Math.min(offset, maxOffset); ++ if ((_a3 = state.props) == null ? void 0 : _a3.data) { ++ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); + } +- clampedOffset = Math.max(0, clampedOffset); +- return clampedOffset; ++ checkThresholds(ctx); + } + + // src/core/finishScrollTo.ts +@@ -1357,6 +973,124 @@ function finishScrollTo(ctx) { + } + } + ++// src/core/initialScrollSession.ts ++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; ++function hasInitialScrollSessionCompletion(completion) { ++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); ++} ++function clearInitialScrollSession(state) { ++ state.initialScrollSession = void 0; ++ return void 0; ++} ++function createInitialScrollSession(options) { ++ const { bootstrap, completion, kind, previousDataLength } = options; ++ return kind === "offset" ? { ++ completion, ++ kind, ++ previousDataLength ++ } : { ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }; ++} ++function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { ++ var _a4, _b2; ++ if (!state.initialScrollSession) { ++ state.initialScrollSession = createInitialScrollSession({ ++ completion: {}, ++ kind, ++ previousDataLength: 0 ++ }); ++ } else if (state.initialScrollSession.kind !== kind) { ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, ++ completion: state.initialScrollSession.completion, ++ kind, ++ previousDataLength: state.initialScrollSession.previousDataLength ++ }); ++ } ++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; ++ return state.initialScrollSession.completion; ++} ++var initialScrollCompletion = { ++ didDispatchNativeScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); ++ }, ++ didRetrySilentInitialScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); ++ }, ++ markInitialScrollNativeDispatch(state) { ++ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; ++ }, ++ markSilentInitialScrollRetry(state) { ++ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; ++ }, ++ resetFlags(state) { ++ if (!state.initialScrollSession) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); ++ completion.didDispatchNativeScroll = void 0; ++ completion.didRetrySilentInitialScroll = void 0; ++ } ++}; ++var initialScrollWatchdog = { ++ clear(state) { ++ initialScrollWatchdog.set(state, void 0); ++ }, ++ didReachTarget(newScroll, watchdog) { ++ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); ++ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ get(state) { ++ var _a3, _b; ++ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; ++ }, ++ hasNonZeroTargetOffset(targetOffset) { ++ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ isAtZeroTargetOffset(targetOffset) { ++ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ set(state, watchdog) { ++ var _a3, _b; ++ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state); ++ completion.watchdog = watchdog ? { ++ startScroll: watchdog.startScroll, ++ targetOffset: watchdog.targetOffset ++ } : void 0; ++ } ++}; ++function setInitialScrollSession(state, options = {}) { ++ var _a3, _b, _c, _d; ++ const existingSession = state.initialScrollSession; ++ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; ++ const completion = existingSession == null ? void 0 : existingSession.completion; ++ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; ++ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; ++ if (!kind) { ++ return clearInitialScrollSession(state); ++ } ++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { ++ return clearInitialScrollSession(state); ++ } ++ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }); ++ return state.initialScrollSession; ++} ++ + // src/core/checkFinishedScroll.ts + var INITIAL_SCROLL_MAX_FALLBACK_CHECKS = 20; + var INITIAL_SCROLL_COMPLETION_TARGET_EPSILON = 1; +@@ -1549,6 +1283,69 @@ function doScrollTo(ctx, params) { + } + } + ++// src/core/adaptiveRender.ts ++var DEFAULT_ADAPTIVE_RENDER_ENTER_VELOCITY = 3; ++var DEFAULT_ADAPTIVE_RENDER_EXIT_VELOCITY = 1; ++var DEFAULT_ADAPTIVE_RENDER_EXIT_DELAY = 250; ++var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; ++function clearAdaptiveRenderExitTimeout(ctx) { ++ ctx.state.scheduledWork.cancel("adaptiveRender"); ++} ++function scheduleAdaptiveRenderExit(ctx, exitDelay) { ++ const state = ctx.state; ++ clearAdaptiveRenderExitTimeout(ctx); ++ if (exitDelay <= 0) { ++ setAdaptiveRender(ctx, "normal", "scroll"); ++ } else { ++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); ++ } ++} ++function setAdaptiveRender(ctx, mode, reason) { ++ var _a3, _b; ++ const previousMode = peek$(ctx, "adaptiveRender"); ++ if (previousMode !== mode) { ++ set$(ctx, "adaptiveRender", mode); ++ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); ++ } ++} ++function resetAdaptiveRender(ctx) { ++ var _a3, _b; ++ clearAdaptiveRenderExitTimeout(ctx); ++ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; ++ if (peek$(ctx, "adaptiveRender") !== mode) { ++ setAdaptiveRender(ctx, mode, "initial"); ++ } ++} ++function updateAdaptiveRender(ctx, scrollVelocity, options) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ const adaptiveRender = state.props.adaptiveRender; ++ const currentMode = peek$(ctx, "adaptiveRender"); ++ if (peek$(ctx, "readyToRender")) { ++ if (adaptiveRender) { ++ const isWeb = Platform.OS === "web"; ++ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY : DEFAULT_ADAPTIVE_RENDER_ENTER_VELOCITY; ++ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY : DEFAULT_ADAPTIVE_RENDER_EXIT_VELOCITY; ++ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY : DEFAULT_ADAPTIVE_RENDER_EXIT_DELAY; ++ const threshold = currentMode === "light" ? exitVelocity : enterVelocity; ++ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; ++ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; ++ if (nextMode !== previousMode) { ++ if (nextMode === "light") { ++ setAdaptiveRender(ctx, "light", "scroll"); ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } else if (currentMode === "light") { ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } ++ } ++ } else { ++ resetAdaptiveRender(ctx); ++ } ++ } ++} ++ + // src/core/doMaintainScrollAtEnd.ts + function doMaintainScrollAtEnd(ctx) { + const state = ctx.state; +@@ -1977,6 +1774,27 @@ var flushSync = (fn) => { + fn(); + }; + ++// src/utils/getEffectiveDrawDistance.ts ++var INITIAL_DRAW_DISTANCE = 50; ++function getEffectiveDrawDistance(ctx, mode) { ++ var _a3; ++ const drawDistance = ctx.state.props.drawDistance; ++ const initialScroll = ctx.state.initialScroll; ++ const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; ++ const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; ++ return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; ++} ++function scheduleFullDrawDistancePrewarm(ctx) { ++ const { state } = ctx; ++ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { ++ return; ++ } ++ state.scheduledWork.frame(() => { ++ var _a3; ++ return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); ++ }, "fullDrawDistancePrewarm"); ++} ++ + // src/utils/getScrollVelocity.ts + var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; + var SCROLL_VELOCITY_HALF_LIFE_MS = 200; +@@ -2224,7 +2042,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + } + } + function scrollTo(ctx, params) { +- var _a3, _b; ++ var _a3, _b, _c; + const state = ctx.state; + const { noScrollingTo, forceScroll, ...scrollTarget } = params; + const { +@@ -2256,6 +2074,15 @@ function scrollTo(ctx, params) { + }; + if (!isInitialScroll) { + pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); ++ if (scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { ++ beginScrollTargetSettle(ctx, { ++ index: scrollTarget.index, ++ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, ++ viewPosition: scrollTarget.viewPosition ++ }); ++ } else { ++ clearScrollTargetSettle(state); ++ } + } + } + state.scrollPending = targetOffset; +@@ -2263,7 +2090,7 @@ function scrollTo(ctx, params) { + if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { + if (animated) { + if (state.scrollTargetPinnedRange) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); + } + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); +@@ -2276,6 +2103,274 @@ function scrollTo(ctx, params) { + } + } + ++// src/core/scrollTargetSettle.ts ++var SETTLE_POSITION_EPSILON = 0.5; ++var SETTLE_QUIET_PASSES_TO_RELEASE = 2; ++var SETTLE_TTL_MS = 500; ++var SETTLE_MAX_MS = 1e3; ++var SETTLE_MAX_CORRECTIONS = 8; ++function clearScrollTargetSettle(state) { ++ state.scrollTargetSettle = void 0; ++} ++function beginScrollTargetSettle(ctx, params) { ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ const { data } = state.props; ++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; ++ if (index < 0 || index >= data.length || isEndAlignedLastItem) { ++ clearScrollTargetSettle(state); ++ return; ++ } ++ const now = Date.now(); ++ const id = getId(state, index); ++ const existing = state.scrollTargetSettle; ++ const isSameTarget = (existing == null ? void 0 : existing.id) === id; ++ state.scrollTargetSettle = { ++ corrections: isSameTarget ? existing.corrections : 0, ++ deadline: isSameTarget ? existing.deadline : now + SETTLE_MAX_MS, ++ expiresAt: now + SETTLE_TTL_MS, ++ id, ++ quietPasses: 0, ++ viewOffset, ++ viewPosition ++ }; ++} ++function getSettleTargetOffset(ctx, settle, index, position) { ++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; ++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); ++} ++function settleScrollTarget(ctx, isCompensating) { ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ return false; ++ } ++ if (isCompensating) { ++ settle.quietPasses = 0; ++ settle.expiresAt = Date.now() + SETTLE_TTL_MS; ++ return false; ++ } ++ const index = state.indexByKey.get(settle.id); ++ const position = index === void 0 ? void 0 : state.positions[index]; ++ if (index === void 0 || position === void 0) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ const now = Date.now(); ++ if (now > settle.expiresAt || now > settle.deadline) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position); ++ const diff = targetOffset - state.scroll; ++ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ settle.quietPasses++; ++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { ++ clearScrollTargetSettle(state); ++ } ++ return false; ++ } ++ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ settle.quietPasses = 0; ++ settle.corrections++; ++ settle.expiresAt = now + SETTLE_TTL_MS; ++ scrollTo(ctx, { ++ animated: false, ++ index, ++ itemSize: getItemSizeAtIndex(ctx, index), ++ offset: position, ++ viewOffset: settle.viewOffset, ++ viewPosition: settle.viewPosition ++ }); ++ return true; ++} ++ ++// src/core/cancelImperativeScroll.ts ++function cancelScrollCompletionChecks({ scheduledWork }) { ++ scheduledWork.cancel("checkFinishedScrollFrame"); ++ scheduledWork.cancel("checkFinishedScrollRetryFrame"); ++ scheduledWork.cancel("checkFinishedScrollFallback"); ++ scheduledWork.cancel("platformScrollCompletion"); ++} ++function settlePendingImperativeScroll(state) { ++ var _a3, _b; ++ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; ++ state.pendingScrollResolve = void 0; ++ state.pendingScrollToEnd = void 0; ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); ++} ++function cancelImperativeScroll(state) { ++ cancelScrollCompletionChecks(state); ++ state.scheduledWork.cancel("imperativeScrollReady"); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ clearScrollTargetSettle(state); ++ settlePendingImperativeScroll(state); ++} ++ ++// src/core/deferredPublicOnScroll.ts ++function withResolvedContentOffset(state, event, resolvedOffset) { ++ return { ++ ...event, ++ nativeEvent: { ++ ...event.nativeEvent, ++ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } ++ } ++ }; ++} ++function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { ++ var _a3, _b, _c, _d; ++ const state = ctx.state; ++ const deferredEvent = state.deferredPublicOnScrollEvent; ++ state.deferredPublicOnScrollEvent = void 0; ++ if (deferredEvent) { ++ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( ++ _c, ++ withResolvedContentOffset( ++ state, ++ deferredEvent, ++ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 ++ ) ++ ); ++ } ++} ++ ++// src/utils/setInitialRenderState.ts ++function resetInitialRenderState(ctx, { ++ resetLayout, ++ resetInitialScroll ++}) { ++ const { state } = ctx; ++ if (resetLayout) { ++ state.didContainersLayout = false; ++ state.queuedInitialLayout = false; ++ } ++ if (resetInitialScroll) { ++ state.didFinishInitialScroll = false; ++ } ++ set$(ctx, "readyToRender", false); ++ resetAdaptiveRender(ctx); ++} ++function setInitialRenderState(ctx, { ++ didLayout, ++ didInitialScroll ++}) { ++ const { state } = ctx; ++ const { ++ loadStartTime, ++ props: { onLoad } ++ } = state; ++ if (didLayout) { ++ state.didContainersLayout = true; ++ } ++ if (didInitialScroll) { ++ state.didFinishInitialScroll = true; ++ } ++ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); ++ if (isReadyToRender && !peek$(ctx, "readyToRender")) { ++ set$(ctx, "readyToRender", true); ++ setAdaptiveRender(ctx, "normal", "ready"); ++ if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { ++ scheduleFullDrawDistancePrewarm(ctx); ++ } ++ if (!state.didLoad) { ++ state.didLoad = true; ++ if (onLoad) { ++ onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); ++ } ++ } ++ } ++} ++ ++// src/core/finishInitialScroll.ts ++var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; ++function syncInitialScrollOffset(state, offset) { ++ state.scroll = offset; ++ state.scrollPending = offset; ++ state.scrollPrev = offset; ++} ++function clearPreservedInitialScrollTargetTimeout(state) { ++ state.scheduledWork.cancel("preservedInitialScroll"); ++} ++function clearPreservedInitialScrollTarget(state) { ++ clearPreservedInitialScrollTargetTimeout(state); ++ state.clearPreservedInitialScrollOnNextFinish = void 0; ++ state.initialScroll = void 0; ++ setInitialScrollSession(state); ++} ++function supersedeInitialScroll(ctx) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; ++ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { ++ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { ++ cancelAnimationFrame(bootstrapInitialScroll.frameHandle); ++ } ++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { ++ cancelScrollCompletionChecks(state); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ } ++ initialScrollCompletion.resetFlags(state); ++ setInitialScrollSession(state, { bootstrap: null }); ++ finishInitialScroll(ctx); ++ } ++} ++function finishInitialScroll(ctx, options) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { ++ syncInitialScrollOffset(state, options.resolvedOffset); ++ } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { ++ const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); ++ if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { ++ syncInitialScrollOffset(state, observedOffset); ++ } ++ } ++ const complete = () => { ++ var _a4, _b2, _c2, _d, _e; ++ const shouldReleaseDeferredPublicOnScroll = Platform.OS === "web" && ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; ++ const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0; ++ initialScrollWatchdog.clear(state); ++ if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) { ++ state.clearPreservedInitialScrollOnNextFinish = void 0; ++ setInitialScrollSession(state); ++ clearPreservedInitialScrollTargetTimeout(state); ++ if (options == null ? void 0 : options.schedulePreservedTargetClear) { ++ state.scheduledWork.timeout( ++ () => { ++ var _a5; ++ if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) { ++ return; ++ } ++ clearPreservedInitialScrollTarget(state); ++ }, ++ PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, ++ "preservedInitialScroll" ++ ); ++ } ++ } else { ++ clearPreservedInitialScrollTarget(state); ++ } ++ if (options == null ? void 0 : options.recalculateItems) { ++ recalculateSettledScroll(ctx); ++ } ++ setInitialRenderState(ctx, { didInitialScroll: true }); ++ if (shouldReleaseDeferredPublicOnScroll) { ++ releaseDeferredPublicOnScroll(ctx, finalScrollOffset); ++ } ++ (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); ++ }; ++ if (options == null ? void 0 : options.waitForCompletionFrame) { ++ requestAnimationFrame(complete); ++ return; ++ } ++ complete(); ++} ++ + // src/core/scrollToIndex.ts + function clampScrollIndex(index, dataLength) { + if (dataLength <= 0) { +@@ -4337,7 +4432,10 @@ function calculateItemsInView(ctx, params = {}) { + const scrollBeforeMVCP = state.scroll; + const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; + checkMVCP == null ? void 0 : checkMVCP(); +- const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); ++ const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; ++ const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); ++ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx, didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0); ++ const didMVCPAdjustScroll = didMVCPAdjust || didSettleScrollTarget; + if (didMVCPAdjustScroll) { + updateScroll2(state.scroll); + updateScrollRange(); +@@ -6557,6 +6655,22 @@ function createColumnWrapperStyle(contentContainerStyle) { + } + } + ++// src/core/isScrollExtentSynced.ts ++var SCROLL_EXTENT_EPSILON = 1; ++function isScrollExtentSynced(ctx) { ++ var _a3, _b; ++ const state = ctx.state; ++ const platformMaxOffset = (_b = (_a3 = state.refScroller.current) == null ? void 0 : _a3.getMaxScrollOffset) == null ? void 0 : _b.call(_a3); ++ if (platformMaxOffset === void 0) { ++ return true; ++ } ++ if (state.scrollLength <= 0) { ++ return true; ++ } ++ const contentMaxOffset = Math.max(0, getContentSize(ctx) - state.scrollLength); ++ return platformMaxOffset >= contentMaxOffset - SCROLL_EXTENT_EPSILON; ++} ++ + // src/core/scrollToEnd.ts + function scrollToEnd(ctx, options) { + const state = ctx.state; +@@ -6606,6 +6720,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { + const IMPERATIVE_SCROLL_SETTLE_STABLE_FRAMES = 2; + let imperativeScrollToken = 0; + const isSettlingAfterDataChange = () => !!state.didDataChange || !!state.didColumnsChange || state.scheduledWork.has("mvcpRecalculate") || state.ignoreScrollFromMVCP !== void 0; ++ const isScrollBlocked = () => isSettlingAfterDataChange() || !isScrollExtentSynced(ctx); + const isScrollToIndexReady = (targetIndex, allowEmpty = false) => { + var _a3; + const props = state.props; +@@ -6629,7 +6744,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { + if (token !== imperativeScrollToken) { + return; + } +- if (isSettlingAfterDataChange() || !isReady()) { ++ if (isScrollBlocked() || !isReady()) { + stableFrames = 0; + } else { + stableFrames += 1; +@@ -6656,7 +6771,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { + resolve(); + } + }; +- if (isSettlingAfterDataChange() || !isReady()) { ++ if (isScrollBlocked() || !isReady()) { + runWhenReady(token, runNow, isReady); + } else { + runNow(); +@@ -7652,6 +7767,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + onScrollBeginDrag: (event) => { + var _a4, _b2; + prepareReachedEdgeForNextUserScroll(ctx); ++ clearScrollTargetSettle(state); + (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); + }, + onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) +diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs +index 40e87cd..af27abe 100644 +--- a/node_modules/@legendapp/list/react-native.mjs ++++ b/node_modules/@legendapp/list/react-native.mjs +@@ -436,611 +436,6 @@ var EDGE_POSITION_EPSILON = 1; + var ENABLE_DEVMODE = IS_DEV && false; + var ENABLE_DEBUG_VIEW = IS_DEV && false; + +-// src/core/cancelImperativeScroll.ts +-function cancelScrollCompletionChecks({ scheduledWork }) { +- scheduledWork.cancel("checkFinishedScrollFrame"); +- scheduledWork.cancel("checkFinishedScrollRetryFrame"); +- scheduledWork.cancel("checkFinishedScrollFallback"); +- scheduledWork.cancel("platformScrollCompletion"); +-} +-function settlePendingImperativeScroll(state) { +- var _a3, _b; +- const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; +- state.pendingScrollResolve = void 0; +- state.pendingScrollToEnd = void 0; +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +-} +-function cancelImperativeScroll(state) { +- cancelScrollCompletionChecks(state); +- state.scheduledWork.cancel("imperativeScrollReady"); +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- settlePendingImperativeScroll(state); +-} +- +-// src/core/deferredPublicOnScroll.ts +-function withResolvedContentOffset(state, event, resolvedOffset) { +- return { +- ...event, +- nativeEvent: { +- ...event.nativeEvent, +- contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } +- } +- }; +-} +-function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { +- var _a3, _b, _c, _d; +- const state = ctx.state; +- const deferredEvent = state.deferredPublicOnScrollEvent; +- state.deferredPublicOnScrollEvent = void 0; +- if (deferredEvent) { +- (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( +- _c, +- withResolvedContentOffset( +- state, +- deferredEvent, +- (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 +- ) +- ); +- } +-} +- +-// src/core/initialScrollSession.ts +-var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +-function hasInitialScrollSessionCompletion(completion) { +- return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); +-} +-function clearInitialScrollSession(state) { +- state.initialScrollSession = void 0; +- return void 0; +-} +-function createInitialScrollSession(options) { +- const { bootstrap, completion, kind, previousDataLength } = options; +- return kind === "offset" ? { +- completion, +- kind, +- previousDataLength +- } : { +- bootstrap, +- completion, +- kind, +- previousDataLength +- }; +-} +-function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { +- var _a4, _b2; +- if (!state.initialScrollSession) { +- state.initialScrollSession = createInitialScrollSession({ +- completion: {}, +- kind, +- previousDataLength: 0 +- }); +- } else if (state.initialScrollSession.kind !== kind) { +- state.initialScrollSession = createInitialScrollSession({ +- bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, +- completion: state.initialScrollSession.completion, +- kind, +- previousDataLength: state.initialScrollSession.previousDataLength +- }); +- } +- (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; +- return state.initialScrollSession.completion; +-} +-var initialScrollCompletion = { +- didDispatchNativeScroll(state) { +- var _a3, _b; +- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); +- }, +- didRetrySilentInitialScroll(state) { +- var _a3, _b; +- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); +- }, +- markInitialScrollNativeDispatch(state) { +- ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; +- }, +- markSilentInitialScrollRetry(state) { +- ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; +- }, +- resetFlags(state) { +- if (!state.initialScrollSession) { +- return; +- } +- const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); +- completion.didDispatchNativeScroll = void 0; +- completion.didRetrySilentInitialScroll = void 0; +- } +-}; +-var initialScrollWatchdog = { +- clear(state) { +- initialScrollWatchdog.set(state, void 0); +- }, +- didReachTarget(newScroll, watchdog) { +- const nextDistance = Math.abs(newScroll - watchdog.targetOffset); +- return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; +- }, +- get(state) { +- var _a3, _b; +- return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; +- }, +- hasNonZeroTargetOffset(targetOffset) { +- return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; +- }, +- isAtZeroTargetOffset(targetOffset) { +- return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; +- }, +- set(state, watchdog) { +- var _a3, _b; +- if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { +- return; +- } +- const completion = ensureInitialScrollSessionCompletion(state); +- completion.watchdog = watchdog ? { +- startScroll: watchdog.startScroll, +- targetOffset: watchdog.targetOffset +- } : void 0; +- } +-}; +-function setInitialScrollSession(state, options = {}) { +- var _a3, _b, _c, _d; +- const existingSession = state.initialScrollSession; +- const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; +- const completion = existingSession == null ? void 0 : existingSession.completion; +- const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; +- const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; +- if (!kind) { +- return clearInitialScrollSession(state); +- } +- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { +- return clearInitialScrollSession(state); +- } +- const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; +- state.initialScrollSession = createInitialScrollSession({ +- bootstrap, +- completion, +- kind, +- previousDataLength +- }); +- return state.initialScrollSession; +-} +- +-// src/utils/checkThreshold.ts +-var HYSTERESIS_MULTIPLIER = 1.3; +-function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { +- const absDistance = Math.abs(distance); +- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; +-} +-var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { +- const absDistance = Math.abs(distance); +- const within = atThreshold || threshold > 0 && absDistance <= threshold; +- const updateSnapshot = () => { +- setSnapshot({ +- atThreshold, +- contentSize: context.contentSize, +- dataLength: context.dataLength, +- scrollPosition: context.scrollPosition +- }); +- }; +- if (!wasReached) { +- if (!within) { +- return false; +- } +- onReached(distance); +- updateSnapshot(); +- return true; +- } +- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); +- if (reset) { +- setSnapshot(void 0); +- return false; +- } +- if (within) { +- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; +- if (changed) { +- updateSnapshot(); +- } +- } +- return true; +-}; +- +-// src/utils/edgeReachedGate.ts +-function resetEdgeLatch(ctx, edge) { +- const state = ctx.state; +- if (edge === "start") { +- state.isStartReached = false; +- state.startReachedSnapshot = void 0; +- } else { +- state.isEndReached = false; +- state.endReachedSnapshot = void 0; +- } +-} +-function resetSharedEdgeGateIfOutsideHysteresis(ctx) { +- const state = ctx.state; +- if (!state.edgeReachedGate) { +- return; +- } +- const contentSize = getContentSize(ctx); +- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); +- const isContentLess = contentSize < state.scrollLength; +- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; +- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; +- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); +- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); +- if (isOutsideStart && isOutsideEnd) { +- state.edgeReachedGate = void 0; +- } +-} +-function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { +- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; +-} +-function markReachedEdge(ctx) { +- ctx.state.edgeReachedGate = "closed"; +-} +-function prepareReachedEdgeForNextUserScroll(ctx) { +- if (ctx.state.edgeReachedGate) { +- ctx.state.edgeReachedGate = "prepared"; +- } +-} +-function beginReachedEdgeUserScroll(ctx, scrollDelta) { +- const state = ctx.state; +- if (state.edgeReachedGate !== "prepared") { +- return void 0; +- } +- const allowedEdge = scrollDelta < 0 ? "start" : "end"; +- state.edgeReachedGate = "closed"; +- resetEdgeLatch(ctx, allowedEdge); +- return allowedEdge; +-} +- +-// src/utils/hasActiveInitialScroll.ts +-function hasActiveInitialScroll(state) { +- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; +-} +- +-// src/utils/checkAtBottom.ts +-function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { +- var _a3; +- const state = ctx.state; +- if (!state) { +- return; +- } +- const { +- queuedInitialLayout, +- scrollLength, +- scroll, +- maintainingScrollAtEnd, +- props: { maintainScrollAtEndThreshold, onEndReachedThreshold } +- } = state; +- const contentSize = getContentSize(ctx); +- resetSharedEdgeGateIfOutsideHysteresis(ctx); +- if (contentSize > 0 && queuedInitialLayout) { +- const insetEnd = getContentInsetEnd(ctx); +- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; +- const isContentLess = contentSize < scrollLength; +- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); +- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); +- set$( +- ctx, +- "isWithinMaintainScrollAtEndThreshold", +- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength +- ); +- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; +- if (!shouldSkipThresholdChecks) { +- state.isEndReached = checkThreshold( +- distanceFromEnd, +- isContentLess, +- onEndReachedThreshold * scrollLength, +- state.isEndReached, +- state.endReachedSnapshot, +- { +- contentSize, +- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, +- scrollPosition: scroll +- }, +- (distance) => { +- var _a4, _b; +- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { +- markReachedEdge(ctx); +- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); +- } +- }, +- (snapshot) => { +- state.endReachedSnapshot = snapshot; +- } +- ); +- } +- } +-} +- +-// src/utils/checkAtTop.ts +-function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { +- const state = ctx == null ? void 0 : ctx.state; +- if (!state) { +- return; +- } +- const { +- isStartReached, +- props: { data, onStartReachedThreshold }, +- scroll, +- scrollLength, +- startReachedSnapshot, +- totalSize +- } = state; +- const dataLength = data.length; +- const threshold = onStartReachedThreshold * scrollLength; +- resetSharedEdgeGateIfOutsideHysteresis(ctx); +- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { +- state.isStartReached = false; +- state.startReachedSnapshot = void 0; +- } +- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); +- set$(ctx, "isNearStart", scroll <= threshold); +- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; +- if (!shouldSkipThresholdChecks) { +- state.isStartReached = checkThreshold( +- scroll, +- false, +- threshold, +- state.isStartReached, +- startReachedSnapshot, +- { +- contentSize: totalSize, +- dataLength, +- scrollPosition: scroll +- }, +- (distance) => { +- var _a3, _b; +- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { +- markReachedEdge(ctx); +- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); +- } +- }, +- (snapshot) => { +- state.startReachedSnapshot = snapshot; +- } +- ); +- } +-} +- +-// src/utils/checkThresholds.ts +-function checkThresholds(ctx, allowedEdge) { +- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; +- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); +- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); +-} +- +-// src/core/recalculateSettledScroll.ts +-function recalculateSettledScroll(ctx) { +- var _a3, _b; +- const state = ctx.state; +- if ((_a3 = state.props) == null ? void 0 : _a3.data) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); +- } +- checkThresholds(ctx); +-} +- +-// src/core/adaptiveRender.ts +-var DEFAULT_ADAPTIVE_RENDER_ENTER_VELOCITY = 3; +-var DEFAULT_ADAPTIVE_RENDER_EXIT_VELOCITY = 1; +-var DEFAULT_ADAPTIVE_RENDER_EXIT_DELAY = 250; +-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; +-function clearAdaptiveRenderExitTimeout(ctx) { +- ctx.state.scheduledWork.cancel("adaptiveRender"); +-} +-function scheduleAdaptiveRenderExit(ctx, exitDelay) { +- const state = ctx.state; +- clearAdaptiveRenderExitTimeout(ctx); +- if (exitDelay <= 0) { +- setAdaptiveRender(ctx, "normal", "scroll"); +- } else { +- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); +- } +-} +-function setAdaptiveRender(ctx, mode, reason) { +- var _a3, _b; +- const previousMode = peek$(ctx, "adaptiveRender"); +- if (previousMode !== mode) { +- set$(ctx, "adaptiveRender", mode); +- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); +- } +-} +-function resetAdaptiveRender(ctx) { +- var _a3, _b; +- clearAdaptiveRenderExitTimeout(ctx); +- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; +- if (peek$(ctx, "adaptiveRender") !== mode) { +- setAdaptiveRender(ctx, mode, "initial"); +- } +-} +-function updateAdaptiveRender(ctx, scrollVelocity, options) { +- var _a3, _b, _c; +- const state = ctx.state; +- const adaptiveRender = state.props.adaptiveRender; +- const currentMode = peek$(ctx, "adaptiveRender"); +- if (peek$(ctx, "readyToRender")) { +- if (adaptiveRender) { +- const isWeb = Platform.OS === "web"; +- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY : DEFAULT_ADAPTIVE_RENDER_ENTER_VELOCITY; +- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY : DEFAULT_ADAPTIVE_RENDER_EXIT_VELOCITY; +- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY : DEFAULT_ADAPTIVE_RENDER_EXIT_DELAY; +- const threshold = currentMode === "light" ? exitVelocity : enterVelocity; +- const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; +- const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; +- if (nextMode !== previousMode) { +- if (nextMode === "light") { +- setAdaptiveRender(ctx, "light", "scroll"); +- scheduleAdaptiveRenderExit(ctx, exitDelay); +- } else if (currentMode === "light") { +- scheduleAdaptiveRenderExit(ctx, exitDelay); +- } +- } +- } else { +- resetAdaptiveRender(ctx); +- } +- } +-} +- +-// src/utils/getEffectiveDrawDistance.ts +-var INITIAL_DRAW_DISTANCE = 50; +-function getEffectiveDrawDistance(ctx, mode) { +- var _a3; +- const drawDistance = ctx.state.props.drawDistance; +- const initialScroll = ctx.state.initialScroll; +- const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; +- const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; +- return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; +-} +-function scheduleFullDrawDistancePrewarm(ctx) { +- const { state } = ctx; +- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { +- return; +- } +- state.scheduledWork.frame(() => { +- var _a3; +- return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +- }, "fullDrawDistancePrewarm"); +-} +- +-// src/utils/setInitialRenderState.ts +-function resetInitialRenderState(ctx, { +- resetLayout, +- resetInitialScroll +-}) { +- const { state } = ctx; +- if (resetLayout) { +- state.didContainersLayout = false; +- state.queuedInitialLayout = false; +- } +- if (resetInitialScroll) { +- state.didFinishInitialScroll = false; +- } +- set$(ctx, "readyToRender", false); +- resetAdaptiveRender(ctx); +-} +-function setInitialRenderState(ctx, { +- didLayout, +- didInitialScroll +-}) { +- const { state } = ctx; +- const { +- loadStartTime, +- props: { onLoad } +- } = state; +- if (didLayout) { +- state.didContainersLayout = true; +- } +- if (didInitialScroll) { +- state.didFinishInitialScroll = true; +- } +- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); +- if (isReadyToRender && !peek$(ctx, "readyToRender")) { +- set$(ctx, "readyToRender", true); +- setAdaptiveRender(ctx, "normal", "ready"); +- if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { +- scheduleFullDrawDistancePrewarm(ctx); +- } +- if (!state.didLoad) { +- state.didLoad = true; +- if (onLoad) { +- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); +- } +- } +- } +-} +- +-// src/core/finishInitialScroll.ts +-var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; +-function syncInitialScrollOffset(state, offset) { +- state.scroll = offset; +- state.scrollPending = offset; +- state.scrollPrev = offset; +-} +-function clearPreservedInitialScrollTargetTimeout(state) { +- state.scheduledWork.cancel("preservedInitialScroll"); +-} +-function clearPreservedInitialScrollTarget(state) { +- clearPreservedInitialScrollTargetTimeout(state); +- state.clearPreservedInitialScrollOnNextFinish = void 0; +- state.initialScroll = void 0; +- setInitialScrollSession(state); +-} +-function supersedeInitialScroll(ctx) { +- var _a3, _b, _c; +- const state = ctx.state; +- const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; +- if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { +- if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { +- cancelAnimationFrame(bootstrapInitialScroll.frameHandle); +- } +- if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { +- cancelScrollCompletionChecks(state); +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- } +- initialScrollCompletion.resetFlags(state); +- setInitialScrollSession(state, { bootstrap: null }); +- finishInitialScroll(ctx); +- } +-} +-function finishInitialScroll(ctx, options) { +- var _a3, _b, _c; +- const state = ctx.state; +- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { +- syncInitialScrollOffset(state, options.resolvedOffset); +- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { +- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); +- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { +- syncInitialScrollOffset(state, observedOffset); +- } +- } +- const complete = () => { +- var _a4, _b2, _c2, _d, _e; +- const shouldReleaseDeferredPublicOnScroll = Platform.OS === "web" && ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; +- const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0; +- initialScrollWatchdog.clear(state); +- if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) { +- state.clearPreservedInitialScrollOnNextFinish = void 0; +- setInitialScrollSession(state); +- clearPreservedInitialScrollTargetTimeout(state); +- if (options == null ? void 0 : options.schedulePreservedTargetClear) { +- state.scheduledWork.timeout( +- () => { +- var _a5; +- if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) { +- return; +- } +- clearPreservedInitialScrollTarget(state); +- }, +- PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, +- "preservedInitialScroll" +- ); +- } +- } else { +- clearPreservedInitialScrollTarget(state); +- } +- if (options == null ? void 0 : options.recalculateItems) { +- recalculateSettledScroll(ctx); +- } +- setInitialRenderState(ctx, { didInitialScroll: true }); +- if (shouldReleaseDeferredPublicOnScroll) { +- releaseDeferredPublicOnScroll(ctx, finalScrollOffset); +- } +- (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); +- }; +- if (options == null ? void 0 : options.waitForCompletionFrame) { +- requestAnimationFrame(complete); +- return; +- } +- complete(); +-} +- +-// src/core/calculateOffsetForIndex.ts +-function calculateOffsetForIndex(ctx, index) { +- const state = ctx.state; +- return index !== void 0 ? state.positions[index] || 0 : 0; +-} +- + // src/core/getStartOffsetAdjustment.ts + function getStartOffsetAdjustment(ctx) { + const { state } = ctx; +@@ -1244,58 +639,279 @@ function getItemSizeAtIndex(ctx, index) { + if (index === void 0 || index < 0) { + return void 0; + } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; ++ } ++ if (index !== void 0) { ++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); ++ if (startOffsetAdjustment) { ++ offset += startOffsetAdjustment; ++ } ++ } ++ if (viewPosition !== void 0 && index !== void 0) { ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return offset; ++ } ++ const isOutOfBounds = index < 0 || index >= dataLength; ++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; ++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); ++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); ++ const trailingInset = getContentInsetEnd(ctx); ++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); ++ if (!isOutOfBounds && index === state.props.data.length - 1) { ++ const footerSize = peek$(ctx, "footerSize") || 0; ++ offset += footerSize; ++ } ++ } ++ return offset; ++} ++ ++// src/core/clampScrollOffset.ts ++function clampScrollOffset(ctx, offset, scrollTarget) { ++ const state = ctx.state; ++ const contentSize = getContentSize(ctx); ++ let clampedOffset = offset; ++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android" || state.lastLayout)) { ++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); ++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; ++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; ++ const maxOffset = baseMaxOffset + extraEndOffset; ++ clampedOffset = Math.min(offset, maxOffset); ++ } ++ clampedOffset = Math.max(0, clampedOffset); ++ return clampedOffset; ++} ++ ++// src/core/calculateOffsetForIndex.ts ++function calculateOffsetForIndex(ctx, index) { ++ const state = ctx.state; ++ return index !== void 0 ? state.positions[index] || 0 : 0; ++} ++ ++// src/utils/checkThreshold.ts ++var HYSTERESIS_MULTIPLIER = 1.3; ++function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { ++ const absDistance = Math.abs(distance); ++ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; ++} ++var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { ++ const absDistance = Math.abs(distance); ++ const within = atThreshold || threshold > 0 && absDistance <= threshold; ++ const updateSnapshot = () => { ++ setSnapshot({ ++ atThreshold, ++ contentSize: context.contentSize, ++ dataLength: context.dataLength, ++ scrollPosition: context.scrollPosition ++ }); ++ }; ++ if (!wasReached) { ++ if (!within) { ++ return false; ++ } ++ onReached(distance); ++ updateSnapshot(); ++ return true; ++ } ++ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); ++ if (reset) { ++ setSnapshot(void 0); ++ return false; ++ } ++ if (within) { ++ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; ++ if (changed) { ++ updateSnapshot(); ++ } ++ } ++ return true; ++}; ++ ++// src/utils/edgeReachedGate.ts ++function resetEdgeLatch(ctx, edge) { ++ const state = ctx.state; ++ if (edge === "start") { ++ state.isStartReached = false; ++ state.startReachedSnapshot = void 0; ++ } else { ++ state.isEndReached = false; ++ state.endReachedSnapshot = void 0; ++ } ++} ++function resetSharedEdgeGateIfOutsideHysteresis(ctx) { ++ const state = ctx.state; ++ if (!state.edgeReachedGate) { ++ return; ++ } ++ const contentSize = getContentSize(ctx); ++ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); ++ const isContentLess = contentSize < state.scrollLength; ++ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; ++ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; ++ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); ++ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); ++ if (isOutsideStart && isOutsideEnd) { ++ state.edgeReachedGate = void 0; ++ } ++} ++function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { ++ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; ++} ++function markReachedEdge(ctx) { ++ ctx.state.edgeReachedGate = "closed"; ++} ++function prepareReachedEdgeForNextUserScroll(ctx) { ++ if (ctx.state.edgeReachedGate) { ++ ctx.state.edgeReachedGate = "prepared"; ++ } ++} ++function beginReachedEdgeUserScroll(ctx, scrollDelta) { ++ const state = ctx.state; ++ if (state.edgeReachedGate !== "prepared") { ++ return void 0; ++ } ++ const allowedEdge = scrollDelta < 0 ? "start" : "end"; ++ state.edgeReachedGate = "closed"; ++ resetEdgeLatch(ctx, allowedEdge); ++ return allowedEdge; ++} ++ ++// src/utils/hasActiveInitialScroll.ts ++function hasActiveInitialScroll(state) { ++ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; ++} ++ ++// src/utils/checkAtBottom.ts ++function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { ++ var _a3; ++ const state = ctx.state; ++ if (!state) { ++ return; ++ } ++ const { ++ queuedInitialLayout, ++ scrollLength, ++ scroll, ++ maintainingScrollAtEnd, ++ props: { maintainScrollAtEndThreshold, onEndReachedThreshold } ++ } = state; ++ const contentSize = getContentSize(ctx); ++ resetSharedEdgeGateIfOutsideHysteresis(ctx); ++ if (contentSize > 0 && queuedInitialLayout) { ++ const insetEnd = getContentInsetEnd(ctx); ++ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; ++ const isContentLess = contentSize < scrollLength; ++ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); ++ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); ++ set$( ++ ctx, ++ "isWithinMaintainScrollAtEndThreshold", ++ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength ++ ); ++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; ++ if (!shouldSkipThresholdChecks) { ++ state.isEndReached = checkThreshold( ++ distanceFromEnd, ++ isContentLess, ++ onEndReachedThreshold * scrollLength, ++ state.isEndReached, ++ state.endReachedSnapshot, ++ { ++ contentSize, ++ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, ++ scrollPosition: scroll ++ }, ++ (distance) => { ++ var _a4, _b; ++ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { ++ markReachedEdge(ctx); ++ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); ++ } ++ }, ++ (snapshot) => { ++ state.endReachedSnapshot = snapshot; ++ } ++ ); ++ } ++ } ++} ++ ++// src/utils/checkAtTop.ts ++function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { ++ const state = ctx == null ? void 0 : ctx.state; ++ if (!state) { ++ return; ++ } ++ const { ++ isStartReached, ++ props: { data, onStartReachedThreshold }, ++ scroll, ++ scrollLength, ++ startReachedSnapshot, ++ totalSize ++ } = state; ++ const dataLength = data.length; ++ const threshold = onStartReachedThreshold * scrollLength; ++ resetSharedEdgeGateIfOutsideHysteresis(ctx); ++ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { ++ state.isStartReached = false; ++ state.startReachedSnapshot = void 0; ++ } ++ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); ++ set$(ctx, "isNearStart", scroll <= threshold); ++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; ++ if (!shouldSkipThresholdChecks) { ++ state.isStartReached = checkThreshold( ++ scroll, ++ false, ++ threshold, ++ state.isStartReached, ++ startReachedSnapshot, ++ { ++ contentSize: totalSize, ++ dataLength, ++ scrollPosition: scroll ++ }, ++ (distance) => { ++ var _a3, _b; ++ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { ++ markReachedEdge(ctx); ++ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); ++ } ++ }, ++ (snapshot) => { ++ state.startReachedSnapshot = snapshot; ++ } ++ ); ++ } + } + +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +- } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } +- } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } +- } +- return offset; ++// src/utils/checkThresholds.ts ++function checkThresholds(ctx, allowedEdge) { ++ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; ++ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); ++ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + } + +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { ++// src/core/recalculateSettledScroll.ts ++function recalculateSettledScroll(ctx) { ++ var _a3, _b; + const state = ctx.state; +- const contentSize = getContentSize(ctx); +- let clampedOffset = offset; +- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android" || state.lastLayout)) { +- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); +- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; +- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; +- const maxOffset = baseMaxOffset + extraEndOffset; +- clampedOffset = Math.min(offset, maxOffset); ++ if ((_a3 = state.props) == null ? void 0 : _a3.data) { ++ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); + } +- clampedOffset = Math.max(0, clampedOffset); +- return clampedOffset; ++ checkThresholds(ctx); + } + + // src/core/finishScrollTo.ts +@@ -1336,6 +952,124 @@ function finishScrollTo(ctx) { + } + } + ++// src/core/initialScrollSession.ts ++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; ++function hasInitialScrollSessionCompletion(completion) { ++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); ++} ++function clearInitialScrollSession(state) { ++ state.initialScrollSession = void 0; ++ return void 0; ++} ++function createInitialScrollSession(options) { ++ const { bootstrap, completion, kind, previousDataLength } = options; ++ return kind === "offset" ? { ++ completion, ++ kind, ++ previousDataLength ++ } : { ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }; ++} ++function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { ++ var _a4, _b2; ++ if (!state.initialScrollSession) { ++ state.initialScrollSession = createInitialScrollSession({ ++ completion: {}, ++ kind, ++ previousDataLength: 0 ++ }); ++ } else if (state.initialScrollSession.kind !== kind) { ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, ++ completion: state.initialScrollSession.completion, ++ kind, ++ previousDataLength: state.initialScrollSession.previousDataLength ++ }); ++ } ++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; ++ return state.initialScrollSession.completion; ++} ++var initialScrollCompletion = { ++ didDispatchNativeScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); ++ }, ++ didRetrySilentInitialScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); ++ }, ++ markInitialScrollNativeDispatch(state) { ++ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; ++ }, ++ markSilentInitialScrollRetry(state) { ++ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; ++ }, ++ resetFlags(state) { ++ if (!state.initialScrollSession) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); ++ completion.didDispatchNativeScroll = void 0; ++ completion.didRetrySilentInitialScroll = void 0; ++ } ++}; ++var initialScrollWatchdog = { ++ clear(state) { ++ initialScrollWatchdog.set(state, void 0); ++ }, ++ didReachTarget(newScroll, watchdog) { ++ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); ++ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ get(state) { ++ var _a3, _b; ++ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; ++ }, ++ hasNonZeroTargetOffset(targetOffset) { ++ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ isAtZeroTargetOffset(targetOffset) { ++ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ set(state, watchdog) { ++ var _a3, _b; ++ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state); ++ completion.watchdog = watchdog ? { ++ startScroll: watchdog.startScroll, ++ targetOffset: watchdog.targetOffset ++ } : void 0; ++ } ++}; ++function setInitialScrollSession(state, options = {}) { ++ var _a3, _b, _c, _d; ++ const existingSession = state.initialScrollSession; ++ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; ++ const completion = existingSession == null ? void 0 : existingSession.completion; ++ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; ++ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; ++ if (!kind) { ++ return clearInitialScrollSession(state); ++ } ++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { ++ return clearInitialScrollSession(state); ++ } ++ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }); ++ return state.initialScrollSession; ++} ++ + // src/core/checkFinishedScroll.ts + var INITIAL_SCROLL_MAX_FALLBACK_CHECKS = 20; + var INITIAL_SCROLL_COMPLETION_TARGET_EPSILON = 1; +@@ -1528,6 +1262,69 @@ function doScrollTo(ctx, params) { + } + } + ++// src/core/adaptiveRender.ts ++var DEFAULT_ADAPTIVE_RENDER_ENTER_VELOCITY = 3; ++var DEFAULT_ADAPTIVE_RENDER_EXIT_VELOCITY = 1; ++var DEFAULT_ADAPTIVE_RENDER_EXIT_DELAY = 250; ++var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; ++function clearAdaptiveRenderExitTimeout(ctx) { ++ ctx.state.scheduledWork.cancel("adaptiveRender"); ++} ++function scheduleAdaptiveRenderExit(ctx, exitDelay) { ++ const state = ctx.state; ++ clearAdaptiveRenderExitTimeout(ctx); ++ if (exitDelay <= 0) { ++ setAdaptiveRender(ctx, "normal", "scroll"); ++ } else { ++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); ++ } ++} ++function setAdaptiveRender(ctx, mode, reason) { ++ var _a3, _b; ++ const previousMode = peek$(ctx, "adaptiveRender"); ++ if (previousMode !== mode) { ++ set$(ctx, "adaptiveRender", mode); ++ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); ++ } ++} ++function resetAdaptiveRender(ctx) { ++ var _a3, _b; ++ clearAdaptiveRenderExitTimeout(ctx); ++ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; ++ if (peek$(ctx, "adaptiveRender") !== mode) { ++ setAdaptiveRender(ctx, mode, "initial"); ++ } ++} ++function updateAdaptiveRender(ctx, scrollVelocity, options) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ const adaptiveRender = state.props.adaptiveRender; ++ const currentMode = peek$(ctx, "adaptiveRender"); ++ if (peek$(ctx, "readyToRender")) { ++ if (adaptiveRender) { ++ const isWeb = Platform.OS === "web"; ++ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY : DEFAULT_ADAPTIVE_RENDER_ENTER_VELOCITY; ++ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY : DEFAULT_ADAPTIVE_RENDER_EXIT_VELOCITY; ++ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY : DEFAULT_ADAPTIVE_RENDER_EXIT_DELAY; ++ const threshold = currentMode === "light" ? exitVelocity : enterVelocity; ++ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; ++ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; ++ if (nextMode !== previousMode) { ++ if (nextMode === "light") { ++ setAdaptiveRender(ctx, "light", "scroll"); ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } else if (currentMode === "light") { ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } ++ } ++ } else { ++ resetAdaptiveRender(ctx); ++ } ++ } ++} ++ + // src/core/doMaintainScrollAtEnd.ts + function doMaintainScrollAtEnd(ctx) { + const state = ctx.state; +@@ -1956,6 +1753,27 @@ var flushSync = (fn) => { + fn(); + }; + ++// src/utils/getEffectiveDrawDistance.ts ++var INITIAL_DRAW_DISTANCE = 50; ++function getEffectiveDrawDistance(ctx, mode) { ++ var _a3; ++ const drawDistance = ctx.state.props.drawDistance; ++ const initialScroll = ctx.state.initialScroll; ++ const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; ++ const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; ++ return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; ++} ++function scheduleFullDrawDistancePrewarm(ctx) { ++ const { state } = ctx; ++ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { ++ return; ++ } ++ state.scheduledWork.frame(() => { ++ var _a3; ++ return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); ++ }, "fullDrawDistancePrewarm"); ++} ++ + // src/utils/getScrollVelocity.ts + var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; + var SCROLL_VELOCITY_HALF_LIFE_MS = 200; +@@ -2203,7 +2021,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + } + } + function scrollTo(ctx, params) { +- var _a3, _b; ++ var _a3, _b, _c; + const state = ctx.state; + const { noScrollingTo, forceScroll, ...scrollTarget } = params; + const { +@@ -2235,6 +2053,15 @@ function scrollTo(ctx, params) { + }; + if (!isInitialScroll) { + pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); ++ if (scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { ++ beginScrollTargetSettle(ctx, { ++ index: scrollTarget.index, ++ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, ++ viewPosition: scrollTarget.viewPosition ++ }); ++ } else { ++ clearScrollTargetSettle(state); ++ } + } + } + state.scrollPending = targetOffset; +@@ -2242,7 +2069,7 @@ function scrollTo(ctx, params) { + if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { + if (animated) { + if (state.scrollTargetPinnedRange) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); + } + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); +@@ -2255,6 +2082,274 @@ function scrollTo(ctx, params) { + } + } + ++// src/core/scrollTargetSettle.ts ++var SETTLE_POSITION_EPSILON = 0.5; ++var SETTLE_QUIET_PASSES_TO_RELEASE = 2; ++var SETTLE_TTL_MS = 500; ++var SETTLE_MAX_MS = 1e3; ++var SETTLE_MAX_CORRECTIONS = 8; ++function clearScrollTargetSettle(state) { ++ state.scrollTargetSettle = void 0; ++} ++function beginScrollTargetSettle(ctx, params) { ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ const { data } = state.props; ++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; ++ if (index < 0 || index >= data.length || isEndAlignedLastItem) { ++ clearScrollTargetSettle(state); ++ return; ++ } ++ const now = Date.now(); ++ const id = getId(state, index); ++ const existing = state.scrollTargetSettle; ++ const isSameTarget = (existing == null ? void 0 : existing.id) === id; ++ state.scrollTargetSettle = { ++ corrections: isSameTarget ? existing.corrections : 0, ++ deadline: isSameTarget ? existing.deadline : now + SETTLE_MAX_MS, ++ expiresAt: now + SETTLE_TTL_MS, ++ id, ++ quietPasses: 0, ++ viewOffset, ++ viewPosition ++ }; ++} ++function getSettleTargetOffset(ctx, settle, index, position) { ++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; ++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); ++} ++function settleScrollTarget(ctx, isCompensating) { ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ return false; ++ } ++ if (isCompensating) { ++ settle.quietPasses = 0; ++ settle.expiresAt = Date.now() + SETTLE_TTL_MS; ++ return false; ++ } ++ const index = state.indexByKey.get(settle.id); ++ const position = index === void 0 ? void 0 : state.positions[index]; ++ if (index === void 0 || position === void 0) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ const now = Date.now(); ++ if (now > settle.expiresAt || now > settle.deadline) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position); ++ const diff = targetOffset - state.scroll; ++ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ settle.quietPasses++; ++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { ++ clearScrollTargetSettle(state); ++ } ++ return false; ++ } ++ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ settle.quietPasses = 0; ++ settle.corrections++; ++ settle.expiresAt = now + SETTLE_TTL_MS; ++ scrollTo(ctx, { ++ animated: false, ++ index, ++ itemSize: getItemSizeAtIndex(ctx, index), ++ offset: position, ++ viewOffset: settle.viewOffset, ++ viewPosition: settle.viewPosition ++ }); ++ return true; ++} ++ ++// src/core/cancelImperativeScroll.ts ++function cancelScrollCompletionChecks({ scheduledWork }) { ++ scheduledWork.cancel("checkFinishedScrollFrame"); ++ scheduledWork.cancel("checkFinishedScrollRetryFrame"); ++ scheduledWork.cancel("checkFinishedScrollFallback"); ++ scheduledWork.cancel("platformScrollCompletion"); ++} ++function settlePendingImperativeScroll(state) { ++ var _a3, _b; ++ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; ++ state.pendingScrollResolve = void 0; ++ state.pendingScrollToEnd = void 0; ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); ++} ++function cancelImperativeScroll(state) { ++ cancelScrollCompletionChecks(state); ++ state.scheduledWork.cancel("imperativeScrollReady"); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ clearScrollTargetSettle(state); ++ settlePendingImperativeScroll(state); ++} ++ ++// src/core/deferredPublicOnScroll.ts ++function withResolvedContentOffset(state, event, resolvedOffset) { ++ return { ++ ...event, ++ nativeEvent: { ++ ...event.nativeEvent, ++ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } ++ } ++ }; ++} ++function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { ++ var _a3, _b, _c, _d; ++ const state = ctx.state; ++ const deferredEvent = state.deferredPublicOnScrollEvent; ++ state.deferredPublicOnScrollEvent = void 0; ++ if (deferredEvent) { ++ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( ++ _c, ++ withResolvedContentOffset( ++ state, ++ deferredEvent, ++ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 ++ ) ++ ); ++ } ++} ++ ++// src/utils/setInitialRenderState.ts ++function resetInitialRenderState(ctx, { ++ resetLayout, ++ resetInitialScroll ++}) { ++ const { state } = ctx; ++ if (resetLayout) { ++ state.didContainersLayout = false; ++ state.queuedInitialLayout = false; ++ } ++ if (resetInitialScroll) { ++ state.didFinishInitialScroll = false; ++ } ++ set$(ctx, "readyToRender", false); ++ resetAdaptiveRender(ctx); ++} ++function setInitialRenderState(ctx, { ++ didLayout, ++ didInitialScroll ++}) { ++ const { state } = ctx; ++ const { ++ loadStartTime, ++ props: { onLoad } ++ } = state; ++ if (didLayout) { ++ state.didContainersLayout = true; ++ } ++ if (didInitialScroll) { ++ state.didFinishInitialScroll = true; ++ } ++ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); ++ if (isReadyToRender && !peek$(ctx, "readyToRender")) { ++ set$(ctx, "readyToRender", true); ++ setAdaptiveRender(ctx, "normal", "ready"); ++ if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { ++ scheduleFullDrawDistancePrewarm(ctx); ++ } ++ if (!state.didLoad) { ++ state.didLoad = true; ++ if (onLoad) { ++ onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); ++ } ++ } ++ } ++} ++ ++// src/core/finishInitialScroll.ts ++var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; ++function syncInitialScrollOffset(state, offset) { ++ state.scroll = offset; ++ state.scrollPending = offset; ++ state.scrollPrev = offset; ++} ++function clearPreservedInitialScrollTargetTimeout(state) { ++ state.scheduledWork.cancel("preservedInitialScroll"); ++} ++function clearPreservedInitialScrollTarget(state) { ++ clearPreservedInitialScrollTargetTimeout(state); ++ state.clearPreservedInitialScrollOnNextFinish = void 0; ++ state.initialScroll = void 0; ++ setInitialScrollSession(state); ++} ++function supersedeInitialScroll(ctx) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; ++ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { ++ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { ++ cancelAnimationFrame(bootstrapInitialScroll.frameHandle); ++ } ++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { ++ cancelScrollCompletionChecks(state); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ } ++ initialScrollCompletion.resetFlags(state); ++ setInitialScrollSession(state, { bootstrap: null }); ++ finishInitialScroll(ctx); ++ } ++} ++function finishInitialScroll(ctx, options) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { ++ syncInitialScrollOffset(state, options.resolvedOffset); ++ } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { ++ const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); ++ if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { ++ syncInitialScrollOffset(state, observedOffset); ++ } ++ } ++ const complete = () => { ++ var _a4, _b2, _c2, _d, _e; ++ const shouldReleaseDeferredPublicOnScroll = Platform.OS === "web" && ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; ++ const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0; ++ initialScrollWatchdog.clear(state); ++ if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) { ++ state.clearPreservedInitialScrollOnNextFinish = void 0; ++ setInitialScrollSession(state); ++ clearPreservedInitialScrollTargetTimeout(state); ++ if (options == null ? void 0 : options.schedulePreservedTargetClear) { ++ state.scheduledWork.timeout( ++ () => { ++ var _a5; ++ if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) { ++ return; ++ } ++ clearPreservedInitialScrollTarget(state); ++ }, ++ PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, ++ "preservedInitialScroll" ++ ); ++ } ++ } else { ++ clearPreservedInitialScrollTarget(state); ++ } ++ if (options == null ? void 0 : options.recalculateItems) { ++ recalculateSettledScroll(ctx); ++ } ++ setInitialRenderState(ctx, { didInitialScroll: true }); ++ if (shouldReleaseDeferredPublicOnScroll) { ++ releaseDeferredPublicOnScroll(ctx, finalScrollOffset); ++ } ++ (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); ++ }; ++ if (options == null ? void 0 : options.waitForCompletionFrame) { ++ requestAnimationFrame(complete); ++ return; ++ } ++ complete(); ++} ++ + // src/core/scrollToIndex.ts + function clampScrollIndex(index, dataLength) { + if (dataLength <= 0) { +@@ -4316,7 +4411,10 @@ function calculateItemsInView(ctx, params = {}) { + const scrollBeforeMVCP = state.scroll; + const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; + checkMVCP == null ? void 0 : checkMVCP(); +- const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); ++ const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; ++ const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); ++ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx, didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0); ++ const didMVCPAdjustScroll = didMVCPAdjust || didSettleScrollTarget; + if (didMVCPAdjustScroll) { + updateScroll2(state.scroll); + updateScrollRange(); +@@ -6536,6 +6634,22 @@ function createColumnWrapperStyle(contentContainerStyle) { + } + } + ++// src/core/isScrollExtentSynced.ts ++var SCROLL_EXTENT_EPSILON = 1; ++function isScrollExtentSynced(ctx) { ++ var _a3, _b; ++ const state = ctx.state; ++ const platformMaxOffset = (_b = (_a3 = state.refScroller.current) == null ? void 0 : _a3.getMaxScrollOffset) == null ? void 0 : _b.call(_a3); ++ if (platformMaxOffset === void 0) { ++ return true; ++ } ++ if (state.scrollLength <= 0) { ++ return true; ++ } ++ const contentMaxOffset = Math.max(0, getContentSize(ctx) - state.scrollLength); ++ return platformMaxOffset >= contentMaxOffset - SCROLL_EXTENT_EPSILON; ++} ++ + // src/core/scrollToEnd.ts + function scrollToEnd(ctx, options) { + const state = ctx.state; +@@ -6585,6 +6699,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { + const IMPERATIVE_SCROLL_SETTLE_STABLE_FRAMES = 2; + let imperativeScrollToken = 0; + const isSettlingAfterDataChange = () => !!state.didDataChange || !!state.didColumnsChange || state.scheduledWork.has("mvcpRecalculate") || state.ignoreScrollFromMVCP !== void 0; ++ const isScrollBlocked = () => isSettlingAfterDataChange() || !isScrollExtentSynced(ctx); + const isScrollToIndexReady = (targetIndex, allowEmpty = false) => { + var _a3; + const props = state.props; +@@ -6608,7 +6723,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { + if (token !== imperativeScrollToken) { + return; + } +- if (isSettlingAfterDataChange() || !isReady()) { ++ if (isScrollBlocked() || !isReady()) { + stableFrames = 0; + } else { + stableFrames += 1; +@@ -6635,7 +6750,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { + resolve(); + } + }; +- if (isSettlingAfterDataChange() || !isReady()) { ++ if (isScrollBlocked() || !isReady()) { + runWhenReady(token, runNow, isReady); + } else { + runNow(); +@@ -7631,6 +7746,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + onScrollBeginDrag: (event) => { + var _a4, _b2; + prepareReachedEdgeForNextUserScroll(ctx); ++ clearScrollTargetSettle(state); + (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); + }, + onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) +diff --git a/node_modules/@legendapp/list/react-native.web.d.ts b/node_modules/@legendapp/list/react-native.web.d.ts +index b6c7481..0ec3d1e 100644 +--- a/node_modules/@legendapp/list/react-native.web.d.ts ++++ b/node_modules/@legendapp/list/react-native.web.d.ts +@@ -6,6 +6,7 @@ type ScrollEventTarget = Window | HTMLElement; + interface ScrollViewMethods { + getBoundingClientRect(): DOMRect | null | undefined; + getCurrentScrollOffset(): number; ++ getMaxScrollOffset(): number; + getScrollableNode(): HTMLElement; + getScrollEventTarget(): ScrollEventTarget | null; + getScrollResponder(): HTMLElement | null; +diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js +index 914d2da..e11dd28 100644 +--- a/node_modules/@legendapp/list/react-native.web.js ++++ b/node_modules/@legendapp/list/react-native.web.js +@@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; + var ENABLE_DEVMODE = IS_DEV && false; + var ENABLE_DEBUG_VIEW = IS_DEV && false; + +-// src/core/cancelImperativeScroll.ts +-function cancelScrollCompletionChecks({ scheduledWork }) { +- scheduledWork.cancel("checkFinishedScrollFrame"); +- scheduledWork.cancel("checkFinishedScrollRetryFrame"); +- scheduledWork.cancel("checkFinishedScrollFallback"); +- scheduledWork.cancel("platformScrollCompletion"); +-} +-function settlePendingImperativeScroll(state) { +- var _a3, _b; +- const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; +- state.pendingScrollResolve = void 0; +- state.pendingScrollToEnd = void 0; +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +-} +-function cancelImperativeScroll(state) { +- cancelScrollCompletionChecks(state); +- state.scheduledWork.cancel("imperativeScrollReady"); +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- settlePendingImperativeScroll(state); +-} +- +-// src/core/deferredPublicOnScroll.ts +-function withResolvedContentOffset(state, event, resolvedOffset) { +- return { +- ...event, +- nativeEvent: { +- ...event.nativeEvent, +- contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } +- } +- }; +-} +-function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { +- var _a3, _b, _c, _d; +- const state = ctx.state; +- const deferredEvent = state.deferredPublicOnScrollEvent; +- state.deferredPublicOnScrollEvent = void 0; +- if (deferredEvent) { +- (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( +- _c, +- withResolvedContentOffset( +- state, +- deferredEvent, +- (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 +- ) +- ); +- } +-} +- +-// src/core/initialScrollSession.ts +-var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +-function hasInitialScrollSessionCompletion(completion) { +- return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); +-} +-function clearInitialScrollSession(state) { +- state.initialScrollSession = void 0; +- return void 0; +-} +-function createInitialScrollSession(options) { +- const { bootstrap, completion, kind, previousDataLength } = options; +- return kind === "offset" ? { +- completion, +- kind, +- previousDataLength +- } : { +- bootstrap, +- completion, +- kind, +- previousDataLength +- }; +-} +-function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { +- var _a4, _b2; +- if (!state.initialScrollSession) { +- state.initialScrollSession = createInitialScrollSession({ +- completion: {}, +- kind, +- previousDataLength: 0 +- }); +- } else if (state.initialScrollSession.kind !== kind) { +- state.initialScrollSession = createInitialScrollSession({ +- bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, +- completion: state.initialScrollSession.completion, +- kind, +- previousDataLength: state.initialScrollSession.previousDataLength +- }); +- } +- (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; +- return state.initialScrollSession.completion; +-} +-var initialScrollCompletion = { +- didDispatchNativeScroll(state) { +- var _a3, _b; +- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); +- }, +- didRetrySilentInitialScroll(state) { +- var _a3, _b; +- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); +- }, +- markInitialScrollNativeDispatch(state) { +- ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; +- }, +- markSilentInitialScrollRetry(state) { +- ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; +- }, +- resetFlags(state) { +- if (!state.initialScrollSession) { +- return; +- } +- const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); +- completion.didDispatchNativeScroll = void 0; +- completion.didRetrySilentInitialScroll = void 0; +- } +-}; +-var initialScrollWatchdog = { +- clear(state) { +- initialScrollWatchdog.set(state, void 0); +- }, +- didReachTarget(newScroll, watchdog) { +- const nextDistance = Math.abs(newScroll - watchdog.targetOffset); +- return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; +- }, +- get(state) { +- var _a3, _b; +- return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; +- }, +- hasNonZeroTargetOffset(targetOffset) { +- return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; +- }, +- isAtZeroTargetOffset(targetOffset) { +- return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; +- }, +- set(state, watchdog) { +- var _a3, _b; +- if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { +- return; +- } +- const completion = ensureInitialScrollSessionCompletion(state); +- completion.watchdog = watchdog ? { +- startScroll: watchdog.startScroll, +- targetOffset: watchdog.targetOffset +- } : void 0; +- } +-}; +-function setInitialScrollSession(state, options = {}) { +- var _a3, _b, _c, _d; +- const existingSession = state.initialScrollSession; +- const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; +- const completion = existingSession == null ? void 0 : existingSession.completion; +- const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; +- const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; +- if (!kind) { +- return clearInitialScrollSession(state); +- } +- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { +- return clearInitialScrollSession(state); +- } +- const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; +- state.initialScrollSession = createInitialScrollSession({ +- bootstrap, +- completion, +- kind, +- previousDataLength +- }); +- return state.initialScrollSession; +-} +- +-// src/utils/checkThreshold.ts +-var HYSTERESIS_MULTIPLIER = 1.3; +-function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { +- const absDistance = Math.abs(distance); +- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; +-} +-var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { +- const absDistance = Math.abs(distance); +- const within = atThreshold || threshold > 0 && absDistance <= threshold; +- const updateSnapshot = () => { +- setSnapshot({ +- atThreshold, +- contentSize: context.contentSize, +- dataLength: context.dataLength, +- scrollPosition: context.scrollPosition +- }); +- }; +- if (!wasReached) { +- if (!within) { +- return false; +- } +- onReached(distance); +- updateSnapshot(); +- return true; +- } +- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); +- if (reset) { +- setSnapshot(void 0); +- return false; +- } +- if (within) { +- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; +- if (changed) { +- updateSnapshot(); +- } +- } +- return true; +-}; +- +-// src/utils/edgeReachedGate.ts +-function resetEdgeLatch(ctx, edge) { +- const state = ctx.state; +- if (edge === "start") { +- state.isStartReached = false; +- state.startReachedSnapshot = void 0; +- } else { +- state.isEndReached = false; +- state.endReachedSnapshot = void 0; +- } +-} +-function resetSharedEdgeGateIfOutsideHysteresis(ctx) { +- const state = ctx.state; +- if (!state.edgeReachedGate) { +- return; +- } +- const contentSize = getContentSize(ctx); +- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); +- const isContentLess = contentSize < state.scrollLength; +- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; +- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; +- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); +- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); +- if (isOutsideStart && isOutsideEnd) { +- state.edgeReachedGate = void 0; +- } +-} +-function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { +- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; +-} +-function markReachedEdge(ctx) { +- ctx.state.edgeReachedGate = "closed"; +-} +-function prepareReachedEdgeForNextUserScroll(ctx) { +- if (ctx.state.edgeReachedGate) { +- ctx.state.edgeReachedGate = "prepared"; +- } +-} +-function beginReachedEdgeUserScroll(ctx, scrollDelta) { +- const state = ctx.state; +- if (state.edgeReachedGate !== "prepared") { +- return void 0; +- } +- const allowedEdge = scrollDelta < 0 ? "start" : "end"; +- state.edgeReachedGate = "closed"; +- resetEdgeLatch(ctx, allowedEdge); +- return allowedEdge; +-} +- +-// src/utils/hasActiveInitialScroll.ts +-function hasActiveInitialScroll(state) { +- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; +-} +- +-// src/utils/checkAtBottom.ts +-function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { +- var _a3; +- const state = ctx.state; +- if (!state) { +- return; +- } +- const { +- queuedInitialLayout, +- scrollLength, +- scroll, +- maintainingScrollAtEnd, +- props: { maintainScrollAtEndThreshold, onEndReachedThreshold } +- } = state; +- const contentSize = getContentSize(ctx); +- resetSharedEdgeGateIfOutsideHysteresis(ctx); +- if (contentSize > 0 && queuedInitialLayout) { +- const insetEnd = getContentInsetEnd(ctx); +- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; +- const isContentLess = contentSize < scrollLength; +- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); +- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); +- set$( +- ctx, +- "isWithinMaintainScrollAtEndThreshold", +- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength +- ); +- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; +- if (!shouldSkipThresholdChecks) { +- state.isEndReached = checkThreshold( +- distanceFromEnd, +- isContentLess, +- onEndReachedThreshold * scrollLength, +- state.isEndReached, +- state.endReachedSnapshot, +- { +- contentSize, +- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, +- scrollPosition: scroll +- }, +- (distance) => { +- var _a4, _b; +- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { +- markReachedEdge(ctx); +- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); +- } +- }, +- (snapshot) => { +- state.endReachedSnapshot = snapshot; +- } +- ); +- } +- } +-} +- +-// src/utils/checkAtTop.ts +-function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { +- const state = ctx == null ? void 0 : ctx.state; +- if (!state) { +- return; +- } +- const { +- isStartReached, +- props: { data, onStartReachedThreshold }, +- scroll, +- scrollLength, +- startReachedSnapshot, +- totalSize +- } = state; +- const dataLength = data.length; +- const threshold = onStartReachedThreshold * scrollLength; +- resetSharedEdgeGateIfOutsideHysteresis(ctx); +- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { +- state.isStartReached = false; +- state.startReachedSnapshot = void 0; +- } +- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); +- set$(ctx, "isNearStart", scroll <= threshold); +- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; +- if (!shouldSkipThresholdChecks) { +- state.isStartReached = checkThreshold( +- scroll, +- false, +- threshold, +- state.isStartReached, +- startReachedSnapshot, +- { +- contentSize: totalSize, +- dataLength, +- scrollPosition: scroll +- }, +- (distance) => { +- var _a3, _b; +- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { +- markReachedEdge(ctx); +- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); +- } +- }, +- (snapshot) => { +- state.startReachedSnapshot = snapshot; +- } +- ); +- } +-} +- +-// src/utils/checkThresholds.ts +-function checkThresholds(ctx, allowedEdge) { +- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; +- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); +- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); +-} +- +-// src/core/recalculateSettledScroll.ts +-function recalculateSettledScroll(ctx) { +- var _a3, _b; +- const state = ctx.state; +- if ((_a3 = state.props) == null ? void 0 : _a3.data) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); +- } +- checkThresholds(ctx); +-} +-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; +-function clearAdaptiveRenderExitTimeout(ctx) { +- ctx.state.scheduledWork.cancel("adaptiveRender"); +-} +-function scheduleAdaptiveRenderExit(ctx, exitDelay) { +- const state = ctx.state; +- clearAdaptiveRenderExitTimeout(ctx); +- if (exitDelay <= 0) { +- setAdaptiveRender(ctx, "normal", "scroll"); +- } else { +- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); +- } +-} +-function setAdaptiveRender(ctx, mode, reason) { +- var _a3, _b; +- const previousMode = peek$(ctx, "adaptiveRender"); +- if (previousMode !== mode) { +- set$(ctx, "adaptiveRender", mode); +- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); +- } +-} +-function resetAdaptiveRender(ctx) { +- var _a3, _b; +- clearAdaptiveRenderExitTimeout(ctx); +- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; +- if (peek$(ctx, "adaptiveRender") !== mode) { +- setAdaptiveRender(ctx, mode, "initial"); +- } +-} +-function updateAdaptiveRender(ctx, scrollVelocity, options) { +- var _a3, _b, _c; +- const state = ctx.state; +- const adaptiveRender = state.props.adaptiveRender; +- const currentMode = peek$(ctx, "adaptiveRender"); +- if (peek$(ctx, "readyToRender")) { +- if (adaptiveRender) { +- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; +- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; +- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; +- const threshold = currentMode === "light" ? exitVelocity : enterVelocity; +- const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; +- const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; +- if (nextMode !== previousMode) { +- if (nextMode === "light") { +- setAdaptiveRender(ctx, "light", "scroll"); +- scheduleAdaptiveRenderExit(ctx, exitDelay); +- } else if (currentMode === "light") { +- scheduleAdaptiveRenderExit(ctx, exitDelay); +- } +- } +- } else { +- resetAdaptiveRender(ctx); +- } +- } +-} +- +-// src/utils/getEffectiveDrawDistance.ts +-var INITIAL_DRAW_DISTANCE = 50; +-function getEffectiveDrawDistance(ctx, mode) { +- var _a3; +- const drawDistance = ctx.state.props.drawDistance; +- const initialScroll = ctx.state.initialScroll; +- const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; +- const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; +- return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; +-} +-function scheduleFullDrawDistancePrewarm(ctx) { +- const { state } = ctx; +- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { +- return; +- } +- state.scheduledWork.frame(() => { +- var _a3; +- return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +- }, "fullDrawDistancePrewarm"); +-} +- +-// src/utils/setInitialRenderState.ts +-function resetInitialRenderState(ctx, { +- resetLayout, +- resetInitialScroll +-}) { +- const { state } = ctx; +- if (resetLayout) { +- state.didContainersLayout = false; +- state.queuedInitialLayout = false; +- } +- if (resetInitialScroll) { +- state.didFinishInitialScroll = false; +- } +- set$(ctx, "readyToRender", false); +- resetAdaptiveRender(ctx); +-} +-function setInitialRenderState(ctx, { +- didLayout, +- didInitialScroll +-}) { +- const { state } = ctx; +- const { +- loadStartTime, +- props: { onLoad } +- } = state; +- if (didLayout) { +- state.didContainersLayout = true; +- } +- if (didInitialScroll) { +- state.didFinishInitialScroll = true; +- } +- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); +- if (isReadyToRender && !peek$(ctx, "readyToRender")) { +- set$(ctx, "readyToRender", true); +- setAdaptiveRender(ctx, "normal", "ready"); +- if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { +- scheduleFullDrawDistancePrewarm(ctx); +- } +- if (!state.didLoad) { +- state.didLoad = true; +- if (onLoad) { +- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); +- } +- } +- } +-} +- +-// src/core/finishInitialScroll.ts +-var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; +-function syncInitialScrollOffset(state, offset) { +- state.scroll = offset; +- state.scrollPending = offset; +- state.scrollPrev = offset; +-} +-function clearPreservedInitialScrollTargetTimeout(state) { +- state.scheduledWork.cancel("preservedInitialScroll"); +-} +-function clearPreservedInitialScrollTarget(state) { +- clearPreservedInitialScrollTargetTimeout(state); +- state.clearPreservedInitialScrollOnNextFinish = void 0; +- state.initialScroll = void 0; +- setInitialScrollSession(state); +-} +-function supersedeInitialScroll(ctx) { +- var _a3, _b, _c; +- const state = ctx.state; +- const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; +- if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { +- if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { +- cancelAnimationFrame(bootstrapInitialScroll.frameHandle); +- } +- if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { +- cancelScrollCompletionChecks(state); +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- } +- initialScrollCompletion.resetFlags(state); +- setInitialScrollSession(state, { bootstrap: null }); +- finishInitialScroll(ctx); +- } +-} +-function finishInitialScroll(ctx, options) { +- var _a3, _b, _c; +- const state = ctx.state; +- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { +- syncInitialScrollOffset(state, options.resolvedOffset); +- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { +- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); +- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { +- syncInitialScrollOffset(state, observedOffset); +- } +- } +- const complete = () => { +- var _a4, _b2, _c2, _d, _e; +- const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; +- const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0; +- initialScrollWatchdog.clear(state); +- if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) { +- state.clearPreservedInitialScrollOnNextFinish = void 0; +- setInitialScrollSession(state); +- clearPreservedInitialScrollTargetTimeout(state); +- if (options == null ? void 0 : options.schedulePreservedTargetClear) { +- state.scheduledWork.timeout( +- () => { +- var _a5; +- if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) { +- return; +- } +- clearPreservedInitialScrollTarget(state); +- }, +- PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, +- "preservedInitialScroll" +- ); +- } +- } else { +- clearPreservedInitialScrollTarget(state); +- } +- if (options == null ? void 0 : options.recalculateItems) { +- recalculateSettledScroll(ctx); +- } +- setInitialRenderState(ctx, { didInitialScroll: true }); +- if (shouldReleaseDeferredPublicOnScroll) { +- releaseDeferredPublicOnScroll(ctx, finalScrollOffset); +- } +- (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); +- }; +- if (options == null ? void 0 : options.waitForCompletionFrame) { +- requestAnimationFrame(complete); +- return; +- } +- complete(); +-} +- +-// src/core/calculateOffsetForIndex.ts +-function calculateOffsetForIndex(ctx, index) { +- const state = ctx.state; +- return index !== void 0 ? state.positions[index] || 0 : 0; +-} +- + // src/core/getStartOffsetAdjustment.ts + function getStartOffsetAdjustment(ctx) { + const { state } = ctx; +@@ -1234,58 +635,273 @@ function getItemSizeAtIndex(ctx, index) { + if (index === void 0 || index < 0) { + return void 0; + } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; ++ } ++ if (index !== void 0) { ++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); ++ if (startOffsetAdjustment) { ++ offset += startOffsetAdjustment; ++ } ++ } ++ if (viewPosition !== void 0 && index !== void 0) { ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return offset; ++ } ++ const isOutOfBounds = index < 0 || index >= dataLength; ++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; ++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); ++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); ++ const trailingInset = getContentInsetEnd(ctx); ++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); ++ if (!isOutOfBounds && index === state.props.data.length - 1) { ++ const footerSize = peek$(ctx, "footerSize") || 0; ++ offset += footerSize; ++ } ++ } ++ return offset; ++} ++ ++// src/core/clampScrollOffset.ts ++function clampScrollOffset(ctx, offset, scrollTarget) { ++ const state = ctx.state; ++ const contentSize = getContentSize(ctx); ++ let clampedOffset = offset; ++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { ++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); ++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; ++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; ++ const maxOffset = baseMaxOffset + extraEndOffset; ++ clampedOffset = Math.min(offset, maxOffset); ++ } ++ clampedOffset = Math.max(0, clampedOffset); ++ return clampedOffset; ++} ++ ++// src/utils/checkThreshold.ts ++var HYSTERESIS_MULTIPLIER = 1.3; ++function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { ++ const absDistance = Math.abs(distance); ++ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; ++} ++var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { ++ const absDistance = Math.abs(distance); ++ const within = atThreshold || threshold > 0 && absDistance <= threshold; ++ const updateSnapshot = () => { ++ setSnapshot({ ++ atThreshold, ++ contentSize: context.contentSize, ++ dataLength: context.dataLength, ++ scrollPosition: context.scrollPosition ++ }); ++ }; ++ if (!wasReached) { ++ if (!within) { ++ return false; ++ } ++ onReached(distance); ++ updateSnapshot(); ++ return true; ++ } ++ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); ++ if (reset) { ++ setSnapshot(void 0); ++ return false; ++ } ++ if (within) { ++ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; ++ if (changed) { ++ updateSnapshot(); ++ } ++ } ++ return true; ++}; ++ ++// src/utils/edgeReachedGate.ts ++function resetEdgeLatch(ctx, edge) { ++ const state = ctx.state; ++ if (edge === "start") { ++ state.isStartReached = false; ++ state.startReachedSnapshot = void 0; ++ } else { ++ state.isEndReached = false; ++ state.endReachedSnapshot = void 0; ++ } ++} ++function resetSharedEdgeGateIfOutsideHysteresis(ctx) { ++ const state = ctx.state; ++ if (!state.edgeReachedGate) { ++ return; ++ } ++ const contentSize = getContentSize(ctx); ++ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); ++ const isContentLess = contentSize < state.scrollLength; ++ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; ++ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; ++ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); ++ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); ++ if (isOutsideStart && isOutsideEnd) { ++ state.edgeReachedGate = void 0; ++ } ++} ++function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { ++ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; ++} ++function markReachedEdge(ctx) { ++ ctx.state.edgeReachedGate = "closed"; ++} ++function prepareReachedEdgeForNextUserScroll(ctx) { ++ if (ctx.state.edgeReachedGate) { ++ ctx.state.edgeReachedGate = "prepared"; ++ } ++} ++function beginReachedEdgeUserScroll(ctx, scrollDelta) { ++ const state = ctx.state; ++ if (state.edgeReachedGate !== "prepared") { ++ return void 0; ++ } ++ const allowedEdge = scrollDelta < 0 ? "start" : "end"; ++ state.edgeReachedGate = "closed"; ++ resetEdgeLatch(ctx, allowedEdge); ++ return allowedEdge; ++} ++ ++// src/utils/hasActiveInitialScroll.ts ++function hasActiveInitialScroll(state) { ++ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; ++} ++ ++// src/utils/checkAtBottom.ts ++function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { ++ var _a3; ++ const state = ctx.state; ++ if (!state) { ++ return; ++ } ++ const { ++ queuedInitialLayout, ++ scrollLength, ++ scroll, ++ maintainingScrollAtEnd, ++ props: { maintainScrollAtEndThreshold, onEndReachedThreshold } ++ } = state; ++ const contentSize = getContentSize(ctx); ++ resetSharedEdgeGateIfOutsideHysteresis(ctx); ++ if (contentSize > 0 && queuedInitialLayout) { ++ const insetEnd = getContentInsetEnd(ctx); ++ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; ++ const isContentLess = contentSize < scrollLength; ++ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); ++ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); ++ set$( ++ ctx, ++ "isWithinMaintainScrollAtEndThreshold", ++ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength ++ ); ++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; ++ if (!shouldSkipThresholdChecks) { ++ state.isEndReached = checkThreshold( ++ distanceFromEnd, ++ isContentLess, ++ onEndReachedThreshold * scrollLength, ++ state.isEndReached, ++ state.endReachedSnapshot, ++ { ++ contentSize, ++ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, ++ scrollPosition: scroll ++ }, ++ (distance) => { ++ var _a4, _b; ++ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { ++ markReachedEdge(ctx); ++ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); ++ } ++ }, ++ (snapshot) => { ++ state.endReachedSnapshot = snapshot; ++ } ++ ); ++ } ++ } ++} ++ ++// src/utils/checkAtTop.ts ++function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { ++ const state = ctx == null ? void 0 : ctx.state; ++ if (!state) { ++ return; ++ } ++ const { ++ isStartReached, ++ props: { data, onStartReachedThreshold }, ++ scroll, ++ scrollLength, ++ startReachedSnapshot, ++ totalSize ++ } = state; ++ const dataLength = data.length; ++ const threshold = onStartReachedThreshold * scrollLength; ++ resetSharedEdgeGateIfOutsideHysteresis(ctx); ++ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { ++ state.isStartReached = false; ++ state.startReachedSnapshot = void 0; ++ } ++ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); ++ set$(ctx, "isNearStart", scroll <= threshold); ++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; ++ if (!shouldSkipThresholdChecks) { ++ state.isStartReached = checkThreshold( ++ scroll, ++ false, ++ threshold, ++ state.isStartReached, ++ startReachedSnapshot, ++ { ++ contentSize: totalSize, ++ dataLength, ++ scrollPosition: scroll ++ }, ++ (distance) => { ++ var _a3, _b; ++ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { ++ markReachedEdge(ctx); ++ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); ++ } ++ }, ++ (snapshot) => { ++ state.startReachedSnapshot = snapshot; ++ } ++ ); ++ } + } + +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +- } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } +- } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } +- } +- return offset; ++// src/utils/checkThresholds.ts ++function checkThresholds(ctx, allowedEdge) { ++ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; ++ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); ++ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + } + +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { ++// src/core/recalculateSettledScroll.ts ++function recalculateSettledScroll(ctx) { ++ var _a3, _b; + const state = ctx.state; +- const contentSize = getContentSize(ctx); +- let clampedOffset = offset; +- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { +- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); +- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; +- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; +- const maxOffset = baseMaxOffset + extraEndOffset; +- clampedOffset = Math.min(offset, maxOffset); ++ if ((_a3 = state.props) == null ? void 0 : _a3.data) { ++ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); + } +- clampedOffset = Math.max(0, clampedOffset); +- return clampedOffset; ++ checkThresholds(ctx); + } + + // src/core/finishScrollTo.ts +@@ -1422,7 +1038,182 @@ function listenForScrollEnd(ctx, params) { + } else { + idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); + } +- scheduledWork.register("platformScrollCompletion", cancel); ++ scheduledWork.register("platformScrollCompletion", cancel); ++} ++ ++// src/core/initialScrollSession.ts ++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; ++function hasInitialScrollSessionCompletion(completion) { ++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); ++} ++function clearInitialScrollSession(state) { ++ state.initialScrollSession = void 0; ++ return void 0; ++} ++function createInitialScrollSession(options) { ++ const { bootstrap, completion, kind, previousDataLength } = options; ++ return kind === "offset" ? { ++ completion, ++ kind, ++ previousDataLength ++ } : { ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }; ++} ++function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { ++ var _a4, _b2; ++ if (!state.initialScrollSession) { ++ state.initialScrollSession = createInitialScrollSession({ ++ completion: {}, ++ kind, ++ previousDataLength: 0 ++ }); ++ } else if (state.initialScrollSession.kind !== kind) { ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, ++ completion: state.initialScrollSession.completion, ++ kind, ++ previousDataLength: state.initialScrollSession.previousDataLength ++ }); ++ } ++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; ++ return state.initialScrollSession.completion; ++} ++var initialScrollCompletion = { ++ didDispatchNativeScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); ++ }, ++ didRetrySilentInitialScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); ++ }, ++ markInitialScrollNativeDispatch(state) { ++ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; ++ }, ++ markSilentInitialScrollRetry(state) { ++ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; ++ }, ++ resetFlags(state) { ++ if (!state.initialScrollSession) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); ++ completion.didDispatchNativeScroll = void 0; ++ completion.didRetrySilentInitialScroll = void 0; ++ } ++}; ++var initialScrollWatchdog = { ++ clear(state) { ++ initialScrollWatchdog.set(state, void 0); ++ }, ++ didReachTarget(newScroll, watchdog) { ++ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); ++ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ get(state) { ++ var _a3, _b; ++ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; ++ }, ++ hasNonZeroTargetOffset(targetOffset) { ++ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ isAtZeroTargetOffset(targetOffset) { ++ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ set(state, watchdog) { ++ var _a3, _b; ++ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state); ++ completion.watchdog = watchdog ? { ++ startScroll: watchdog.startScroll, ++ targetOffset: watchdog.targetOffset ++ } : void 0; ++ } ++}; ++function setInitialScrollSession(state, options = {}) { ++ var _a3, _b, _c, _d; ++ const existingSession = state.initialScrollSession; ++ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; ++ const completion = existingSession == null ? void 0 : existingSession.completion; ++ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; ++ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; ++ if (!kind) { ++ return clearInitialScrollSession(state); ++ } ++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { ++ return clearInitialScrollSession(state); ++ } ++ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }); ++ return state.initialScrollSession; ++} ++var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; ++function clearAdaptiveRenderExitTimeout(ctx) { ++ ctx.state.scheduledWork.cancel("adaptiveRender"); ++} ++function scheduleAdaptiveRenderExit(ctx, exitDelay) { ++ const state = ctx.state; ++ clearAdaptiveRenderExitTimeout(ctx); ++ if (exitDelay <= 0) { ++ setAdaptiveRender(ctx, "normal", "scroll"); ++ } else { ++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); ++ } ++} ++function setAdaptiveRender(ctx, mode, reason) { ++ var _a3, _b; ++ const previousMode = peek$(ctx, "adaptiveRender"); ++ if (previousMode !== mode) { ++ set$(ctx, "adaptiveRender", mode); ++ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); ++ } ++} ++function resetAdaptiveRender(ctx) { ++ var _a3, _b; ++ clearAdaptiveRenderExitTimeout(ctx); ++ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; ++ if (peek$(ctx, "adaptiveRender") !== mode) { ++ setAdaptiveRender(ctx, mode, "initial"); ++ } ++} ++function updateAdaptiveRender(ctx, scrollVelocity, options) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ const adaptiveRender = state.props.adaptiveRender; ++ const currentMode = peek$(ctx, "adaptiveRender"); ++ if (peek$(ctx, "readyToRender")) { ++ if (adaptiveRender) { ++ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; ++ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; ++ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; ++ const threshold = currentMode === "light" ? exitVelocity : enterVelocity; ++ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; ++ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; ++ if (nextMode !== previousMode) { ++ if (nextMode === "light") { ++ setAdaptiveRender(ctx, "light", "scroll"); ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } else if (currentMode === "light") { ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } ++ } ++ } else { ++ resetAdaptiveRender(ctx); ++ } ++ } + } + + // src/core/doMaintainScrollAtEnd.ts +@@ -1809,6 +1600,27 @@ function prepareMVCP(ctx, dataChanged) { + } + } + ++// src/utils/getEffectiveDrawDistance.ts ++var INITIAL_DRAW_DISTANCE = 50; ++function getEffectiveDrawDistance(ctx, mode) { ++ var _a3; ++ const drawDistance = ctx.state.props.drawDistance; ++ const initialScroll = ctx.state.initialScroll; ++ const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; ++ const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; ++ return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; ++} ++function scheduleFullDrawDistancePrewarm(ctx) { ++ const { state } = ctx; ++ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { ++ return; ++ } ++ state.scheduledWork.frame(() => { ++ var _a3; ++ return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); ++ }, "fullDrawDistancePrewarm"); ++} ++ + // src/utils/getScrollVelocity.ts + var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; + var SCROLL_VELOCITY_HALF_LIFE_MS = 200; +@@ -2069,7 +1881,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + } + } + function scrollTo(ctx, params) { +- var _a3, _b; ++ var _a3, _b, _c; + const state = ctx.state; + const { noScrollingTo, forceScroll, ...scrollTarget } = params; + const { +@@ -2101,6 +1913,15 @@ function scrollTo(ctx, params) { + }; + if (!isInitialScroll) { + pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); ++ if (scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { ++ beginScrollTargetSettle(ctx, { ++ index: scrollTarget.index, ++ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, ++ viewPosition: scrollTarget.viewPosition ++ }); ++ } else { ++ clearScrollTargetSettle(state); ++ } + } + } + state.scrollPending = targetOffset; +@@ -2108,7 +1929,7 @@ function scrollTo(ctx, params) { + if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { + if (animated) { + if (state.scrollTargetPinnedRange) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); + } + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); +@@ -2121,6 +1942,280 @@ function scrollTo(ctx, params) { + } + } + ++// src/core/scrollTargetSettle.ts ++var SETTLE_POSITION_EPSILON = 0.5; ++var SETTLE_QUIET_PASSES_TO_RELEASE = 2; ++var SETTLE_TTL_MS = 500; ++var SETTLE_MAX_MS = 1e3; ++var SETTLE_MAX_CORRECTIONS = 8; ++function clearScrollTargetSettle(state) { ++ state.scrollTargetSettle = void 0; ++} ++function beginScrollTargetSettle(ctx, params) { ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ const { data } = state.props; ++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; ++ if (index < 0 || index >= data.length || isEndAlignedLastItem) { ++ clearScrollTargetSettle(state); ++ return; ++ } ++ const now = Date.now(); ++ const id = getId(state, index); ++ const existing = state.scrollTargetSettle; ++ const isSameTarget = (existing == null ? void 0 : existing.id) === id; ++ state.scrollTargetSettle = { ++ corrections: isSameTarget ? existing.corrections : 0, ++ deadline: isSameTarget ? existing.deadline : now + SETTLE_MAX_MS, ++ expiresAt: now + SETTLE_TTL_MS, ++ id, ++ quietPasses: 0, ++ viewOffset, ++ viewPosition ++ }; ++} ++function getSettleTargetOffset(ctx, settle, index, position) { ++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; ++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); ++} ++function settleScrollTarget(ctx, isCompensating) { ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ return false; ++ } ++ if (isCompensating) { ++ settle.quietPasses = 0; ++ settle.expiresAt = Date.now() + SETTLE_TTL_MS; ++ return false; ++ } ++ const index = state.indexByKey.get(settle.id); ++ const position = index === void 0 ? void 0 : state.positions[index]; ++ if (index === void 0 || position === void 0) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ const now = Date.now(); ++ if (now > settle.expiresAt || now > settle.deadline) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position); ++ const diff = targetOffset - state.scroll; ++ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ settle.quietPasses++; ++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { ++ clearScrollTargetSettle(state); ++ } ++ return false; ++ } ++ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ settle.quietPasses = 0; ++ settle.corrections++; ++ settle.expiresAt = now + SETTLE_TTL_MS; ++ scrollTo(ctx, { ++ animated: false, ++ index, ++ itemSize: getItemSizeAtIndex(ctx, index), ++ offset: position, ++ viewOffset: settle.viewOffset, ++ viewPosition: settle.viewPosition ++ }); ++ return true; ++} ++ ++// src/core/cancelImperativeScroll.ts ++function cancelScrollCompletionChecks({ scheduledWork }) { ++ scheduledWork.cancel("checkFinishedScrollFrame"); ++ scheduledWork.cancel("checkFinishedScrollRetryFrame"); ++ scheduledWork.cancel("checkFinishedScrollFallback"); ++ scheduledWork.cancel("platformScrollCompletion"); ++} ++function settlePendingImperativeScroll(state) { ++ var _a3, _b; ++ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; ++ state.pendingScrollResolve = void 0; ++ state.pendingScrollToEnd = void 0; ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); ++} ++function cancelImperativeScroll(state) { ++ cancelScrollCompletionChecks(state); ++ state.scheduledWork.cancel("imperativeScrollReady"); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ clearScrollTargetSettle(state); ++ settlePendingImperativeScroll(state); ++} ++ ++// src/core/deferredPublicOnScroll.ts ++function withResolvedContentOffset(state, event, resolvedOffset) { ++ return { ++ ...event, ++ nativeEvent: { ++ ...event.nativeEvent, ++ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } ++ } ++ }; ++} ++function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { ++ var _a3, _b, _c, _d; ++ const state = ctx.state; ++ const deferredEvent = state.deferredPublicOnScrollEvent; ++ state.deferredPublicOnScrollEvent = void 0; ++ if (deferredEvent) { ++ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( ++ _c, ++ withResolvedContentOffset( ++ state, ++ deferredEvent, ++ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 ++ ) ++ ); ++ } ++} ++ ++// src/utils/setInitialRenderState.ts ++function resetInitialRenderState(ctx, { ++ resetLayout, ++ resetInitialScroll ++}) { ++ const { state } = ctx; ++ if (resetLayout) { ++ state.didContainersLayout = false; ++ state.queuedInitialLayout = false; ++ } ++ if (resetInitialScroll) { ++ state.didFinishInitialScroll = false; ++ } ++ set$(ctx, "readyToRender", false); ++ resetAdaptiveRender(ctx); ++} ++function setInitialRenderState(ctx, { ++ didLayout, ++ didInitialScroll ++}) { ++ const { state } = ctx; ++ const { ++ loadStartTime, ++ props: { onLoad } ++ } = state; ++ if (didLayout) { ++ state.didContainersLayout = true; ++ } ++ if (didInitialScroll) { ++ state.didFinishInitialScroll = true; ++ } ++ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); ++ if (isReadyToRender && !peek$(ctx, "readyToRender")) { ++ set$(ctx, "readyToRender", true); ++ setAdaptiveRender(ctx, "normal", "ready"); ++ if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { ++ scheduleFullDrawDistancePrewarm(ctx); ++ } ++ if (!state.didLoad) { ++ state.didLoad = true; ++ if (onLoad) { ++ onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); ++ } ++ } ++ } ++} ++ ++// src/core/finishInitialScroll.ts ++var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; ++function syncInitialScrollOffset(state, offset) { ++ state.scroll = offset; ++ state.scrollPending = offset; ++ state.scrollPrev = offset; ++} ++function clearPreservedInitialScrollTargetTimeout(state) { ++ state.scheduledWork.cancel("preservedInitialScroll"); ++} ++function clearPreservedInitialScrollTarget(state) { ++ clearPreservedInitialScrollTargetTimeout(state); ++ state.clearPreservedInitialScrollOnNextFinish = void 0; ++ state.initialScroll = void 0; ++ setInitialScrollSession(state); ++} ++function supersedeInitialScroll(ctx) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; ++ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { ++ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { ++ cancelAnimationFrame(bootstrapInitialScroll.frameHandle); ++ } ++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { ++ cancelScrollCompletionChecks(state); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ } ++ initialScrollCompletion.resetFlags(state); ++ setInitialScrollSession(state, { bootstrap: null }); ++ finishInitialScroll(ctx); ++ } ++} ++function finishInitialScroll(ctx, options) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { ++ syncInitialScrollOffset(state, options.resolvedOffset); ++ } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { ++ const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); ++ if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { ++ syncInitialScrollOffset(state, observedOffset); ++ } ++ } ++ const complete = () => { ++ var _a4, _b2, _c2, _d, _e; ++ const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; ++ const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0; ++ initialScrollWatchdog.clear(state); ++ if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) { ++ state.clearPreservedInitialScrollOnNextFinish = void 0; ++ setInitialScrollSession(state); ++ clearPreservedInitialScrollTargetTimeout(state); ++ if (options == null ? void 0 : options.schedulePreservedTargetClear) { ++ state.scheduledWork.timeout( ++ () => { ++ var _a5; ++ if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) { ++ return; ++ } ++ clearPreservedInitialScrollTarget(state); ++ }, ++ PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, ++ "preservedInitialScroll" ++ ); ++ } ++ } else { ++ clearPreservedInitialScrollTarget(state); ++ } ++ if (options == null ? void 0 : options.recalculateItems) { ++ recalculateSettledScroll(ctx); ++ } ++ setInitialRenderState(ctx, { didInitialScroll: true }); ++ if (shouldReleaseDeferredPublicOnScroll) { ++ releaseDeferredPublicOnScroll(ctx, finalScrollOffset); ++ } ++ (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); ++ }; ++ if (options == null ? void 0 : options.waitForCompletionFrame) { ++ requestAnimationFrame(complete); ++ return; ++ } ++ complete(); ++} ++ ++// src/core/calculateOffsetForIndex.ts ++function calculateOffsetForIndex(ctx, index) { ++ const state = ctx.state; ++ return index !== void 0 ? state.positions[index] || 0 : 0; ++} ++ + // src/core/scrollToIndex.ts + function clampScrollIndex(index, dataLength) { + if (dataLength <= 0) { +@@ -4367,7 +4462,10 @@ function calculateItemsInView(ctx, params = {}) { + const scrollBeforeMVCP = state.scroll; + const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; + checkMVCP == null ? void 0 : checkMVCP(); +- const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); ++ const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; ++ const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); ++ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx, didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0); ++ const didMVCPAdjustScroll = didMVCPAdjust || didSettleScrollTarget; + if (didMVCPAdjustScroll) { + updateScroll2(state.scroll); + updateScrollRange(); +@@ -6132,6 +6230,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + return (_a4 = scrollRef.current) == null ? void 0 : _a4.getBoundingClientRect(); + }, + getCurrentScrollOffset, ++ getMaxScrollOffset, + getScrollableNode: () => resolveScrollableNode(scrollRef.current, isWindowScroll), + getScrollEventTarget: () => getScrollTarget(), + getScrollResponder: () => resolveScrollableNode(scrollRef.current, isWindowScroll), +@@ -7205,6 +7304,22 @@ function createColumnWrapperStyle(contentContainerStyle) { + } + } + ++// src/core/isScrollExtentSynced.ts ++var SCROLL_EXTENT_EPSILON = 1; ++function isScrollExtentSynced(ctx) { ++ var _a3, _b; ++ const state = ctx.state; ++ const platformMaxOffset = (_b = (_a3 = state.refScroller.current) == null ? void 0 : _a3.getMaxScrollOffset) == null ? void 0 : _b.call(_a3); ++ if (platformMaxOffset === void 0) { ++ return true; ++ } ++ if (state.scrollLength <= 0) { ++ return true; ++ } ++ const contentMaxOffset = Math.max(0, getContentSize(ctx) - state.scrollLength); ++ return platformMaxOffset >= contentMaxOffset - SCROLL_EXTENT_EPSILON; ++} ++ + // src/core/scrollToEnd.ts + function scrollToEnd(ctx, options) { + const state = ctx.state; +@@ -7250,6 +7365,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { + const IMPERATIVE_SCROLL_SETTLE_STABLE_FRAMES = 2; + let imperativeScrollToken = 0; + const isSettlingAfterDataChange = () => !!state.didDataChange || !!state.didColumnsChange || state.scheduledWork.has("mvcpRecalculate") || state.ignoreScrollFromMVCP !== void 0; ++ const isScrollBlocked = () => isSettlingAfterDataChange() || !isScrollExtentSynced(ctx); + const isScrollToIndexReady = (targetIndex, allowEmpty = false) => { + var _a3; + const props = state.props; +@@ -7273,7 +7389,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { + if (token !== imperativeScrollToken) { + return; + } +- if (isSettlingAfterDataChange() || !isReady()) { ++ if (isScrollBlocked() || !isReady()) { + stableFrames = 0; + } else { + stableFrames += 1; +@@ -7300,7 +7416,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { + resolve(); + } + }; +- if (isSettlingAfterDataChange() || !isReady()) { ++ if (isScrollBlocked() || !isReady()) { + runWhenReady(token, runNow, isReady); + } else { + runNow(); +@@ -8288,6 +8404,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + onScrollBeginDrag: (event) => { + var _a4, _b2; + prepareReachedEdgeForNextUserScroll(ctx); ++ clearScrollTargetSettle(state); + (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); + }, + onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) +diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs +index 95465f2..a3e870c 100644 +--- a/node_modules/@legendapp/list/react-native.web.mjs ++++ b/node_modules/@legendapp/list/react-native.web.mjs +@@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; + var ENABLE_DEVMODE = IS_DEV && false; + var ENABLE_DEBUG_VIEW = IS_DEV && false; + +-// src/core/cancelImperativeScroll.ts +-function cancelScrollCompletionChecks({ scheduledWork }) { +- scheduledWork.cancel("checkFinishedScrollFrame"); +- scheduledWork.cancel("checkFinishedScrollRetryFrame"); +- scheduledWork.cancel("checkFinishedScrollFallback"); +- scheduledWork.cancel("platformScrollCompletion"); +-} +-function settlePendingImperativeScroll(state) { +- var _a3, _b; +- const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; +- state.pendingScrollResolve = void 0; +- state.pendingScrollToEnd = void 0; +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +-} +-function cancelImperativeScroll(state) { +- cancelScrollCompletionChecks(state); +- state.scheduledWork.cancel("imperativeScrollReady"); +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- settlePendingImperativeScroll(state); +-} +- +-// src/core/deferredPublicOnScroll.ts +-function withResolvedContentOffset(state, event, resolvedOffset) { +- return { +- ...event, +- nativeEvent: { +- ...event.nativeEvent, +- contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } +- } +- }; +-} +-function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { +- var _a3, _b, _c, _d; +- const state = ctx.state; +- const deferredEvent = state.deferredPublicOnScrollEvent; +- state.deferredPublicOnScrollEvent = void 0; +- if (deferredEvent) { +- (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( +- _c, +- withResolvedContentOffset( +- state, +- deferredEvent, +- (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 +- ) +- ); +- } +-} +- +-// src/core/initialScrollSession.ts +-var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +-function hasInitialScrollSessionCompletion(completion) { +- return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); +-} +-function clearInitialScrollSession(state) { +- state.initialScrollSession = void 0; +- return void 0; +-} +-function createInitialScrollSession(options) { +- const { bootstrap, completion, kind, previousDataLength } = options; +- return kind === "offset" ? { +- completion, +- kind, +- previousDataLength +- } : { +- bootstrap, +- completion, +- kind, +- previousDataLength +- }; +-} +-function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { +- var _a4, _b2; +- if (!state.initialScrollSession) { +- state.initialScrollSession = createInitialScrollSession({ +- completion: {}, +- kind, +- previousDataLength: 0 +- }); +- } else if (state.initialScrollSession.kind !== kind) { +- state.initialScrollSession = createInitialScrollSession({ +- bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, +- completion: state.initialScrollSession.completion, +- kind, +- previousDataLength: state.initialScrollSession.previousDataLength +- }); +- } +- (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; +- return state.initialScrollSession.completion; +-} +-var initialScrollCompletion = { +- didDispatchNativeScroll(state) { +- var _a3, _b; +- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); +- }, +- didRetrySilentInitialScroll(state) { +- var _a3, _b; +- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); +- }, +- markInitialScrollNativeDispatch(state) { +- ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; +- }, +- markSilentInitialScrollRetry(state) { +- ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; +- }, +- resetFlags(state) { +- if (!state.initialScrollSession) { +- return; +- } +- const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); +- completion.didDispatchNativeScroll = void 0; +- completion.didRetrySilentInitialScroll = void 0; +- } +-}; +-var initialScrollWatchdog = { +- clear(state) { +- initialScrollWatchdog.set(state, void 0); +- }, +- didReachTarget(newScroll, watchdog) { +- const nextDistance = Math.abs(newScroll - watchdog.targetOffset); +- return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; +- }, +- get(state) { +- var _a3, _b; +- return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; +- }, +- hasNonZeroTargetOffset(targetOffset) { +- return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; +- }, +- isAtZeroTargetOffset(targetOffset) { +- return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; +- }, +- set(state, watchdog) { +- var _a3, _b; +- if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { +- return; +- } +- const completion = ensureInitialScrollSessionCompletion(state); +- completion.watchdog = watchdog ? { +- startScroll: watchdog.startScroll, +- targetOffset: watchdog.targetOffset +- } : void 0; +- } +-}; +-function setInitialScrollSession(state, options = {}) { +- var _a3, _b, _c, _d; +- const existingSession = state.initialScrollSession; +- const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; +- const completion = existingSession == null ? void 0 : existingSession.completion; +- const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; +- const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; +- if (!kind) { +- return clearInitialScrollSession(state); +- } +- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { +- return clearInitialScrollSession(state); +- } +- const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; +- state.initialScrollSession = createInitialScrollSession({ +- bootstrap, +- completion, +- kind, +- previousDataLength +- }); +- return state.initialScrollSession; +-} +- +-// src/utils/checkThreshold.ts +-var HYSTERESIS_MULTIPLIER = 1.3; +-function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { +- const absDistance = Math.abs(distance); +- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; +-} +-var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { +- const absDistance = Math.abs(distance); +- const within = atThreshold || threshold > 0 && absDistance <= threshold; +- const updateSnapshot = () => { +- setSnapshot({ +- atThreshold, +- contentSize: context.contentSize, +- dataLength: context.dataLength, +- scrollPosition: context.scrollPosition +- }); +- }; +- if (!wasReached) { +- if (!within) { +- return false; +- } +- onReached(distance); +- updateSnapshot(); +- return true; +- } +- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); +- if (reset) { +- setSnapshot(void 0); +- return false; +- } +- if (within) { +- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; +- if (changed) { +- updateSnapshot(); +- } +- } +- return true; +-}; +- +-// src/utils/edgeReachedGate.ts +-function resetEdgeLatch(ctx, edge) { +- const state = ctx.state; +- if (edge === "start") { +- state.isStartReached = false; +- state.startReachedSnapshot = void 0; +- } else { +- state.isEndReached = false; +- state.endReachedSnapshot = void 0; +- } +-} +-function resetSharedEdgeGateIfOutsideHysteresis(ctx) { +- const state = ctx.state; +- if (!state.edgeReachedGate) { +- return; +- } +- const contentSize = getContentSize(ctx); +- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); +- const isContentLess = contentSize < state.scrollLength; +- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; +- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; +- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); +- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); +- if (isOutsideStart && isOutsideEnd) { +- state.edgeReachedGate = void 0; +- } +-} +-function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { +- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; +-} +-function markReachedEdge(ctx) { +- ctx.state.edgeReachedGate = "closed"; +-} +-function prepareReachedEdgeForNextUserScroll(ctx) { +- if (ctx.state.edgeReachedGate) { +- ctx.state.edgeReachedGate = "prepared"; +- } +-} +-function beginReachedEdgeUserScroll(ctx, scrollDelta) { +- const state = ctx.state; +- if (state.edgeReachedGate !== "prepared") { +- return void 0; +- } +- const allowedEdge = scrollDelta < 0 ? "start" : "end"; +- state.edgeReachedGate = "closed"; +- resetEdgeLatch(ctx, allowedEdge); +- return allowedEdge; +-} +- +-// src/utils/hasActiveInitialScroll.ts +-function hasActiveInitialScroll(state) { +- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; +-} +- +-// src/utils/checkAtBottom.ts +-function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { +- var _a3; +- const state = ctx.state; +- if (!state) { +- return; +- } +- const { +- queuedInitialLayout, +- scrollLength, +- scroll, +- maintainingScrollAtEnd, +- props: { maintainScrollAtEndThreshold, onEndReachedThreshold } +- } = state; +- const contentSize = getContentSize(ctx); +- resetSharedEdgeGateIfOutsideHysteresis(ctx); +- if (contentSize > 0 && queuedInitialLayout) { +- const insetEnd = getContentInsetEnd(ctx); +- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; +- const isContentLess = contentSize < scrollLength; +- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); +- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); +- set$( +- ctx, +- "isWithinMaintainScrollAtEndThreshold", +- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength +- ); +- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; +- if (!shouldSkipThresholdChecks) { +- state.isEndReached = checkThreshold( +- distanceFromEnd, +- isContentLess, +- onEndReachedThreshold * scrollLength, +- state.isEndReached, +- state.endReachedSnapshot, +- { +- contentSize, +- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, +- scrollPosition: scroll +- }, +- (distance) => { +- var _a4, _b; +- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { +- markReachedEdge(ctx); +- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); +- } +- }, +- (snapshot) => { +- state.endReachedSnapshot = snapshot; +- } +- ); +- } +- } +-} +- +-// src/utils/checkAtTop.ts +-function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { +- const state = ctx == null ? void 0 : ctx.state; +- if (!state) { +- return; +- } +- const { +- isStartReached, +- props: { data, onStartReachedThreshold }, +- scroll, +- scrollLength, +- startReachedSnapshot, +- totalSize +- } = state; +- const dataLength = data.length; +- const threshold = onStartReachedThreshold * scrollLength; +- resetSharedEdgeGateIfOutsideHysteresis(ctx); +- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { +- state.isStartReached = false; +- state.startReachedSnapshot = void 0; +- } +- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); +- set$(ctx, "isNearStart", scroll <= threshold); +- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; +- if (!shouldSkipThresholdChecks) { +- state.isStartReached = checkThreshold( +- scroll, +- false, +- threshold, +- state.isStartReached, +- startReachedSnapshot, +- { +- contentSize: totalSize, +- dataLength, +- scrollPosition: scroll +- }, +- (distance) => { +- var _a3, _b; +- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { +- markReachedEdge(ctx); +- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); +- } +- }, +- (snapshot) => { +- state.startReachedSnapshot = snapshot; +- } +- ); +- } +-} +- +-// src/utils/checkThresholds.ts +-function checkThresholds(ctx, allowedEdge) { +- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; +- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); +- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); +-} +- +-// src/core/recalculateSettledScroll.ts +-function recalculateSettledScroll(ctx) { +- var _a3, _b; +- const state = ctx.state; +- if ((_a3 = state.props) == null ? void 0 : _a3.data) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); +- } +- checkThresholds(ctx); +-} +-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; +-function clearAdaptiveRenderExitTimeout(ctx) { +- ctx.state.scheduledWork.cancel("adaptiveRender"); +-} +-function scheduleAdaptiveRenderExit(ctx, exitDelay) { +- const state = ctx.state; +- clearAdaptiveRenderExitTimeout(ctx); +- if (exitDelay <= 0) { +- setAdaptiveRender(ctx, "normal", "scroll"); +- } else { +- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); +- } +-} +-function setAdaptiveRender(ctx, mode, reason) { +- var _a3, _b; +- const previousMode = peek$(ctx, "adaptiveRender"); +- if (previousMode !== mode) { +- set$(ctx, "adaptiveRender", mode); +- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); +- } +-} +-function resetAdaptiveRender(ctx) { +- var _a3, _b; +- clearAdaptiveRenderExitTimeout(ctx); +- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; +- if (peek$(ctx, "adaptiveRender") !== mode) { +- setAdaptiveRender(ctx, mode, "initial"); +- } +-} +-function updateAdaptiveRender(ctx, scrollVelocity, options) { +- var _a3, _b, _c; +- const state = ctx.state; +- const adaptiveRender = state.props.adaptiveRender; +- const currentMode = peek$(ctx, "adaptiveRender"); +- if (peek$(ctx, "readyToRender")) { +- if (adaptiveRender) { +- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; +- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; +- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; +- const threshold = currentMode === "light" ? exitVelocity : enterVelocity; +- const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; +- const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; +- if (nextMode !== previousMode) { +- if (nextMode === "light") { +- setAdaptiveRender(ctx, "light", "scroll"); +- scheduleAdaptiveRenderExit(ctx, exitDelay); +- } else if (currentMode === "light") { +- scheduleAdaptiveRenderExit(ctx, exitDelay); +- } +- } +- } else { +- resetAdaptiveRender(ctx); +- } +- } +-} +- +-// src/utils/getEffectiveDrawDistance.ts +-var INITIAL_DRAW_DISTANCE = 50; +-function getEffectiveDrawDistance(ctx, mode) { +- var _a3; +- const drawDistance = ctx.state.props.drawDistance; +- const initialScroll = ctx.state.initialScroll; +- const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; +- const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; +- return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; +-} +-function scheduleFullDrawDistancePrewarm(ctx) { +- const { state } = ctx; +- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { +- return; +- } +- state.scheduledWork.frame(() => { +- var _a3; +- return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +- }, "fullDrawDistancePrewarm"); +-} +- +-// src/utils/setInitialRenderState.ts +-function resetInitialRenderState(ctx, { +- resetLayout, +- resetInitialScroll +-}) { +- const { state } = ctx; +- if (resetLayout) { +- state.didContainersLayout = false; +- state.queuedInitialLayout = false; +- } +- if (resetInitialScroll) { +- state.didFinishInitialScroll = false; +- } +- set$(ctx, "readyToRender", false); +- resetAdaptiveRender(ctx); +-} +-function setInitialRenderState(ctx, { +- didLayout, +- didInitialScroll +-}) { +- const { state } = ctx; +- const { +- loadStartTime, +- props: { onLoad } +- } = state; +- if (didLayout) { +- state.didContainersLayout = true; +- } +- if (didInitialScroll) { +- state.didFinishInitialScroll = true; +- } +- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); +- if (isReadyToRender && !peek$(ctx, "readyToRender")) { +- set$(ctx, "readyToRender", true); +- setAdaptiveRender(ctx, "normal", "ready"); +- if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { +- scheduleFullDrawDistancePrewarm(ctx); +- } +- if (!state.didLoad) { +- state.didLoad = true; +- if (onLoad) { +- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); +- } +- } +- } +-} +- +-// src/core/finishInitialScroll.ts +-var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; +-function syncInitialScrollOffset(state, offset) { +- state.scroll = offset; +- state.scrollPending = offset; +- state.scrollPrev = offset; +-} +-function clearPreservedInitialScrollTargetTimeout(state) { +- state.scheduledWork.cancel("preservedInitialScroll"); +-} +-function clearPreservedInitialScrollTarget(state) { +- clearPreservedInitialScrollTargetTimeout(state); +- state.clearPreservedInitialScrollOnNextFinish = void 0; +- state.initialScroll = void 0; +- setInitialScrollSession(state); +-} +-function supersedeInitialScroll(ctx) { +- var _a3, _b, _c; +- const state = ctx.state; +- const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; +- if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { +- if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { +- cancelAnimationFrame(bootstrapInitialScroll.frameHandle); +- } +- if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { +- cancelScrollCompletionChecks(state); +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- } +- initialScrollCompletion.resetFlags(state); +- setInitialScrollSession(state, { bootstrap: null }); +- finishInitialScroll(ctx); +- } +-} +-function finishInitialScroll(ctx, options) { +- var _a3, _b, _c; +- const state = ctx.state; +- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { +- syncInitialScrollOffset(state, options.resolvedOffset); +- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { +- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); +- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { +- syncInitialScrollOffset(state, observedOffset); +- } +- } +- const complete = () => { +- var _a4, _b2, _c2, _d, _e; +- const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; +- const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0; +- initialScrollWatchdog.clear(state); +- if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) { +- state.clearPreservedInitialScrollOnNextFinish = void 0; +- setInitialScrollSession(state); +- clearPreservedInitialScrollTargetTimeout(state); +- if (options == null ? void 0 : options.schedulePreservedTargetClear) { +- state.scheduledWork.timeout( +- () => { +- var _a5; +- if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) { +- return; +- } +- clearPreservedInitialScrollTarget(state); +- }, +- PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, +- "preservedInitialScroll" +- ); +- } +- } else { +- clearPreservedInitialScrollTarget(state); +- } +- if (options == null ? void 0 : options.recalculateItems) { +- recalculateSettledScroll(ctx); +- } +- setInitialRenderState(ctx, { didInitialScroll: true }); +- if (shouldReleaseDeferredPublicOnScroll) { +- releaseDeferredPublicOnScroll(ctx, finalScrollOffset); +- } +- (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); +- }; +- if (options == null ? void 0 : options.waitForCompletionFrame) { +- requestAnimationFrame(complete); +- return; +- } +- complete(); +-} +- +-// src/core/calculateOffsetForIndex.ts +-function calculateOffsetForIndex(ctx, index) { +- const state = ctx.state; +- return index !== void 0 ? state.positions[index] || 0 : 0; +-} +- + // src/core/getStartOffsetAdjustment.ts + function getStartOffsetAdjustment(ctx) { + const { state } = ctx; +@@ -1213,58 +614,273 @@ function getItemSizeAtIndex(ctx, index) { + if (index === void 0 || index < 0) { + return void 0; + } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; ++ } ++ if (index !== void 0) { ++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); ++ if (startOffsetAdjustment) { ++ offset += startOffsetAdjustment; ++ } ++ } ++ if (viewPosition !== void 0 && index !== void 0) { ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return offset; ++ } ++ const isOutOfBounds = index < 0 || index >= dataLength; ++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; ++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); ++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); ++ const trailingInset = getContentInsetEnd(ctx); ++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); ++ if (!isOutOfBounds && index === state.props.data.length - 1) { ++ const footerSize = peek$(ctx, "footerSize") || 0; ++ offset += footerSize; ++ } ++ } ++ return offset; ++} ++ ++// src/core/clampScrollOffset.ts ++function clampScrollOffset(ctx, offset, scrollTarget) { ++ const state = ctx.state; ++ const contentSize = getContentSize(ctx); ++ let clampedOffset = offset; ++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { ++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); ++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; ++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; ++ const maxOffset = baseMaxOffset + extraEndOffset; ++ clampedOffset = Math.min(offset, maxOffset); ++ } ++ clampedOffset = Math.max(0, clampedOffset); ++ return clampedOffset; ++} ++ ++// src/utils/checkThreshold.ts ++var HYSTERESIS_MULTIPLIER = 1.3; ++function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { ++ const absDistance = Math.abs(distance); ++ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; ++} ++var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { ++ const absDistance = Math.abs(distance); ++ const within = atThreshold || threshold > 0 && absDistance <= threshold; ++ const updateSnapshot = () => { ++ setSnapshot({ ++ atThreshold, ++ contentSize: context.contentSize, ++ dataLength: context.dataLength, ++ scrollPosition: context.scrollPosition ++ }); ++ }; ++ if (!wasReached) { ++ if (!within) { ++ return false; ++ } ++ onReached(distance); ++ updateSnapshot(); ++ return true; ++ } ++ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); ++ if (reset) { ++ setSnapshot(void 0); ++ return false; ++ } ++ if (within) { ++ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; ++ if (changed) { ++ updateSnapshot(); ++ } ++ } ++ return true; ++}; ++ ++// src/utils/edgeReachedGate.ts ++function resetEdgeLatch(ctx, edge) { ++ const state = ctx.state; ++ if (edge === "start") { ++ state.isStartReached = false; ++ state.startReachedSnapshot = void 0; ++ } else { ++ state.isEndReached = false; ++ state.endReachedSnapshot = void 0; ++ } ++} ++function resetSharedEdgeGateIfOutsideHysteresis(ctx) { ++ const state = ctx.state; ++ if (!state.edgeReachedGate) { ++ return; ++ } ++ const contentSize = getContentSize(ctx); ++ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); ++ const isContentLess = contentSize < state.scrollLength; ++ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; ++ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; ++ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); ++ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); ++ if (isOutsideStart && isOutsideEnd) { ++ state.edgeReachedGate = void 0; ++ } ++} ++function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { ++ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; ++} ++function markReachedEdge(ctx) { ++ ctx.state.edgeReachedGate = "closed"; ++} ++function prepareReachedEdgeForNextUserScroll(ctx) { ++ if (ctx.state.edgeReachedGate) { ++ ctx.state.edgeReachedGate = "prepared"; ++ } ++} ++function beginReachedEdgeUserScroll(ctx, scrollDelta) { ++ const state = ctx.state; ++ if (state.edgeReachedGate !== "prepared") { ++ return void 0; ++ } ++ const allowedEdge = scrollDelta < 0 ? "start" : "end"; ++ state.edgeReachedGate = "closed"; ++ resetEdgeLatch(ctx, allowedEdge); ++ return allowedEdge; ++} ++ ++// src/utils/hasActiveInitialScroll.ts ++function hasActiveInitialScroll(state) { ++ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; ++} ++ ++// src/utils/checkAtBottom.ts ++function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { ++ var _a3; ++ const state = ctx.state; ++ if (!state) { ++ return; ++ } ++ const { ++ queuedInitialLayout, ++ scrollLength, ++ scroll, ++ maintainingScrollAtEnd, ++ props: { maintainScrollAtEndThreshold, onEndReachedThreshold } ++ } = state; ++ const contentSize = getContentSize(ctx); ++ resetSharedEdgeGateIfOutsideHysteresis(ctx); ++ if (contentSize > 0 && queuedInitialLayout) { ++ const insetEnd = getContentInsetEnd(ctx); ++ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; ++ const isContentLess = contentSize < scrollLength; ++ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); ++ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); ++ set$( ++ ctx, ++ "isWithinMaintainScrollAtEndThreshold", ++ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength ++ ); ++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; ++ if (!shouldSkipThresholdChecks) { ++ state.isEndReached = checkThreshold( ++ distanceFromEnd, ++ isContentLess, ++ onEndReachedThreshold * scrollLength, ++ state.isEndReached, ++ state.endReachedSnapshot, ++ { ++ contentSize, ++ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, ++ scrollPosition: scroll ++ }, ++ (distance) => { ++ var _a4, _b; ++ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { ++ markReachedEdge(ctx); ++ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); ++ } ++ }, ++ (snapshot) => { ++ state.endReachedSnapshot = snapshot; ++ } ++ ); ++ } ++ } ++} ++ ++// src/utils/checkAtTop.ts ++function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { ++ const state = ctx == null ? void 0 : ctx.state; ++ if (!state) { ++ return; ++ } ++ const { ++ isStartReached, ++ props: { data, onStartReachedThreshold }, ++ scroll, ++ scrollLength, ++ startReachedSnapshot, ++ totalSize ++ } = state; ++ const dataLength = data.length; ++ const threshold = onStartReachedThreshold * scrollLength; ++ resetSharedEdgeGateIfOutsideHysteresis(ctx); ++ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { ++ state.isStartReached = false; ++ state.startReachedSnapshot = void 0; ++ } ++ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); ++ set$(ctx, "isNearStart", scroll <= threshold); ++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; ++ if (!shouldSkipThresholdChecks) { ++ state.isStartReached = checkThreshold( ++ scroll, ++ false, ++ threshold, ++ state.isStartReached, ++ startReachedSnapshot, ++ { ++ contentSize: totalSize, ++ dataLength, ++ scrollPosition: scroll ++ }, ++ (distance) => { ++ var _a3, _b; ++ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { ++ markReachedEdge(ctx); ++ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); ++ } ++ }, ++ (snapshot) => { ++ state.startReachedSnapshot = snapshot; ++ } ++ ); ++ } + } + +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +- } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } +- } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } +- } +- return offset; ++// src/utils/checkThresholds.ts ++function checkThresholds(ctx, allowedEdge) { ++ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; ++ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); ++ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + } + +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { ++// src/core/recalculateSettledScroll.ts ++function recalculateSettledScroll(ctx) { ++ var _a3, _b; + const state = ctx.state; +- const contentSize = getContentSize(ctx); +- let clampedOffset = offset; +- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { +- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); +- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; +- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; +- const maxOffset = baseMaxOffset + extraEndOffset; +- clampedOffset = Math.min(offset, maxOffset); ++ if ((_a3 = state.props) == null ? void 0 : _a3.data) { ++ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); + } +- clampedOffset = Math.max(0, clampedOffset); +- return clampedOffset; ++ checkThresholds(ctx); + } + + // src/core/finishScrollTo.ts +@@ -1401,7 +1017,182 @@ function listenForScrollEnd(ctx, params) { + } else { + idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); + } +- scheduledWork.register("platformScrollCompletion", cancel); ++ scheduledWork.register("platformScrollCompletion", cancel); ++} ++ ++// src/core/initialScrollSession.ts ++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; ++function hasInitialScrollSessionCompletion(completion) { ++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); ++} ++function clearInitialScrollSession(state) { ++ state.initialScrollSession = void 0; ++ return void 0; ++} ++function createInitialScrollSession(options) { ++ const { bootstrap, completion, kind, previousDataLength } = options; ++ return kind === "offset" ? { ++ completion, ++ kind, ++ previousDataLength ++ } : { ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }; ++} ++function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { ++ var _a4, _b2; ++ if (!state.initialScrollSession) { ++ state.initialScrollSession = createInitialScrollSession({ ++ completion: {}, ++ kind, ++ previousDataLength: 0 ++ }); ++ } else if (state.initialScrollSession.kind !== kind) { ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, ++ completion: state.initialScrollSession.completion, ++ kind, ++ previousDataLength: state.initialScrollSession.previousDataLength ++ }); ++ } ++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; ++ return state.initialScrollSession.completion; ++} ++var initialScrollCompletion = { ++ didDispatchNativeScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); ++ }, ++ didRetrySilentInitialScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); ++ }, ++ markInitialScrollNativeDispatch(state) { ++ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; ++ }, ++ markSilentInitialScrollRetry(state) { ++ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; ++ }, ++ resetFlags(state) { ++ if (!state.initialScrollSession) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); ++ completion.didDispatchNativeScroll = void 0; ++ completion.didRetrySilentInitialScroll = void 0; ++ } ++}; ++var initialScrollWatchdog = { ++ clear(state) { ++ initialScrollWatchdog.set(state, void 0); ++ }, ++ didReachTarget(newScroll, watchdog) { ++ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); ++ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ get(state) { ++ var _a3, _b; ++ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; ++ }, ++ hasNonZeroTargetOffset(targetOffset) { ++ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ isAtZeroTargetOffset(targetOffset) { ++ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ set(state, watchdog) { ++ var _a3, _b; ++ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state); ++ completion.watchdog = watchdog ? { ++ startScroll: watchdog.startScroll, ++ targetOffset: watchdog.targetOffset ++ } : void 0; ++ } ++}; ++function setInitialScrollSession(state, options = {}) { ++ var _a3, _b, _c, _d; ++ const existingSession = state.initialScrollSession; ++ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; ++ const completion = existingSession == null ? void 0 : existingSession.completion; ++ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; ++ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; ++ if (!kind) { ++ return clearInitialScrollSession(state); ++ } ++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { ++ return clearInitialScrollSession(state); ++ } ++ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }); ++ return state.initialScrollSession; ++} ++var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; ++function clearAdaptiveRenderExitTimeout(ctx) { ++ ctx.state.scheduledWork.cancel("adaptiveRender"); ++} ++function scheduleAdaptiveRenderExit(ctx, exitDelay) { ++ const state = ctx.state; ++ clearAdaptiveRenderExitTimeout(ctx); ++ if (exitDelay <= 0) { ++ setAdaptiveRender(ctx, "normal", "scroll"); ++ } else { ++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); ++ } ++} ++function setAdaptiveRender(ctx, mode, reason) { ++ var _a3, _b; ++ const previousMode = peek$(ctx, "adaptiveRender"); ++ if (previousMode !== mode) { ++ set$(ctx, "adaptiveRender", mode); ++ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); ++ } ++} ++function resetAdaptiveRender(ctx) { ++ var _a3, _b; ++ clearAdaptiveRenderExitTimeout(ctx); ++ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; ++ if (peek$(ctx, "adaptiveRender") !== mode) { ++ setAdaptiveRender(ctx, mode, "initial"); ++ } ++} ++function updateAdaptiveRender(ctx, scrollVelocity, options) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ const adaptiveRender = state.props.adaptiveRender; ++ const currentMode = peek$(ctx, "adaptiveRender"); ++ if (peek$(ctx, "readyToRender")) { ++ if (adaptiveRender) { ++ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; ++ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; ++ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; ++ const threshold = currentMode === "light" ? exitVelocity : enterVelocity; ++ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; ++ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; ++ if (nextMode !== previousMode) { ++ if (nextMode === "light") { ++ setAdaptiveRender(ctx, "light", "scroll"); ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } else if (currentMode === "light") { ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } ++ } ++ } else { ++ resetAdaptiveRender(ctx); ++ } ++ } + } + + // src/core/doMaintainScrollAtEnd.ts +@@ -1788,6 +1579,27 @@ function prepareMVCP(ctx, dataChanged) { + } + } + ++// src/utils/getEffectiveDrawDistance.ts ++var INITIAL_DRAW_DISTANCE = 50; ++function getEffectiveDrawDistance(ctx, mode) { ++ var _a3; ++ const drawDistance = ctx.state.props.drawDistance; ++ const initialScroll = ctx.state.initialScroll; ++ const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; ++ const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; ++ return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; ++} ++function scheduleFullDrawDistancePrewarm(ctx) { ++ const { state } = ctx; ++ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { ++ return; ++ } ++ state.scheduledWork.frame(() => { ++ var _a3; ++ return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); ++ }, "fullDrawDistancePrewarm"); ++} ++ + // src/utils/getScrollVelocity.ts + var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; + var SCROLL_VELOCITY_HALF_LIFE_MS = 200; +@@ -2048,7 +1860,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + } + } + function scrollTo(ctx, params) { +- var _a3, _b; ++ var _a3, _b, _c; + const state = ctx.state; + const { noScrollingTo, forceScroll, ...scrollTarget } = params; + const { +@@ -2080,6 +1892,15 @@ function scrollTo(ctx, params) { + }; + if (!isInitialScroll) { + pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); ++ if (scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { ++ beginScrollTargetSettle(ctx, { ++ index: scrollTarget.index, ++ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, ++ viewPosition: scrollTarget.viewPosition ++ }); ++ } else { ++ clearScrollTargetSettle(state); ++ } + } + } + state.scrollPending = targetOffset; +@@ -2087,7 +1908,7 @@ function scrollTo(ctx, params) { + if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { + if (animated) { + if (state.scrollTargetPinnedRange) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); + } + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); +@@ -2100,6 +1921,280 @@ function scrollTo(ctx, params) { + } + } + ++// src/core/scrollTargetSettle.ts ++var SETTLE_POSITION_EPSILON = 0.5; ++var SETTLE_QUIET_PASSES_TO_RELEASE = 2; ++var SETTLE_TTL_MS = 500; ++var SETTLE_MAX_MS = 1e3; ++var SETTLE_MAX_CORRECTIONS = 8; ++function clearScrollTargetSettle(state) { ++ state.scrollTargetSettle = void 0; ++} ++function beginScrollTargetSettle(ctx, params) { ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ const { data } = state.props; ++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; ++ if (index < 0 || index >= data.length || isEndAlignedLastItem) { ++ clearScrollTargetSettle(state); ++ return; ++ } ++ const now = Date.now(); ++ const id = getId(state, index); ++ const existing = state.scrollTargetSettle; ++ const isSameTarget = (existing == null ? void 0 : existing.id) === id; ++ state.scrollTargetSettle = { ++ corrections: isSameTarget ? existing.corrections : 0, ++ deadline: isSameTarget ? existing.deadline : now + SETTLE_MAX_MS, ++ expiresAt: now + SETTLE_TTL_MS, ++ id, ++ quietPasses: 0, ++ viewOffset, ++ viewPosition ++ }; ++} ++function getSettleTargetOffset(ctx, settle, index, position) { ++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; ++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); ++} ++function settleScrollTarget(ctx, isCompensating) { ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ return false; ++ } ++ if (isCompensating) { ++ settle.quietPasses = 0; ++ settle.expiresAt = Date.now() + SETTLE_TTL_MS; ++ return false; ++ } ++ const index = state.indexByKey.get(settle.id); ++ const position = index === void 0 ? void 0 : state.positions[index]; ++ if (index === void 0 || position === void 0) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ const now = Date.now(); ++ if (now > settle.expiresAt || now > settle.deadline) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position); ++ const diff = targetOffset - state.scroll; ++ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ settle.quietPasses++; ++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { ++ clearScrollTargetSettle(state); ++ } ++ return false; ++ } ++ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ settle.quietPasses = 0; ++ settle.corrections++; ++ settle.expiresAt = now + SETTLE_TTL_MS; ++ scrollTo(ctx, { ++ animated: false, ++ index, ++ itemSize: getItemSizeAtIndex(ctx, index), ++ offset: position, ++ viewOffset: settle.viewOffset, ++ viewPosition: settle.viewPosition ++ }); ++ return true; ++} ++ ++// src/core/cancelImperativeScroll.ts ++function cancelScrollCompletionChecks({ scheduledWork }) { ++ scheduledWork.cancel("checkFinishedScrollFrame"); ++ scheduledWork.cancel("checkFinishedScrollRetryFrame"); ++ scheduledWork.cancel("checkFinishedScrollFallback"); ++ scheduledWork.cancel("platformScrollCompletion"); ++} ++function settlePendingImperativeScroll(state) { ++ var _a3, _b; ++ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; ++ state.pendingScrollResolve = void 0; ++ state.pendingScrollToEnd = void 0; ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); ++} ++function cancelImperativeScroll(state) { ++ cancelScrollCompletionChecks(state); ++ state.scheduledWork.cancel("imperativeScrollReady"); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ clearScrollTargetSettle(state); ++ settlePendingImperativeScroll(state); ++} ++ ++// src/core/deferredPublicOnScroll.ts ++function withResolvedContentOffset(state, event, resolvedOffset) { ++ return { ++ ...event, ++ nativeEvent: { ++ ...event.nativeEvent, ++ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } ++ } ++ }; ++} ++function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { ++ var _a3, _b, _c, _d; ++ const state = ctx.state; ++ const deferredEvent = state.deferredPublicOnScrollEvent; ++ state.deferredPublicOnScrollEvent = void 0; ++ if (deferredEvent) { ++ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( ++ _c, ++ withResolvedContentOffset( ++ state, ++ deferredEvent, ++ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 ++ ) ++ ); ++ } ++} ++ ++// src/utils/setInitialRenderState.ts ++function resetInitialRenderState(ctx, { ++ resetLayout, ++ resetInitialScroll ++}) { ++ const { state } = ctx; ++ if (resetLayout) { ++ state.didContainersLayout = false; ++ state.queuedInitialLayout = false; ++ } ++ if (resetInitialScroll) { ++ state.didFinishInitialScroll = false; ++ } ++ set$(ctx, "readyToRender", false); ++ resetAdaptiveRender(ctx); ++} ++function setInitialRenderState(ctx, { ++ didLayout, ++ didInitialScroll ++}) { ++ const { state } = ctx; ++ const { ++ loadStartTime, ++ props: { onLoad } ++ } = state; ++ if (didLayout) { ++ state.didContainersLayout = true; ++ } ++ if (didInitialScroll) { ++ state.didFinishInitialScroll = true; ++ } ++ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); ++ if (isReadyToRender && !peek$(ctx, "readyToRender")) { ++ set$(ctx, "readyToRender", true); ++ setAdaptiveRender(ctx, "normal", "ready"); ++ if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { ++ scheduleFullDrawDistancePrewarm(ctx); ++ } ++ if (!state.didLoad) { ++ state.didLoad = true; ++ if (onLoad) { ++ onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); ++ } ++ } ++ } ++} ++ ++// src/core/finishInitialScroll.ts ++var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; ++function syncInitialScrollOffset(state, offset) { ++ state.scroll = offset; ++ state.scrollPending = offset; ++ state.scrollPrev = offset; ++} ++function clearPreservedInitialScrollTargetTimeout(state) { ++ state.scheduledWork.cancel("preservedInitialScroll"); ++} ++function clearPreservedInitialScrollTarget(state) { ++ clearPreservedInitialScrollTargetTimeout(state); ++ state.clearPreservedInitialScrollOnNextFinish = void 0; ++ state.initialScroll = void 0; ++ setInitialScrollSession(state); ++} ++function supersedeInitialScroll(ctx) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; ++ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { ++ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { ++ cancelAnimationFrame(bootstrapInitialScroll.frameHandle); ++ } ++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { ++ cancelScrollCompletionChecks(state); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ } ++ initialScrollCompletion.resetFlags(state); ++ setInitialScrollSession(state, { bootstrap: null }); ++ finishInitialScroll(ctx); ++ } ++} ++function finishInitialScroll(ctx, options) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { ++ syncInitialScrollOffset(state, options.resolvedOffset); ++ } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { ++ const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); ++ if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { ++ syncInitialScrollOffset(state, observedOffset); ++ } ++ } ++ const complete = () => { ++ var _a4, _b2, _c2, _d, _e; ++ const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; ++ const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0; ++ initialScrollWatchdog.clear(state); ++ if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) { ++ state.clearPreservedInitialScrollOnNextFinish = void 0; ++ setInitialScrollSession(state); ++ clearPreservedInitialScrollTargetTimeout(state); ++ if (options == null ? void 0 : options.schedulePreservedTargetClear) { ++ state.scheduledWork.timeout( ++ () => { ++ var _a5; ++ if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) { ++ return; ++ } ++ clearPreservedInitialScrollTarget(state); ++ }, ++ PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, ++ "preservedInitialScroll" ++ ); ++ } ++ } else { ++ clearPreservedInitialScrollTarget(state); ++ } ++ if (options == null ? void 0 : options.recalculateItems) { ++ recalculateSettledScroll(ctx); ++ } ++ setInitialRenderState(ctx, { didInitialScroll: true }); ++ if (shouldReleaseDeferredPublicOnScroll) { ++ releaseDeferredPublicOnScroll(ctx, finalScrollOffset); ++ } ++ (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); ++ }; ++ if (options == null ? void 0 : options.waitForCompletionFrame) { ++ requestAnimationFrame(complete); ++ return; ++ } ++ complete(); ++} ++ ++// src/core/calculateOffsetForIndex.ts ++function calculateOffsetForIndex(ctx, index) { ++ const state = ctx.state; ++ return index !== void 0 ? state.positions[index] || 0 : 0; ++} ++ + // src/core/scrollToIndex.ts + function clampScrollIndex(index, dataLength) { + if (dataLength <= 0) { +@@ -4346,7 +4441,10 @@ function calculateItemsInView(ctx, params = {}) { + const scrollBeforeMVCP = state.scroll; + const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; + checkMVCP == null ? void 0 : checkMVCP(); +- const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); ++ const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; ++ const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); ++ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx, didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0); ++ const didMVCPAdjustScroll = didMVCPAdjust || didSettleScrollTarget; + if (didMVCPAdjustScroll) { + updateScroll2(state.scroll); + updateScrollRange(); +@@ -6111,6 +6209,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + return (_a4 = scrollRef.current) == null ? void 0 : _a4.getBoundingClientRect(); + }, + getCurrentScrollOffset, ++ getMaxScrollOffset, + getScrollableNode: () => resolveScrollableNode(scrollRef.current, isWindowScroll), + getScrollEventTarget: () => getScrollTarget(), + getScrollResponder: () => resolveScrollableNode(scrollRef.current, isWindowScroll), +@@ -7184,6 +7283,22 @@ function createColumnWrapperStyle(contentContainerStyle) { + } + } + ++// src/core/isScrollExtentSynced.ts ++var SCROLL_EXTENT_EPSILON = 1; ++function isScrollExtentSynced(ctx) { ++ var _a3, _b; ++ const state = ctx.state; ++ const platformMaxOffset = (_b = (_a3 = state.refScroller.current) == null ? void 0 : _a3.getMaxScrollOffset) == null ? void 0 : _b.call(_a3); ++ if (platformMaxOffset === void 0) { ++ return true; ++ } ++ if (state.scrollLength <= 0) { ++ return true; ++ } ++ const contentMaxOffset = Math.max(0, getContentSize(ctx) - state.scrollLength); ++ return platformMaxOffset >= contentMaxOffset - SCROLL_EXTENT_EPSILON; ++} ++ + // src/core/scrollToEnd.ts + function scrollToEnd(ctx, options) { + const state = ctx.state; +@@ -7229,6 +7344,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { + const IMPERATIVE_SCROLL_SETTLE_STABLE_FRAMES = 2; + let imperativeScrollToken = 0; + const isSettlingAfterDataChange = () => !!state.didDataChange || !!state.didColumnsChange || state.scheduledWork.has("mvcpRecalculate") || state.ignoreScrollFromMVCP !== void 0; ++ const isScrollBlocked = () => isSettlingAfterDataChange() || !isScrollExtentSynced(ctx); + const isScrollToIndexReady = (targetIndex, allowEmpty = false) => { + var _a3; + const props = state.props; +@@ -7252,7 +7368,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { + if (token !== imperativeScrollToken) { + return; + } +- if (isSettlingAfterDataChange() || !isReady()) { ++ if (isScrollBlocked() || !isReady()) { + stableFrames = 0; + } else { + stableFrames += 1; +@@ -7279,7 +7395,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { + resolve(); + } + }; +- if (isSettlingAfterDataChange() || !isReady()) { ++ if (isScrollBlocked() || !isReady()) { + runWhenReady(token, runNow, isReady); + } else { + runNow(); +@@ -8267,6 +8383,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + onScrollBeginDrag: (event) => { + var _a4, _b2; + prepareReachedEdgeForNextUserScroll(ctx); ++ clearScrollTargetSettle(state); + (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); + }, + onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) +diff --git a/node_modules/@legendapp/list/react.d.ts b/node_modules/@legendapp/list/react.d.ts +index b6c7481..0ec3d1e 100644 +--- a/node_modules/@legendapp/list/react.d.ts ++++ b/node_modules/@legendapp/list/react.d.ts +@@ -6,6 +6,7 @@ type ScrollEventTarget = Window | HTMLElement; + interface ScrollViewMethods { + getBoundingClientRect(): DOMRect | null | undefined; + getCurrentScrollOffset(): number; ++ getMaxScrollOffset(): number; + getScrollableNode(): HTMLElement; + getScrollEventTarget(): ScrollEventTarget | null; + getScrollResponder(): HTMLElement | null; +diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js +index 914d2da..e11dd28 100644 +--- a/node_modules/@legendapp/list/react.js ++++ b/node_modules/@legendapp/list/react.js +@@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; + var ENABLE_DEVMODE = IS_DEV && false; + var ENABLE_DEBUG_VIEW = IS_DEV && false; + +-// src/core/cancelImperativeScroll.ts +-function cancelScrollCompletionChecks({ scheduledWork }) { +- scheduledWork.cancel("checkFinishedScrollFrame"); +- scheduledWork.cancel("checkFinishedScrollRetryFrame"); +- scheduledWork.cancel("checkFinishedScrollFallback"); +- scheduledWork.cancel("platformScrollCompletion"); +-} +-function settlePendingImperativeScroll(state) { +- var _a3, _b; +- const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; +- state.pendingScrollResolve = void 0; +- state.pendingScrollToEnd = void 0; +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +-} +-function cancelImperativeScroll(state) { +- cancelScrollCompletionChecks(state); +- state.scheduledWork.cancel("imperativeScrollReady"); +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- settlePendingImperativeScroll(state); +-} +- +-// src/core/deferredPublicOnScroll.ts +-function withResolvedContentOffset(state, event, resolvedOffset) { +- return { +- ...event, +- nativeEvent: { +- ...event.nativeEvent, +- contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } +- } +- }; +-} +-function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { +- var _a3, _b, _c, _d; +- const state = ctx.state; +- const deferredEvent = state.deferredPublicOnScrollEvent; +- state.deferredPublicOnScrollEvent = void 0; +- if (deferredEvent) { +- (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( +- _c, +- withResolvedContentOffset( +- state, +- deferredEvent, +- (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 +- ) +- ); +- } +-} +- +-// src/core/initialScrollSession.ts +-var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +-function hasInitialScrollSessionCompletion(completion) { +- return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); +-} +-function clearInitialScrollSession(state) { +- state.initialScrollSession = void 0; +- return void 0; +-} +-function createInitialScrollSession(options) { +- const { bootstrap, completion, kind, previousDataLength } = options; +- return kind === "offset" ? { +- completion, +- kind, +- previousDataLength +- } : { +- bootstrap, +- completion, +- kind, +- previousDataLength +- }; +-} +-function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { +- var _a4, _b2; +- if (!state.initialScrollSession) { +- state.initialScrollSession = createInitialScrollSession({ +- completion: {}, +- kind, +- previousDataLength: 0 +- }); +- } else if (state.initialScrollSession.kind !== kind) { +- state.initialScrollSession = createInitialScrollSession({ +- bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, +- completion: state.initialScrollSession.completion, +- kind, +- previousDataLength: state.initialScrollSession.previousDataLength +- }); +- } +- (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; +- return state.initialScrollSession.completion; +-} +-var initialScrollCompletion = { +- didDispatchNativeScroll(state) { +- var _a3, _b; +- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); +- }, +- didRetrySilentInitialScroll(state) { +- var _a3, _b; +- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); +- }, +- markInitialScrollNativeDispatch(state) { +- ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; +- }, +- markSilentInitialScrollRetry(state) { +- ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; +- }, +- resetFlags(state) { +- if (!state.initialScrollSession) { +- return; +- } +- const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); +- completion.didDispatchNativeScroll = void 0; +- completion.didRetrySilentInitialScroll = void 0; +- } +-}; +-var initialScrollWatchdog = { +- clear(state) { +- initialScrollWatchdog.set(state, void 0); +- }, +- didReachTarget(newScroll, watchdog) { +- const nextDistance = Math.abs(newScroll - watchdog.targetOffset); +- return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; +- }, +- get(state) { +- var _a3, _b; +- return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; +- }, +- hasNonZeroTargetOffset(targetOffset) { +- return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; +- }, +- isAtZeroTargetOffset(targetOffset) { +- return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; +- }, +- set(state, watchdog) { +- var _a3, _b; +- if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { +- return; +- } +- const completion = ensureInitialScrollSessionCompletion(state); +- completion.watchdog = watchdog ? { +- startScroll: watchdog.startScroll, +- targetOffset: watchdog.targetOffset +- } : void 0; +- } +-}; +-function setInitialScrollSession(state, options = {}) { +- var _a3, _b, _c, _d; +- const existingSession = state.initialScrollSession; +- const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; +- const completion = existingSession == null ? void 0 : existingSession.completion; +- const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; +- const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; +- if (!kind) { +- return clearInitialScrollSession(state); +- } +- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { +- return clearInitialScrollSession(state); +- } +- const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; +- state.initialScrollSession = createInitialScrollSession({ +- bootstrap, +- completion, +- kind, +- previousDataLength +- }); +- return state.initialScrollSession; +-} +- +-// src/utils/checkThreshold.ts +-var HYSTERESIS_MULTIPLIER = 1.3; +-function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { +- const absDistance = Math.abs(distance); +- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; +-} +-var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { +- const absDistance = Math.abs(distance); +- const within = atThreshold || threshold > 0 && absDistance <= threshold; +- const updateSnapshot = () => { +- setSnapshot({ +- atThreshold, +- contentSize: context.contentSize, +- dataLength: context.dataLength, +- scrollPosition: context.scrollPosition +- }); +- }; +- if (!wasReached) { +- if (!within) { +- return false; +- } +- onReached(distance); +- updateSnapshot(); +- return true; +- } +- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); +- if (reset) { +- setSnapshot(void 0); +- return false; +- } +- if (within) { +- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; +- if (changed) { +- updateSnapshot(); +- } +- } +- return true; +-}; +- +-// src/utils/edgeReachedGate.ts +-function resetEdgeLatch(ctx, edge) { +- const state = ctx.state; +- if (edge === "start") { +- state.isStartReached = false; +- state.startReachedSnapshot = void 0; +- } else { +- state.isEndReached = false; +- state.endReachedSnapshot = void 0; +- } +-} +-function resetSharedEdgeGateIfOutsideHysteresis(ctx) { +- const state = ctx.state; +- if (!state.edgeReachedGate) { +- return; +- } +- const contentSize = getContentSize(ctx); +- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); +- const isContentLess = contentSize < state.scrollLength; +- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; +- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; +- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); +- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); +- if (isOutsideStart && isOutsideEnd) { +- state.edgeReachedGate = void 0; +- } +-} +-function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { +- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; +-} +-function markReachedEdge(ctx) { +- ctx.state.edgeReachedGate = "closed"; +-} +-function prepareReachedEdgeForNextUserScroll(ctx) { +- if (ctx.state.edgeReachedGate) { +- ctx.state.edgeReachedGate = "prepared"; +- } +-} +-function beginReachedEdgeUserScroll(ctx, scrollDelta) { +- const state = ctx.state; +- if (state.edgeReachedGate !== "prepared") { +- return void 0; +- } +- const allowedEdge = scrollDelta < 0 ? "start" : "end"; +- state.edgeReachedGate = "closed"; +- resetEdgeLatch(ctx, allowedEdge); +- return allowedEdge; +-} +- +-// src/utils/hasActiveInitialScroll.ts +-function hasActiveInitialScroll(state) { +- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; +-} +- +-// src/utils/checkAtBottom.ts +-function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { +- var _a3; +- const state = ctx.state; +- if (!state) { +- return; +- } +- const { +- queuedInitialLayout, +- scrollLength, +- scroll, +- maintainingScrollAtEnd, +- props: { maintainScrollAtEndThreshold, onEndReachedThreshold } +- } = state; +- const contentSize = getContentSize(ctx); +- resetSharedEdgeGateIfOutsideHysteresis(ctx); +- if (contentSize > 0 && queuedInitialLayout) { +- const insetEnd = getContentInsetEnd(ctx); +- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; +- const isContentLess = contentSize < scrollLength; +- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); +- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); +- set$( +- ctx, +- "isWithinMaintainScrollAtEndThreshold", +- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength +- ); +- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; +- if (!shouldSkipThresholdChecks) { +- state.isEndReached = checkThreshold( +- distanceFromEnd, +- isContentLess, +- onEndReachedThreshold * scrollLength, +- state.isEndReached, +- state.endReachedSnapshot, +- { +- contentSize, +- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, +- scrollPosition: scroll +- }, +- (distance) => { +- var _a4, _b; +- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { +- markReachedEdge(ctx); +- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); +- } +- }, +- (snapshot) => { +- state.endReachedSnapshot = snapshot; +- } +- ); +- } +- } +-} +- +-// src/utils/checkAtTop.ts +-function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { +- const state = ctx == null ? void 0 : ctx.state; +- if (!state) { +- return; +- } +- const { +- isStartReached, +- props: { data, onStartReachedThreshold }, +- scroll, +- scrollLength, +- startReachedSnapshot, +- totalSize +- } = state; +- const dataLength = data.length; +- const threshold = onStartReachedThreshold * scrollLength; +- resetSharedEdgeGateIfOutsideHysteresis(ctx); +- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { +- state.isStartReached = false; +- state.startReachedSnapshot = void 0; +- } +- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); +- set$(ctx, "isNearStart", scroll <= threshold); +- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; +- if (!shouldSkipThresholdChecks) { +- state.isStartReached = checkThreshold( +- scroll, +- false, +- threshold, +- state.isStartReached, +- startReachedSnapshot, +- { +- contentSize: totalSize, +- dataLength, +- scrollPosition: scroll +- }, +- (distance) => { +- var _a3, _b; +- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { +- markReachedEdge(ctx); +- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); +- } +- }, +- (snapshot) => { +- state.startReachedSnapshot = snapshot; +- } +- ); +- } +-} +- +-// src/utils/checkThresholds.ts +-function checkThresholds(ctx, allowedEdge) { +- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; +- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); +- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); +-} +- +-// src/core/recalculateSettledScroll.ts +-function recalculateSettledScroll(ctx) { +- var _a3, _b; +- const state = ctx.state; +- if ((_a3 = state.props) == null ? void 0 : _a3.data) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); +- } +- checkThresholds(ctx); +-} +-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; +-function clearAdaptiveRenderExitTimeout(ctx) { +- ctx.state.scheduledWork.cancel("adaptiveRender"); +-} +-function scheduleAdaptiveRenderExit(ctx, exitDelay) { +- const state = ctx.state; +- clearAdaptiveRenderExitTimeout(ctx); +- if (exitDelay <= 0) { +- setAdaptiveRender(ctx, "normal", "scroll"); +- } else { +- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); +- } +-} +-function setAdaptiveRender(ctx, mode, reason) { +- var _a3, _b; +- const previousMode = peek$(ctx, "adaptiveRender"); +- if (previousMode !== mode) { +- set$(ctx, "adaptiveRender", mode); +- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); +- } +-} +-function resetAdaptiveRender(ctx) { +- var _a3, _b; +- clearAdaptiveRenderExitTimeout(ctx); +- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; +- if (peek$(ctx, "adaptiveRender") !== mode) { +- setAdaptiveRender(ctx, mode, "initial"); +- } +-} +-function updateAdaptiveRender(ctx, scrollVelocity, options) { +- var _a3, _b, _c; +- const state = ctx.state; +- const adaptiveRender = state.props.adaptiveRender; +- const currentMode = peek$(ctx, "adaptiveRender"); +- if (peek$(ctx, "readyToRender")) { +- if (adaptiveRender) { +- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; +- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; +- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; +- const threshold = currentMode === "light" ? exitVelocity : enterVelocity; +- const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; +- const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; +- if (nextMode !== previousMode) { +- if (nextMode === "light") { +- setAdaptiveRender(ctx, "light", "scroll"); +- scheduleAdaptiveRenderExit(ctx, exitDelay); +- } else if (currentMode === "light") { +- scheduleAdaptiveRenderExit(ctx, exitDelay); +- } +- } +- } else { +- resetAdaptiveRender(ctx); +- } +- } +-} +- +-// src/utils/getEffectiveDrawDistance.ts +-var INITIAL_DRAW_DISTANCE = 50; +-function getEffectiveDrawDistance(ctx, mode) { +- var _a3; +- const drawDistance = ctx.state.props.drawDistance; +- const initialScroll = ctx.state.initialScroll; +- const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; +- const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; +- return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; +-} +-function scheduleFullDrawDistancePrewarm(ctx) { +- const { state } = ctx; +- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { +- return; +- } +- state.scheduledWork.frame(() => { +- var _a3; +- return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +- }, "fullDrawDistancePrewarm"); +-} +- +-// src/utils/setInitialRenderState.ts +-function resetInitialRenderState(ctx, { +- resetLayout, +- resetInitialScroll +-}) { +- const { state } = ctx; +- if (resetLayout) { +- state.didContainersLayout = false; +- state.queuedInitialLayout = false; +- } +- if (resetInitialScroll) { +- state.didFinishInitialScroll = false; +- } +- set$(ctx, "readyToRender", false); +- resetAdaptiveRender(ctx); +-} +-function setInitialRenderState(ctx, { +- didLayout, +- didInitialScroll +-}) { +- const { state } = ctx; +- const { +- loadStartTime, +- props: { onLoad } +- } = state; +- if (didLayout) { +- state.didContainersLayout = true; +- } +- if (didInitialScroll) { +- state.didFinishInitialScroll = true; +- } +- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); +- if (isReadyToRender && !peek$(ctx, "readyToRender")) { +- set$(ctx, "readyToRender", true); +- setAdaptiveRender(ctx, "normal", "ready"); +- if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { +- scheduleFullDrawDistancePrewarm(ctx); +- } +- if (!state.didLoad) { +- state.didLoad = true; +- if (onLoad) { +- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); +- } +- } +- } +-} +- +-// src/core/finishInitialScroll.ts +-var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; +-function syncInitialScrollOffset(state, offset) { +- state.scroll = offset; +- state.scrollPending = offset; +- state.scrollPrev = offset; +-} +-function clearPreservedInitialScrollTargetTimeout(state) { +- state.scheduledWork.cancel("preservedInitialScroll"); +-} +-function clearPreservedInitialScrollTarget(state) { +- clearPreservedInitialScrollTargetTimeout(state); +- state.clearPreservedInitialScrollOnNextFinish = void 0; +- state.initialScroll = void 0; +- setInitialScrollSession(state); +-} +-function supersedeInitialScroll(ctx) { +- var _a3, _b, _c; +- const state = ctx.state; +- const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; +- if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { +- if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { +- cancelAnimationFrame(bootstrapInitialScroll.frameHandle); +- } +- if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { +- cancelScrollCompletionChecks(state); +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- } +- initialScrollCompletion.resetFlags(state); +- setInitialScrollSession(state, { bootstrap: null }); +- finishInitialScroll(ctx); +- } +-} +-function finishInitialScroll(ctx, options) { +- var _a3, _b, _c; +- const state = ctx.state; +- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { +- syncInitialScrollOffset(state, options.resolvedOffset); +- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { +- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); +- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { +- syncInitialScrollOffset(state, observedOffset); +- } +- } +- const complete = () => { +- var _a4, _b2, _c2, _d, _e; +- const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; +- const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0; +- initialScrollWatchdog.clear(state); +- if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) { +- state.clearPreservedInitialScrollOnNextFinish = void 0; +- setInitialScrollSession(state); +- clearPreservedInitialScrollTargetTimeout(state); +- if (options == null ? void 0 : options.schedulePreservedTargetClear) { +- state.scheduledWork.timeout( +- () => { +- var _a5; +- if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) { +- return; +- } +- clearPreservedInitialScrollTarget(state); +- }, +- PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, +- "preservedInitialScroll" +- ); +- } +- } else { +- clearPreservedInitialScrollTarget(state); +- } +- if (options == null ? void 0 : options.recalculateItems) { +- recalculateSettledScroll(ctx); +- } +- setInitialRenderState(ctx, { didInitialScroll: true }); +- if (shouldReleaseDeferredPublicOnScroll) { +- releaseDeferredPublicOnScroll(ctx, finalScrollOffset); +- } +- (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); +- }; +- if (options == null ? void 0 : options.waitForCompletionFrame) { +- requestAnimationFrame(complete); +- return; +- } +- complete(); +-} +- +-// src/core/calculateOffsetForIndex.ts +-function calculateOffsetForIndex(ctx, index) { +- const state = ctx.state; +- return index !== void 0 ? state.positions[index] || 0 : 0; +-} +- + // src/core/getStartOffsetAdjustment.ts + function getStartOffsetAdjustment(ctx) { + const { state } = ctx; +@@ -1234,58 +635,273 @@ function getItemSizeAtIndex(ctx, index) { + if (index === void 0 || index < 0) { + return void 0; + } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; ++ } ++ if (index !== void 0) { ++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); ++ if (startOffsetAdjustment) { ++ offset += startOffsetAdjustment; ++ } ++ } ++ if (viewPosition !== void 0 && index !== void 0) { ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return offset; ++ } ++ const isOutOfBounds = index < 0 || index >= dataLength; ++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; ++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); ++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); ++ const trailingInset = getContentInsetEnd(ctx); ++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); ++ if (!isOutOfBounds && index === state.props.data.length - 1) { ++ const footerSize = peek$(ctx, "footerSize") || 0; ++ offset += footerSize; ++ } ++ } ++ return offset; ++} ++ ++// src/core/clampScrollOffset.ts ++function clampScrollOffset(ctx, offset, scrollTarget) { ++ const state = ctx.state; ++ const contentSize = getContentSize(ctx); ++ let clampedOffset = offset; ++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { ++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); ++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; ++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; ++ const maxOffset = baseMaxOffset + extraEndOffset; ++ clampedOffset = Math.min(offset, maxOffset); ++ } ++ clampedOffset = Math.max(0, clampedOffset); ++ return clampedOffset; ++} ++ ++// src/utils/checkThreshold.ts ++var HYSTERESIS_MULTIPLIER = 1.3; ++function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { ++ const absDistance = Math.abs(distance); ++ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; ++} ++var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { ++ const absDistance = Math.abs(distance); ++ const within = atThreshold || threshold > 0 && absDistance <= threshold; ++ const updateSnapshot = () => { ++ setSnapshot({ ++ atThreshold, ++ contentSize: context.contentSize, ++ dataLength: context.dataLength, ++ scrollPosition: context.scrollPosition ++ }); ++ }; ++ if (!wasReached) { ++ if (!within) { ++ return false; ++ } ++ onReached(distance); ++ updateSnapshot(); ++ return true; ++ } ++ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); ++ if (reset) { ++ setSnapshot(void 0); ++ return false; ++ } ++ if (within) { ++ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; ++ if (changed) { ++ updateSnapshot(); ++ } ++ } ++ return true; ++}; ++ ++// src/utils/edgeReachedGate.ts ++function resetEdgeLatch(ctx, edge) { ++ const state = ctx.state; ++ if (edge === "start") { ++ state.isStartReached = false; ++ state.startReachedSnapshot = void 0; ++ } else { ++ state.isEndReached = false; ++ state.endReachedSnapshot = void 0; ++ } ++} ++function resetSharedEdgeGateIfOutsideHysteresis(ctx) { ++ const state = ctx.state; ++ if (!state.edgeReachedGate) { ++ return; ++ } ++ const contentSize = getContentSize(ctx); ++ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); ++ const isContentLess = contentSize < state.scrollLength; ++ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; ++ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; ++ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); ++ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); ++ if (isOutsideStart && isOutsideEnd) { ++ state.edgeReachedGate = void 0; ++ } ++} ++function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { ++ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; ++} ++function markReachedEdge(ctx) { ++ ctx.state.edgeReachedGate = "closed"; ++} ++function prepareReachedEdgeForNextUserScroll(ctx) { ++ if (ctx.state.edgeReachedGate) { ++ ctx.state.edgeReachedGate = "prepared"; ++ } ++} ++function beginReachedEdgeUserScroll(ctx, scrollDelta) { ++ const state = ctx.state; ++ if (state.edgeReachedGate !== "prepared") { ++ return void 0; ++ } ++ const allowedEdge = scrollDelta < 0 ? "start" : "end"; ++ state.edgeReachedGate = "closed"; ++ resetEdgeLatch(ctx, allowedEdge); ++ return allowedEdge; ++} ++ ++// src/utils/hasActiveInitialScroll.ts ++function hasActiveInitialScroll(state) { ++ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; ++} ++ ++// src/utils/checkAtBottom.ts ++function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { ++ var _a3; ++ const state = ctx.state; ++ if (!state) { ++ return; ++ } ++ const { ++ queuedInitialLayout, ++ scrollLength, ++ scroll, ++ maintainingScrollAtEnd, ++ props: { maintainScrollAtEndThreshold, onEndReachedThreshold } ++ } = state; ++ const contentSize = getContentSize(ctx); ++ resetSharedEdgeGateIfOutsideHysteresis(ctx); ++ if (contentSize > 0 && queuedInitialLayout) { ++ const insetEnd = getContentInsetEnd(ctx); ++ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; ++ const isContentLess = contentSize < scrollLength; ++ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); ++ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); ++ set$( ++ ctx, ++ "isWithinMaintainScrollAtEndThreshold", ++ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength ++ ); ++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; ++ if (!shouldSkipThresholdChecks) { ++ state.isEndReached = checkThreshold( ++ distanceFromEnd, ++ isContentLess, ++ onEndReachedThreshold * scrollLength, ++ state.isEndReached, ++ state.endReachedSnapshot, ++ { ++ contentSize, ++ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, ++ scrollPosition: scroll ++ }, ++ (distance) => { ++ var _a4, _b; ++ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { ++ markReachedEdge(ctx); ++ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); ++ } ++ }, ++ (snapshot) => { ++ state.endReachedSnapshot = snapshot; ++ } ++ ); ++ } ++ } ++} ++ ++// src/utils/checkAtTop.ts ++function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { ++ const state = ctx == null ? void 0 : ctx.state; ++ if (!state) { ++ return; ++ } ++ const { ++ isStartReached, ++ props: { data, onStartReachedThreshold }, ++ scroll, ++ scrollLength, ++ startReachedSnapshot, ++ totalSize ++ } = state; ++ const dataLength = data.length; ++ const threshold = onStartReachedThreshold * scrollLength; ++ resetSharedEdgeGateIfOutsideHysteresis(ctx); ++ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { ++ state.isStartReached = false; ++ state.startReachedSnapshot = void 0; ++ } ++ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); ++ set$(ctx, "isNearStart", scroll <= threshold); ++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; ++ if (!shouldSkipThresholdChecks) { ++ state.isStartReached = checkThreshold( ++ scroll, ++ false, ++ threshold, ++ state.isStartReached, ++ startReachedSnapshot, ++ { ++ contentSize: totalSize, ++ dataLength, ++ scrollPosition: scroll ++ }, ++ (distance) => { ++ var _a3, _b; ++ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { ++ markReachedEdge(ctx); ++ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); ++ } ++ }, ++ (snapshot) => { ++ state.startReachedSnapshot = snapshot; ++ } ++ ); ++ } + } + +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +- } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } +- } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } +- } +- return offset; ++// src/utils/checkThresholds.ts ++function checkThresholds(ctx, allowedEdge) { ++ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; ++ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); ++ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + } + +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { ++// src/core/recalculateSettledScroll.ts ++function recalculateSettledScroll(ctx) { ++ var _a3, _b; + const state = ctx.state; +- const contentSize = getContentSize(ctx); +- let clampedOffset = offset; +- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { +- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); +- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; +- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; +- const maxOffset = baseMaxOffset + extraEndOffset; +- clampedOffset = Math.min(offset, maxOffset); ++ if ((_a3 = state.props) == null ? void 0 : _a3.data) { ++ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); + } +- clampedOffset = Math.max(0, clampedOffset); +- return clampedOffset; ++ checkThresholds(ctx); + } + + // src/core/finishScrollTo.ts +@@ -1422,7 +1038,182 @@ function listenForScrollEnd(ctx, params) { + } else { + idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); + } +- scheduledWork.register("platformScrollCompletion", cancel); ++ scheduledWork.register("platformScrollCompletion", cancel); ++} ++ ++// src/core/initialScrollSession.ts ++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; ++function hasInitialScrollSessionCompletion(completion) { ++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); ++} ++function clearInitialScrollSession(state) { ++ state.initialScrollSession = void 0; ++ return void 0; ++} ++function createInitialScrollSession(options) { ++ const { bootstrap, completion, kind, previousDataLength } = options; ++ return kind === "offset" ? { ++ completion, ++ kind, ++ previousDataLength ++ } : { ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }; ++} ++function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { ++ var _a4, _b2; ++ if (!state.initialScrollSession) { ++ state.initialScrollSession = createInitialScrollSession({ ++ completion: {}, ++ kind, ++ previousDataLength: 0 ++ }); ++ } else if (state.initialScrollSession.kind !== kind) { ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, ++ completion: state.initialScrollSession.completion, ++ kind, ++ previousDataLength: state.initialScrollSession.previousDataLength ++ }); ++ } ++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; ++ return state.initialScrollSession.completion; ++} ++var initialScrollCompletion = { ++ didDispatchNativeScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); ++ }, ++ didRetrySilentInitialScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); ++ }, ++ markInitialScrollNativeDispatch(state) { ++ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; ++ }, ++ markSilentInitialScrollRetry(state) { ++ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; ++ }, ++ resetFlags(state) { ++ if (!state.initialScrollSession) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); ++ completion.didDispatchNativeScroll = void 0; ++ completion.didRetrySilentInitialScroll = void 0; ++ } ++}; ++var initialScrollWatchdog = { ++ clear(state) { ++ initialScrollWatchdog.set(state, void 0); ++ }, ++ didReachTarget(newScroll, watchdog) { ++ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); ++ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ get(state) { ++ var _a3, _b; ++ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; ++ }, ++ hasNonZeroTargetOffset(targetOffset) { ++ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ isAtZeroTargetOffset(targetOffset) { ++ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ set(state, watchdog) { ++ var _a3, _b; ++ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state); ++ completion.watchdog = watchdog ? { ++ startScroll: watchdog.startScroll, ++ targetOffset: watchdog.targetOffset ++ } : void 0; ++ } ++}; ++function setInitialScrollSession(state, options = {}) { ++ var _a3, _b, _c, _d; ++ const existingSession = state.initialScrollSession; ++ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; ++ const completion = existingSession == null ? void 0 : existingSession.completion; ++ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; ++ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; ++ if (!kind) { ++ return clearInitialScrollSession(state); ++ } ++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { ++ return clearInitialScrollSession(state); ++ } ++ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }); ++ return state.initialScrollSession; ++} ++var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; ++function clearAdaptiveRenderExitTimeout(ctx) { ++ ctx.state.scheduledWork.cancel("adaptiveRender"); ++} ++function scheduleAdaptiveRenderExit(ctx, exitDelay) { ++ const state = ctx.state; ++ clearAdaptiveRenderExitTimeout(ctx); ++ if (exitDelay <= 0) { ++ setAdaptiveRender(ctx, "normal", "scroll"); ++ } else { ++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); ++ } ++} ++function setAdaptiveRender(ctx, mode, reason) { ++ var _a3, _b; ++ const previousMode = peek$(ctx, "adaptiveRender"); ++ if (previousMode !== mode) { ++ set$(ctx, "adaptiveRender", mode); ++ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); ++ } ++} ++function resetAdaptiveRender(ctx) { ++ var _a3, _b; ++ clearAdaptiveRenderExitTimeout(ctx); ++ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; ++ if (peek$(ctx, "adaptiveRender") !== mode) { ++ setAdaptiveRender(ctx, mode, "initial"); ++ } ++} ++function updateAdaptiveRender(ctx, scrollVelocity, options) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ const adaptiveRender = state.props.adaptiveRender; ++ const currentMode = peek$(ctx, "adaptiveRender"); ++ if (peek$(ctx, "readyToRender")) { ++ if (adaptiveRender) { ++ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; ++ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; ++ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; ++ const threshold = currentMode === "light" ? exitVelocity : enterVelocity; ++ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; ++ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; ++ if (nextMode !== previousMode) { ++ if (nextMode === "light") { ++ setAdaptiveRender(ctx, "light", "scroll"); ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } else if (currentMode === "light") { ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } ++ } ++ } else { ++ resetAdaptiveRender(ctx); ++ } ++ } + } + + // src/core/doMaintainScrollAtEnd.ts +@@ -1809,6 +1600,27 @@ function prepareMVCP(ctx, dataChanged) { + } + } + ++// src/utils/getEffectiveDrawDistance.ts ++var INITIAL_DRAW_DISTANCE = 50; ++function getEffectiveDrawDistance(ctx, mode) { ++ var _a3; ++ const drawDistance = ctx.state.props.drawDistance; ++ const initialScroll = ctx.state.initialScroll; ++ const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; ++ const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; ++ return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; ++} ++function scheduleFullDrawDistancePrewarm(ctx) { ++ const { state } = ctx; ++ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { ++ return; ++ } ++ state.scheduledWork.frame(() => { ++ var _a3; ++ return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); ++ }, "fullDrawDistancePrewarm"); ++} ++ + // src/utils/getScrollVelocity.ts + var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; + var SCROLL_VELOCITY_HALF_LIFE_MS = 200; +@@ -2069,7 +1881,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + } + } + function scrollTo(ctx, params) { +- var _a3, _b; ++ var _a3, _b, _c; + const state = ctx.state; + const { noScrollingTo, forceScroll, ...scrollTarget } = params; + const { +@@ -2101,6 +1913,15 @@ function scrollTo(ctx, params) { + }; + if (!isInitialScroll) { + pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); ++ if (scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { ++ beginScrollTargetSettle(ctx, { ++ index: scrollTarget.index, ++ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, ++ viewPosition: scrollTarget.viewPosition ++ }); ++ } else { ++ clearScrollTargetSettle(state); ++ } + } + } + state.scrollPending = targetOffset; +@@ -2108,7 +1929,7 @@ function scrollTo(ctx, params) { + if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { + if (animated) { + if (state.scrollTargetPinnedRange) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); + } + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); +@@ -2121,6 +1942,280 @@ function scrollTo(ctx, params) { + } + } + ++// src/core/scrollTargetSettle.ts ++var SETTLE_POSITION_EPSILON = 0.5; ++var SETTLE_QUIET_PASSES_TO_RELEASE = 2; ++var SETTLE_TTL_MS = 500; ++var SETTLE_MAX_MS = 1e3; ++var SETTLE_MAX_CORRECTIONS = 8; ++function clearScrollTargetSettle(state) { ++ state.scrollTargetSettle = void 0; ++} ++function beginScrollTargetSettle(ctx, params) { ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ const { data } = state.props; ++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; ++ if (index < 0 || index >= data.length || isEndAlignedLastItem) { ++ clearScrollTargetSettle(state); ++ return; ++ } ++ const now = Date.now(); ++ const id = getId(state, index); ++ const existing = state.scrollTargetSettle; ++ const isSameTarget = (existing == null ? void 0 : existing.id) === id; ++ state.scrollTargetSettle = { ++ corrections: isSameTarget ? existing.corrections : 0, ++ deadline: isSameTarget ? existing.deadline : now + SETTLE_MAX_MS, ++ expiresAt: now + SETTLE_TTL_MS, ++ id, ++ quietPasses: 0, ++ viewOffset, ++ viewPosition ++ }; ++} ++function getSettleTargetOffset(ctx, settle, index, position) { ++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; ++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); ++} ++function settleScrollTarget(ctx, isCompensating) { ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ return false; ++ } ++ if (isCompensating) { ++ settle.quietPasses = 0; ++ settle.expiresAt = Date.now() + SETTLE_TTL_MS; ++ return false; ++ } ++ const index = state.indexByKey.get(settle.id); ++ const position = index === void 0 ? void 0 : state.positions[index]; ++ if (index === void 0 || position === void 0) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ const now = Date.now(); ++ if (now > settle.expiresAt || now > settle.deadline) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position); ++ const diff = targetOffset - state.scroll; ++ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ settle.quietPasses++; ++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { ++ clearScrollTargetSettle(state); ++ } ++ return false; ++ } ++ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ settle.quietPasses = 0; ++ settle.corrections++; ++ settle.expiresAt = now + SETTLE_TTL_MS; ++ scrollTo(ctx, { ++ animated: false, ++ index, ++ itemSize: getItemSizeAtIndex(ctx, index), ++ offset: position, ++ viewOffset: settle.viewOffset, ++ viewPosition: settle.viewPosition ++ }); ++ return true; ++} ++ ++// src/core/cancelImperativeScroll.ts ++function cancelScrollCompletionChecks({ scheduledWork }) { ++ scheduledWork.cancel("checkFinishedScrollFrame"); ++ scheduledWork.cancel("checkFinishedScrollRetryFrame"); ++ scheduledWork.cancel("checkFinishedScrollFallback"); ++ scheduledWork.cancel("platformScrollCompletion"); ++} ++function settlePendingImperativeScroll(state) { ++ var _a3, _b; ++ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; ++ state.pendingScrollResolve = void 0; ++ state.pendingScrollToEnd = void 0; ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); ++} ++function cancelImperativeScroll(state) { ++ cancelScrollCompletionChecks(state); ++ state.scheduledWork.cancel("imperativeScrollReady"); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ clearScrollTargetSettle(state); ++ settlePendingImperativeScroll(state); ++} ++ ++// src/core/deferredPublicOnScroll.ts ++function withResolvedContentOffset(state, event, resolvedOffset) { ++ return { ++ ...event, ++ nativeEvent: { ++ ...event.nativeEvent, ++ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } ++ } ++ }; ++} ++function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { ++ var _a3, _b, _c, _d; ++ const state = ctx.state; ++ const deferredEvent = state.deferredPublicOnScrollEvent; ++ state.deferredPublicOnScrollEvent = void 0; ++ if (deferredEvent) { ++ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( ++ _c, ++ withResolvedContentOffset( ++ state, ++ deferredEvent, ++ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 ++ ) ++ ); ++ } ++} ++ ++// src/utils/setInitialRenderState.ts ++function resetInitialRenderState(ctx, { ++ resetLayout, ++ resetInitialScroll ++}) { ++ const { state } = ctx; ++ if (resetLayout) { ++ state.didContainersLayout = false; ++ state.queuedInitialLayout = false; ++ } ++ if (resetInitialScroll) { ++ state.didFinishInitialScroll = false; ++ } ++ set$(ctx, "readyToRender", false); ++ resetAdaptiveRender(ctx); ++} ++function setInitialRenderState(ctx, { ++ didLayout, ++ didInitialScroll ++}) { ++ const { state } = ctx; ++ const { ++ loadStartTime, ++ props: { onLoad } ++ } = state; ++ if (didLayout) { ++ state.didContainersLayout = true; ++ } ++ if (didInitialScroll) { ++ state.didFinishInitialScroll = true; ++ } ++ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); ++ if (isReadyToRender && !peek$(ctx, "readyToRender")) { ++ set$(ctx, "readyToRender", true); ++ setAdaptiveRender(ctx, "normal", "ready"); ++ if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { ++ scheduleFullDrawDistancePrewarm(ctx); ++ } ++ if (!state.didLoad) { ++ state.didLoad = true; ++ if (onLoad) { ++ onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); ++ } ++ } ++ } ++} ++ ++// src/core/finishInitialScroll.ts ++var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; ++function syncInitialScrollOffset(state, offset) { ++ state.scroll = offset; ++ state.scrollPending = offset; ++ state.scrollPrev = offset; ++} ++function clearPreservedInitialScrollTargetTimeout(state) { ++ state.scheduledWork.cancel("preservedInitialScroll"); ++} ++function clearPreservedInitialScrollTarget(state) { ++ clearPreservedInitialScrollTargetTimeout(state); ++ state.clearPreservedInitialScrollOnNextFinish = void 0; ++ state.initialScroll = void 0; ++ setInitialScrollSession(state); ++} ++function supersedeInitialScroll(ctx) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; ++ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { ++ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { ++ cancelAnimationFrame(bootstrapInitialScroll.frameHandle); ++ } ++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { ++ cancelScrollCompletionChecks(state); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ } ++ initialScrollCompletion.resetFlags(state); ++ setInitialScrollSession(state, { bootstrap: null }); ++ finishInitialScroll(ctx); ++ } ++} ++function finishInitialScroll(ctx, options) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { ++ syncInitialScrollOffset(state, options.resolvedOffset); ++ } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { ++ const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); ++ if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { ++ syncInitialScrollOffset(state, observedOffset); ++ } ++ } ++ const complete = () => { ++ var _a4, _b2, _c2, _d, _e; ++ const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; ++ const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0; ++ initialScrollWatchdog.clear(state); ++ if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) { ++ state.clearPreservedInitialScrollOnNextFinish = void 0; ++ setInitialScrollSession(state); ++ clearPreservedInitialScrollTargetTimeout(state); ++ if (options == null ? void 0 : options.schedulePreservedTargetClear) { ++ state.scheduledWork.timeout( ++ () => { ++ var _a5; ++ if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) { ++ return; ++ } ++ clearPreservedInitialScrollTarget(state); ++ }, ++ PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, ++ "preservedInitialScroll" ++ ); ++ } ++ } else { ++ clearPreservedInitialScrollTarget(state); ++ } ++ if (options == null ? void 0 : options.recalculateItems) { ++ recalculateSettledScroll(ctx); ++ } ++ setInitialRenderState(ctx, { didInitialScroll: true }); ++ if (shouldReleaseDeferredPublicOnScroll) { ++ releaseDeferredPublicOnScroll(ctx, finalScrollOffset); ++ } ++ (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); ++ }; ++ if (options == null ? void 0 : options.waitForCompletionFrame) { ++ requestAnimationFrame(complete); ++ return; ++ } ++ complete(); ++} ++ ++// src/core/calculateOffsetForIndex.ts ++function calculateOffsetForIndex(ctx, index) { ++ const state = ctx.state; ++ return index !== void 0 ? state.positions[index] || 0 : 0; ++} ++ + // src/core/scrollToIndex.ts + function clampScrollIndex(index, dataLength) { + if (dataLength <= 0) { +@@ -4367,7 +4462,10 @@ function calculateItemsInView(ctx, params = {}) { + const scrollBeforeMVCP = state.scroll; + const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; + checkMVCP == null ? void 0 : checkMVCP(); +- const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); ++ const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; ++ const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); ++ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx, didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0); ++ const didMVCPAdjustScroll = didMVCPAdjust || didSettleScrollTarget; + if (didMVCPAdjustScroll) { + updateScroll2(state.scroll); + updateScrollRange(); +@@ -6132,6 +6230,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + return (_a4 = scrollRef.current) == null ? void 0 : _a4.getBoundingClientRect(); + }, + getCurrentScrollOffset, ++ getMaxScrollOffset, + getScrollableNode: () => resolveScrollableNode(scrollRef.current, isWindowScroll), + getScrollEventTarget: () => getScrollTarget(), + getScrollResponder: () => resolveScrollableNode(scrollRef.current, isWindowScroll), +@@ -7205,6 +7304,22 @@ function createColumnWrapperStyle(contentContainerStyle) { + } + } + ++// src/core/isScrollExtentSynced.ts ++var SCROLL_EXTENT_EPSILON = 1; ++function isScrollExtentSynced(ctx) { ++ var _a3, _b; ++ const state = ctx.state; ++ const platformMaxOffset = (_b = (_a3 = state.refScroller.current) == null ? void 0 : _a3.getMaxScrollOffset) == null ? void 0 : _b.call(_a3); ++ if (platformMaxOffset === void 0) { ++ return true; ++ } ++ if (state.scrollLength <= 0) { ++ return true; ++ } ++ const contentMaxOffset = Math.max(0, getContentSize(ctx) - state.scrollLength); ++ return platformMaxOffset >= contentMaxOffset - SCROLL_EXTENT_EPSILON; ++} ++ + // src/core/scrollToEnd.ts + function scrollToEnd(ctx, options) { + const state = ctx.state; +@@ -7250,6 +7365,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { + const IMPERATIVE_SCROLL_SETTLE_STABLE_FRAMES = 2; + let imperativeScrollToken = 0; + const isSettlingAfterDataChange = () => !!state.didDataChange || !!state.didColumnsChange || state.scheduledWork.has("mvcpRecalculate") || state.ignoreScrollFromMVCP !== void 0; ++ const isScrollBlocked = () => isSettlingAfterDataChange() || !isScrollExtentSynced(ctx); + const isScrollToIndexReady = (targetIndex, allowEmpty = false) => { + var _a3; + const props = state.props; +@@ -7273,7 +7389,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { + if (token !== imperativeScrollToken) { + return; + } +- if (isSettlingAfterDataChange() || !isReady()) { ++ if (isScrollBlocked() || !isReady()) { + stableFrames = 0; + } else { + stableFrames += 1; +@@ -7300,7 +7416,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { + resolve(); + } + }; +- if (isSettlingAfterDataChange() || !isReady()) { ++ if (isScrollBlocked() || !isReady()) { + runWhenReady(token, runNow, isReady); + } else { + runNow(); +@@ -8288,6 +8404,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + onScrollBeginDrag: (event) => { + var _a4, _b2; + prepareReachedEdgeForNextUserScroll(ctx); ++ clearScrollTargetSettle(state); + (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); + }, + onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) +diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs +index 95465f2..a3e870c 100644 +--- a/node_modules/@legendapp/list/react.mjs ++++ b/node_modules/@legendapp/list/react.mjs +@@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; + var ENABLE_DEVMODE = IS_DEV && false; + var ENABLE_DEBUG_VIEW = IS_DEV && false; + +-// src/core/cancelImperativeScroll.ts +-function cancelScrollCompletionChecks({ scheduledWork }) { +- scheduledWork.cancel("checkFinishedScrollFrame"); +- scheduledWork.cancel("checkFinishedScrollRetryFrame"); +- scheduledWork.cancel("checkFinishedScrollFallback"); +- scheduledWork.cancel("platformScrollCompletion"); +-} +-function settlePendingImperativeScroll(state) { +- var _a3, _b; +- const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; +- state.pendingScrollResolve = void 0; +- state.pendingScrollToEnd = void 0; +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +-} +-function cancelImperativeScroll(state) { +- cancelScrollCompletionChecks(state); +- state.scheduledWork.cancel("imperativeScrollReady"); +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- settlePendingImperativeScroll(state); +-} +- +-// src/core/deferredPublicOnScroll.ts +-function withResolvedContentOffset(state, event, resolvedOffset) { +- return { +- ...event, +- nativeEvent: { +- ...event.nativeEvent, +- contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } +- } +- }; +-} +-function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { +- var _a3, _b, _c, _d; +- const state = ctx.state; +- const deferredEvent = state.deferredPublicOnScrollEvent; +- state.deferredPublicOnScrollEvent = void 0; +- if (deferredEvent) { +- (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( +- _c, +- withResolvedContentOffset( +- state, +- deferredEvent, +- (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 +- ) +- ); +- } +-} +- +-// src/core/initialScrollSession.ts +-var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +-function hasInitialScrollSessionCompletion(completion) { +- return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); +-} +-function clearInitialScrollSession(state) { +- state.initialScrollSession = void 0; +- return void 0; +-} +-function createInitialScrollSession(options) { +- const { bootstrap, completion, kind, previousDataLength } = options; +- return kind === "offset" ? { +- completion, +- kind, +- previousDataLength +- } : { +- bootstrap, +- completion, +- kind, +- previousDataLength +- }; +-} +-function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { +- var _a4, _b2; +- if (!state.initialScrollSession) { +- state.initialScrollSession = createInitialScrollSession({ +- completion: {}, +- kind, +- previousDataLength: 0 +- }); +- } else if (state.initialScrollSession.kind !== kind) { +- state.initialScrollSession = createInitialScrollSession({ +- bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, +- completion: state.initialScrollSession.completion, +- kind, +- previousDataLength: state.initialScrollSession.previousDataLength +- }); +- } +- (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; +- return state.initialScrollSession.completion; +-} +-var initialScrollCompletion = { +- didDispatchNativeScroll(state) { +- var _a3, _b; +- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); +- }, +- didRetrySilentInitialScroll(state) { +- var _a3, _b; +- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); +- }, +- markInitialScrollNativeDispatch(state) { +- ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; +- }, +- markSilentInitialScrollRetry(state) { +- ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; +- }, +- resetFlags(state) { +- if (!state.initialScrollSession) { +- return; +- } +- const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); +- completion.didDispatchNativeScroll = void 0; +- completion.didRetrySilentInitialScroll = void 0; +- } +-}; +-var initialScrollWatchdog = { +- clear(state) { +- initialScrollWatchdog.set(state, void 0); +- }, +- didReachTarget(newScroll, watchdog) { +- const nextDistance = Math.abs(newScroll - watchdog.targetOffset); +- return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; +- }, +- get(state) { +- var _a3, _b; +- return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; +- }, +- hasNonZeroTargetOffset(targetOffset) { +- return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; +- }, +- isAtZeroTargetOffset(targetOffset) { +- return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; +- }, +- set(state, watchdog) { +- var _a3, _b; +- if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { +- return; +- } +- const completion = ensureInitialScrollSessionCompletion(state); +- completion.watchdog = watchdog ? { +- startScroll: watchdog.startScroll, +- targetOffset: watchdog.targetOffset +- } : void 0; +- } +-}; +-function setInitialScrollSession(state, options = {}) { +- var _a3, _b, _c, _d; +- const existingSession = state.initialScrollSession; +- const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; +- const completion = existingSession == null ? void 0 : existingSession.completion; +- const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; +- const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; +- if (!kind) { +- return clearInitialScrollSession(state); +- } +- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { +- return clearInitialScrollSession(state); +- } +- const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; +- state.initialScrollSession = createInitialScrollSession({ +- bootstrap, +- completion, +- kind, +- previousDataLength +- }); +- return state.initialScrollSession; +-} +- +-// src/utils/checkThreshold.ts +-var HYSTERESIS_MULTIPLIER = 1.3; +-function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { +- const absDistance = Math.abs(distance); +- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; +-} +-var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { +- const absDistance = Math.abs(distance); +- const within = atThreshold || threshold > 0 && absDistance <= threshold; +- const updateSnapshot = () => { +- setSnapshot({ +- atThreshold, +- contentSize: context.contentSize, +- dataLength: context.dataLength, +- scrollPosition: context.scrollPosition +- }); +- }; +- if (!wasReached) { +- if (!within) { +- return false; +- } +- onReached(distance); +- updateSnapshot(); +- return true; +- } +- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); +- if (reset) { +- setSnapshot(void 0); +- return false; +- } +- if (within) { +- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; +- if (changed) { +- updateSnapshot(); +- } +- } +- return true; +-}; +- +-// src/utils/edgeReachedGate.ts +-function resetEdgeLatch(ctx, edge) { +- const state = ctx.state; +- if (edge === "start") { +- state.isStartReached = false; +- state.startReachedSnapshot = void 0; +- } else { +- state.isEndReached = false; +- state.endReachedSnapshot = void 0; +- } +-} +-function resetSharedEdgeGateIfOutsideHysteresis(ctx) { +- const state = ctx.state; +- if (!state.edgeReachedGate) { +- return; +- } +- const contentSize = getContentSize(ctx); +- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); +- const isContentLess = contentSize < state.scrollLength; +- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; +- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; +- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); +- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); +- if (isOutsideStart && isOutsideEnd) { +- state.edgeReachedGate = void 0; +- } +-} +-function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { +- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; +-} +-function markReachedEdge(ctx) { +- ctx.state.edgeReachedGate = "closed"; +-} +-function prepareReachedEdgeForNextUserScroll(ctx) { +- if (ctx.state.edgeReachedGate) { +- ctx.state.edgeReachedGate = "prepared"; +- } +-} +-function beginReachedEdgeUserScroll(ctx, scrollDelta) { +- const state = ctx.state; +- if (state.edgeReachedGate !== "prepared") { +- return void 0; +- } +- const allowedEdge = scrollDelta < 0 ? "start" : "end"; +- state.edgeReachedGate = "closed"; +- resetEdgeLatch(ctx, allowedEdge); +- return allowedEdge; +-} +- +-// src/utils/hasActiveInitialScroll.ts +-function hasActiveInitialScroll(state) { +- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; +-} +- +-// src/utils/checkAtBottom.ts +-function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { +- var _a3; +- const state = ctx.state; +- if (!state) { +- return; +- } +- const { +- queuedInitialLayout, +- scrollLength, +- scroll, +- maintainingScrollAtEnd, +- props: { maintainScrollAtEndThreshold, onEndReachedThreshold } +- } = state; +- const contentSize = getContentSize(ctx); +- resetSharedEdgeGateIfOutsideHysteresis(ctx); +- if (contentSize > 0 && queuedInitialLayout) { +- const insetEnd = getContentInsetEnd(ctx); +- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; +- const isContentLess = contentSize < scrollLength; +- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); +- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); +- set$( +- ctx, +- "isWithinMaintainScrollAtEndThreshold", +- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength +- ); +- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; +- if (!shouldSkipThresholdChecks) { +- state.isEndReached = checkThreshold( +- distanceFromEnd, +- isContentLess, +- onEndReachedThreshold * scrollLength, +- state.isEndReached, +- state.endReachedSnapshot, +- { +- contentSize, +- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, +- scrollPosition: scroll +- }, +- (distance) => { +- var _a4, _b; +- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { +- markReachedEdge(ctx); +- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); +- } +- }, +- (snapshot) => { +- state.endReachedSnapshot = snapshot; +- } +- ); +- } +- } +-} +- +-// src/utils/checkAtTop.ts +-function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { +- const state = ctx == null ? void 0 : ctx.state; +- if (!state) { +- return; +- } +- const { +- isStartReached, +- props: { data, onStartReachedThreshold }, +- scroll, +- scrollLength, +- startReachedSnapshot, +- totalSize +- } = state; +- const dataLength = data.length; +- const threshold = onStartReachedThreshold * scrollLength; +- resetSharedEdgeGateIfOutsideHysteresis(ctx); +- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { +- state.isStartReached = false; +- state.startReachedSnapshot = void 0; +- } +- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); +- set$(ctx, "isNearStart", scroll <= threshold); +- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; +- if (!shouldSkipThresholdChecks) { +- state.isStartReached = checkThreshold( +- scroll, +- false, +- threshold, +- state.isStartReached, +- startReachedSnapshot, +- { +- contentSize: totalSize, +- dataLength, +- scrollPosition: scroll +- }, +- (distance) => { +- var _a3, _b; +- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { +- markReachedEdge(ctx); +- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); +- } +- }, +- (snapshot) => { +- state.startReachedSnapshot = snapshot; +- } +- ); +- } +-} +- +-// src/utils/checkThresholds.ts +-function checkThresholds(ctx, allowedEdge) { +- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; +- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); +- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); +-} +- +-// src/core/recalculateSettledScroll.ts +-function recalculateSettledScroll(ctx) { +- var _a3, _b; +- const state = ctx.state; +- if ((_a3 = state.props) == null ? void 0 : _a3.data) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); +- } +- checkThresholds(ctx); +-} +-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; +-function clearAdaptiveRenderExitTimeout(ctx) { +- ctx.state.scheduledWork.cancel("adaptiveRender"); +-} +-function scheduleAdaptiveRenderExit(ctx, exitDelay) { +- const state = ctx.state; +- clearAdaptiveRenderExitTimeout(ctx); +- if (exitDelay <= 0) { +- setAdaptiveRender(ctx, "normal", "scroll"); +- } else { +- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); +- } +-} +-function setAdaptiveRender(ctx, mode, reason) { +- var _a3, _b; +- const previousMode = peek$(ctx, "adaptiveRender"); +- if (previousMode !== mode) { +- set$(ctx, "adaptiveRender", mode); +- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); +- } +-} +-function resetAdaptiveRender(ctx) { +- var _a3, _b; +- clearAdaptiveRenderExitTimeout(ctx); +- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; +- if (peek$(ctx, "adaptiveRender") !== mode) { +- setAdaptiveRender(ctx, mode, "initial"); +- } +-} +-function updateAdaptiveRender(ctx, scrollVelocity, options) { +- var _a3, _b, _c; +- const state = ctx.state; +- const adaptiveRender = state.props.adaptiveRender; +- const currentMode = peek$(ctx, "adaptiveRender"); +- if (peek$(ctx, "readyToRender")) { +- if (adaptiveRender) { +- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; +- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; +- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; +- const threshold = currentMode === "light" ? exitVelocity : enterVelocity; +- const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; +- const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; +- if (nextMode !== previousMode) { +- if (nextMode === "light") { +- setAdaptiveRender(ctx, "light", "scroll"); +- scheduleAdaptiveRenderExit(ctx, exitDelay); +- } else if (currentMode === "light") { +- scheduleAdaptiveRenderExit(ctx, exitDelay); +- } +- } +- } else { +- resetAdaptiveRender(ctx); +- } +- } +-} +- +-// src/utils/getEffectiveDrawDistance.ts +-var INITIAL_DRAW_DISTANCE = 50; +-function getEffectiveDrawDistance(ctx, mode) { +- var _a3; +- const drawDistance = ctx.state.props.drawDistance; +- const initialScroll = ctx.state.initialScroll; +- const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; +- const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; +- return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; +-} +-function scheduleFullDrawDistancePrewarm(ctx) { +- const { state } = ctx; +- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { +- return; +- } +- state.scheduledWork.frame(() => { +- var _a3; +- return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +- }, "fullDrawDistancePrewarm"); +-} +- +-// src/utils/setInitialRenderState.ts +-function resetInitialRenderState(ctx, { +- resetLayout, +- resetInitialScroll +-}) { +- const { state } = ctx; +- if (resetLayout) { +- state.didContainersLayout = false; +- state.queuedInitialLayout = false; +- } +- if (resetInitialScroll) { +- state.didFinishInitialScroll = false; +- } +- set$(ctx, "readyToRender", false); +- resetAdaptiveRender(ctx); +-} +-function setInitialRenderState(ctx, { +- didLayout, +- didInitialScroll +-}) { +- const { state } = ctx; +- const { +- loadStartTime, +- props: { onLoad } +- } = state; +- if (didLayout) { +- state.didContainersLayout = true; +- } +- if (didInitialScroll) { +- state.didFinishInitialScroll = true; +- } +- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); +- if (isReadyToRender && !peek$(ctx, "readyToRender")) { +- set$(ctx, "readyToRender", true); +- setAdaptiveRender(ctx, "normal", "ready"); +- if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { +- scheduleFullDrawDistancePrewarm(ctx); +- } +- if (!state.didLoad) { +- state.didLoad = true; +- if (onLoad) { +- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); +- } +- } +- } +-} +- +-// src/core/finishInitialScroll.ts +-var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; +-function syncInitialScrollOffset(state, offset) { +- state.scroll = offset; +- state.scrollPending = offset; +- state.scrollPrev = offset; +-} +-function clearPreservedInitialScrollTargetTimeout(state) { +- state.scheduledWork.cancel("preservedInitialScroll"); +-} +-function clearPreservedInitialScrollTarget(state) { +- clearPreservedInitialScrollTargetTimeout(state); +- state.clearPreservedInitialScrollOnNextFinish = void 0; +- state.initialScroll = void 0; +- setInitialScrollSession(state); +-} +-function supersedeInitialScroll(ctx) { +- var _a3, _b, _c; +- const state = ctx.state; +- const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; +- if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { +- if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { +- cancelAnimationFrame(bootstrapInitialScroll.frameHandle); +- } +- if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { +- cancelScrollCompletionChecks(state); +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- } +- initialScrollCompletion.resetFlags(state); +- setInitialScrollSession(state, { bootstrap: null }); +- finishInitialScroll(ctx); +- } +-} +-function finishInitialScroll(ctx, options) { +- var _a3, _b, _c; +- const state = ctx.state; +- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { +- syncInitialScrollOffset(state, options.resolvedOffset); +- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { +- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); +- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { +- syncInitialScrollOffset(state, observedOffset); +- } +- } +- const complete = () => { +- var _a4, _b2, _c2, _d, _e; +- const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; +- const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0; +- initialScrollWatchdog.clear(state); +- if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) { +- state.clearPreservedInitialScrollOnNextFinish = void 0; +- setInitialScrollSession(state); +- clearPreservedInitialScrollTargetTimeout(state); +- if (options == null ? void 0 : options.schedulePreservedTargetClear) { +- state.scheduledWork.timeout( +- () => { +- var _a5; +- if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) { +- return; +- } +- clearPreservedInitialScrollTarget(state); +- }, +- PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, +- "preservedInitialScroll" +- ); +- } +- } else { +- clearPreservedInitialScrollTarget(state); +- } +- if (options == null ? void 0 : options.recalculateItems) { +- recalculateSettledScroll(ctx); +- } +- setInitialRenderState(ctx, { didInitialScroll: true }); +- if (shouldReleaseDeferredPublicOnScroll) { +- releaseDeferredPublicOnScroll(ctx, finalScrollOffset); +- } +- (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); +- }; +- if (options == null ? void 0 : options.waitForCompletionFrame) { +- requestAnimationFrame(complete); +- return; +- } +- complete(); +-} +- +-// src/core/calculateOffsetForIndex.ts +-function calculateOffsetForIndex(ctx, index) { +- const state = ctx.state; +- return index !== void 0 ? state.positions[index] || 0 : 0; +-} +- + // src/core/getStartOffsetAdjustment.ts + function getStartOffsetAdjustment(ctx) { + const { state } = ctx; +@@ -1213,58 +614,273 @@ function getItemSizeAtIndex(ctx, index) { + if (index === void 0 || index < 0) { + return void 0; + } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; ++ } ++ if (index !== void 0) { ++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); ++ if (startOffsetAdjustment) { ++ offset += startOffsetAdjustment; ++ } ++ } ++ if (viewPosition !== void 0 && index !== void 0) { ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return offset; ++ } ++ const isOutOfBounds = index < 0 || index >= dataLength; ++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; ++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); ++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); ++ const trailingInset = getContentInsetEnd(ctx); ++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); ++ if (!isOutOfBounds && index === state.props.data.length - 1) { ++ const footerSize = peek$(ctx, "footerSize") || 0; ++ offset += footerSize; ++ } ++ } ++ return offset; ++} ++ ++// src/core/clampScrollOffset.ts ++function clampScrollOffset(ctx, offset, scrollTarget) { ++ const state = ctx.state; ++ const contentSize = getContentSize(ctx); ++ let clampedOffset = offset; ++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { ++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); ++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; ++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; ++ const maxOffset = baseMaxOffset + extraEndOffset; ++ clampedOffset = Math.min(offset, maxOffset); ++ } ++ clampedOffset = Math.max(0, clampedOffset); ++ return clampedOffset; ++} ++ ++// src/utils/checkThreshold.ts ++var HYSTERESIS_MULTIPLIER = 1.3; ++function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { ++ const absDistance = Math.abs(distance); ++ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; ++} ++var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { ++ const absDistance = Math.abs(distance); ++ const within = atThreshold || threshold > 0 && absDistance <= threshold; ++ const updateSnapshot = () => { ++ setSnapshot({ ++ atThreshold, ++ contentSize: context.contentSize, ++ dataLength: context.dataLength, ++ scrollPosition: context.scrollPosition ++ }); ++ }; ++ if (!wasReached) { ++ if (!within) { ++ return false; ++ } ++ onReached(distance); ++ updateSnapshot(); ++ return true; ++ } ++ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); ++ if (reset) { ++ setSnapshot(void 0); ++ return false; ++ } ++ if (within) { ++ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; ++ if (changed) { ++ updateSnapshot(); ++ } ++ } ++ return true; ++}; ++ ++// src/utils/edgeReachedGate.ts ++function resetEdgeLatch(ctx, edge) { ++ const state = ctx.state; ++ if (edge === "start") { ++ state.isStartReached = false; ++ state.startReachedSnapshot = void 0; ++ } else { ++ state.isEndReached = false; ++ state.endReachedSnapshot = void 0; ++ } ++} ++function resetSharedEdgeGateIfOutsideHysteresis(ctx) { ++ const state = ctx.state; ++ if (!state.edgeReachedGate) { ++ return; ++ } ++ const contentSize = getContentSize(ctx); ++ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); ++ const isContentLess = contentSize < state.scrollLength; ++ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; ++ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; ++ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); ++ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); ++ if (isOutsideStart && isOutsideEnd) { ++ state.edgeReachedGate = void 0; ++ } ++} ++function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { ++ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; ++} ++function markReachedEdge(ctx) { ++ ctx.state.edgeReachedGate = "closed"; ++} ++function prepareReachedEdgeForNextUserScroll(ctx) { ++ if (ctx.state.edgeReachedGate) { ++ ctx.state.edgeReachedGate = "prepared"; ++ } ++} ++function beginReachedEdgeUserScroll(ctx, scrollDelta) { ++ const state = ctx.state; ++ if (state.edgeReachedGate !== "prepared") { ++ return void 0; ++ } ++ const allowedEdge = scrollDelta < 0 ? "start" : "end"; ++ state.edgeReachedGate = "closed"; ++ resetEdgeLatch(ctx, allowedEdge); ++ return allowedEdge; ++} ++ ++// src/utils/hasActiveInitialScroll.ts ++function hasActiveInitialScroll(state) { ++ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; ++} ++ ++// src/utils/checkAtBottom.ts ++function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { ++ var _a3; ++ const state = ctx.state; ++ if (!state) { ++ return; ++ } ++ const { ++ queuedInitialLayout, ++ scrollLength, ++ scroll, ++ maintainingScrollAtEnd, ++ props: { maintainScrollAtEndThreshold, onEndReachedThreshold } ++ } = state; ++ const contentSize = getContentSize(ctx); ++ resetSharedEdgeGateIfOutsideHysteresis(ctx); ++ if (contentSize > 0 && queuedInitialLayout) { ++ const insetEnd = getContentInsetEnd(ctx); ++ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; ++ const isContentLess = contentSize < scrollLength; ++ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); ++ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); ++ set$( ++ ctx, ++ "isWithinMaintainScrollAtEndThreshold", ++ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength ++ ); ++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; ++ if (!shouldSkipThresholdChecks) { ++ state.isEndReached = checkThreshold( ++ distanceFromEnd, ++ isContentLess, ++ onEndReachedThreshold * scrollLength, ++ state.isEndReached, ++ state.endReachedSnapshot, ++ { ++ contentSize, ++ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, ++ scrollPosition: scroll ++ }, ++ (distance) => { ++ var _a4, _b; ++ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { ++ markReachedEdge(ctx); ++ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); ++ } ++ }, ++ (snapshot) => { ++ state.endReachedSnapshot = snapshot; ++ } ++ ); ++ } ++ } ++} ++ ++// src/utils/checkAtTop.ts ++function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { ++ const state = ctx == null ? void 0 : ctx.state; ++ if (!state) { ++ return; ++ } ++ const { ++ isStartReached, ++ props: { data, onStartReachedThreshold }, ++ scroll, ++ scrollLength, ++ startReachedSnapshot, ++ totalSize ++ } = state; ++ const dataLength = data.length; ++ const threshold = onStartReachedThreshold * scrollLength; ++ resetSharedEdgeGateIfOutsideHysteresis(ctx); ++ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { ++ state.isStartReached = false; ++ state.startReachedSnapshot = void 0; ++ } ++ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); ++ set$(ctx, "isNearStart", scroll <= threshold); ++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; ++ if (!shouldSkipThresholdChecks) { ++ state.isStartReached = checkThreshold( ++ scroll, ++ false, ++ threshold, ++ state.isStartReached, ++ startReachedSnapshot, ++ { ++ contentSize: totalSize, ++ dataLength, ++ scrollPosition: scroll ++ }, ++ (distance) => { ++ var _a3, _b; ++ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { ++ markReachedEdge(ctx); ++ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); ++ } ++ }, ++ (snapshot) => { ++ state.startReachedSnapshot = snapshot; ++ } ++ ); ++ } + } + +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +- } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } +- } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } +- } +- return offset; ++// src/utils/checkThresholds.ts ++function checkThresholds(ctx, allowedEdge) { ++ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; ++ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); ++ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + } + +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { ++// src/core/recalculateSettledScroll.ts ++function recalculateSettledScroll(ctx) { ++ var _a3, _b; + const state = ctx.state; +- const contentSize = getContentSize(ctx); +- let clampedOffset = offset; +- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { +- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); +- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; +- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; +- const maxOffset = baseMaxOffset + extraEndOffset; +- clampedOffset = Math.min(offset, maxOffset); ++ if ((_a3 = state.props) == null ? void 0 : _a3.data) { ++ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); + } +- clampedOffset = Math.max(0, clampedOffset); +- return clampedOffset; ++ checkThresholds(ctx); + } + + // src/core/finishScrollTo.ts +@@ -1401,7 +1017,182 @@ function listenForScrollEnd(ctx, params) { + } else { + idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); + } +- scheduledWork.register("platformScrollCompletion", cancel); ++ scheduledWork.register("platformScrollCompletion", cancel); ++} ++ ++// src/core/initialScrollSession.ts ++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; ++function hasInitialScrollSessionCompletion(completion) { ++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); ++} ++function clearInitialScrollSession(state) { ++ state.initialScrollSession = void 0; ++ return void 0; ++} ++function createInitialScrollSession(options) { ++ const { bootstrap, completion, kind, previousDataLength } = options; ++ return kind === "offset" ? { ++ completion, ++ kind, ++ previousDataLength ++ } : { ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }; ++} ++function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { ++ var _a4, _b2; ++ if (!state.initialScrollSession) { ++ state.initialScrollSession = createInitialScrollSession({ ++ completion: {}, ++ kind, ++ previousDataLength: 0 ++ }); ++ } else if (state.initialScrollSession.kind !== kind) { ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, ++ completion: state.initialScrollSession.completion, ++ kind, ++ previousDataLength: state.initialScrollSession.previousDataLength ++ }); ++ } ++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; ++ return state.initialScrollSession.completion; ++} ++var initialScrollCompletion = { ++ didDispatchNativeScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); ++ }, ++ didRetrySilentInitialScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); ++ }, ++ markInitialScrollNativeDispatch(state) { ++ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; ++ }, ++ markSilentInitialScrollRetry(state) { ++ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; ++ }, ++ resetFlags(state) { ++ if (!state.initialScrollSession) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); ++ completion.didDispatchNativeScroll = void 0; ++ completion.didRetrySilentInitialScroll = void 0; ++ } ++}; ++var initialScrollWatchdog = { ++ clear(state) { ++ initialScrollWatchdog.set(state, void 0); ++ }, ++ didReachTarget(newScroll, watchdog) { ++ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); ++ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ get(state) { ++ var _a3, _b; ++ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; ++ }, ++ hasNonZeroTargetOffset(targetOffset) { ++ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ isAtZeroTargetOffset(targetOffset) { ++ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ set(state, watchdog) { ++ var _a3, _b; ++ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state); ++ completion.watchdog = watchdog ? { ++ startScroll: watchdog.startScroll, ++ targetOffset: watchdog.targetOffset ++ } : void 0; ++ } ++}; ++function setInitialScrollSession(state, options = {}) { ++ var _a3, _b, _c, _d; ++ const existingSession = state.initialScrollSession; ++ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; ++ const completion = existingSession == null ? void 0 : existingSession.completion; ++ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; ++ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; ++ if (!kind) { ++ return clearInitialScrollSession(state); ++ } ++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { ++ return clearInitialScrollSession(state); ++ } ++ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }); ++ return state.initialScrollSession; ++} ++var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; ++function clearAdaptiveRenderExitTimeout(ctx) { ++ ctx.state.scheduledWork.cancel("adaptiveRender"); ++} ++function scheduleAdaptiveRenderExit(ctx, exitDelay) { ++ const state = ctx.state; ++ clearAdaptiveRenderExitTimeout(ctx); ++ if (exitDelay <= 0) { ++ setAdaptiveRender(ctx, "normal", "scroll"); ++ } else { ++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); ++ } ++} ++function setAdaptiveRender(ctx, mode, reason) { ++ var _a3, _b; ++ const previousMode = peek$(ctx, "adaptiveRender"); ++ if (previousMode !== mode) { ++ set$(ctx, "adaptiveRender", mode); ++ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); ++ } ++} ++function resetAdaptiveRender(ctx) { ++ var _a3, _b; ++ clearAdaptiveRenderExitTimeout(ctx); ++ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; ++ if (peek$(ctx, "adaptiveRender") !== mode) { ++ setAdaptiveRender(ctx, mode, "initial"); ++ } ++} ++function updateAdaptiveRender(ctx, scrollVelocity, options) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ const adaptiveRender = state.props.adaptiveRender; ++ const currentMode = peek$(ctx, "adaptiveRender"); ++ if (peek$(ctx, "readyToRender")) { ++ if (adaptiveRender) { ++ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; ++ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; ++ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; ++ const threshold = currentMode === "light" ? exitVelocity : enterVelocity; ++ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; ++ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; ++ if (nextMode !== previousMode) { ++ if (nextMode === "light") { ++ setAdaptiveRender(ctx, "light", "scroll"); ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } else if (currentMode === "light") { ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } ++ } ++ } else { ++ resetAdaptiveRender(ctx); ++ } ++ } + } + + // src/core/doMaintainScrollAtEnd.ts +@@ -1788,6 +1579,27 @@ function prepareMVCP(ctx, dataChanged) { + } + } + ++// src/utils/getEffectiveDrawDistance.ts ++var INITIAL_DRAW_DISTANCE = 50; ++function getEffectiveDrawDistance(ctx, mode) { ++ var _a3; ++ const drawDistance = ctx.state.props.drawDistance; ++ const initialScroll = ctx.state.initialScroll; ++ const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; ++ const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; ++ return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; ++} ++function scheduleFullDrawDistancePrewarm(ctx) { ++ const { state } = ctx; ++ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { ++ return; ++ } ++ state.scheduledWork.frame(() => { ++ var _a3; ++ return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); ++ }, "fullDrawDistancePrewarm"); ++} ++ + // src/utils/getScrollVelocity.ts + var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; + var SCROLL_VELOCITY_HALF_LIFE_MS = 200; +@@ -2048,7 +1860,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + } + } + function scrollTo(ctx, params) { +- var _a3, _b; ++ var _a3, _b, _c; + const state = ctx.state; + const { noScrollingTo, forceScroll, ...scrollTarget } = params; + const { +@@ -2080,6 +1892,15 @@ function scrollTo(ctx, params) { + }; + if (!isInitialScroll) { + pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); ++ if (scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { ++ beginScrollTargetSettle(ctx, { ++ index: scrollTarget.index, ++ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, ++ viewPosition: scrollTarget.viewPosition ++ }); ++ } else { ++ clearScrollTargetSettle(state); ++ } + } + } + state.scrollPending = targetOffset; +@@ -2087,7 +1908,7 @@ function scrollTo(ctx, params) { + if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { + if (animated) { + if (state.scrollTargetPinnedRange) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); + } + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); +@@ -2100,6 +1921,280 @@ function scrollTo(ctx, params) { + } + } + ++// src/core/scrollTargetSettle.ts ++var SETTLE_POSITION_EPSILON = 0.5; ++var SETTLE_QUIET_PASSES_TO_RELEASE = 2; ++var SETTLE_TTL_MS = 500; ++var SETTLE_MAX_MS = 1e3; ++var SETTLE_MAX_CORRECTIONS = 8; ++function clearScrollTargetSettle(state) { ++ state.scrollTargetSettle = void 0; ++} ++function beginScrollTargetSettle(ctx, params) { ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ const { data } = state.props; ++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; ++ if (index < 0 || index >= data.length || isEndAlignedLastItem) { ++ clearScrollTargetSettle(state); ++ return; ++ } ++ const now = Date.now(); ++ const id = getId(state, index); ++ const existing = state.scrollTargetSettle; ++ const isSameTarget = (existing == null ? void 0 : existing.id) === id; ++ state.scrollTargetSettle = { ++ corrections: isSameTarget ? existing.corrections : 0, ++ deadline: isSameTarget ? existing.deadline : now + SETTLE_MAX_MS, ++ expiresAt: now + SETTLE_TTL_MS, ++ id, ++ quietPasses: 0, ++ viewOffset, ++ viewPosition ++ }; ++} ++function getSettleTargetOffset(ctx, settle, index, position) { ++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; ++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); ++} ++function settleScrollTarget(ctx, isCompensating) { ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ return false; ++ } ++ if (isCompensating) { ++ settle.quietPasses = 0; ++ settle.expiresAt = Date.now() + SETTLE_TTL_MS; ++ return false; ++ } ++ const index = state.indexByKey.get(settle.id); ++ const position = index === void 0 ? void 0 : state.positions[index]; ++ if (index === void 0 || position === void 0) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ const now = Date.now(); ++ if (now > settle.expiresAt || now > settle.deadline) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position); ++ const diff = targetOffset - state.scroll; ++ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ settle.quietPasses++; ++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { ++ clearScrollTargetSettle(state); ++ } ++ return false; ++ } ++ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ settle.quietPasses = 0; ++ settle.corrections++; ++ settle.expiresAt = now + SETTLE_TTL_MS; ++ scrollTo(ctx, { ++ animated: false, ++ index, ++ itemSize: getItemSizeAtIndex(ctx, index), ++ offset: position, ++ viewOffset: settle.viewOffset, ++ viewPosition: settle.viewPosition ++ }); ++ return true; ++} ++ ++// src/core/cancelImperativeScroll.ts ++function cancelScrollCompletionChecks({ scheduledWork }) { ++ scheduledWork.cancel("checkFinishedScrollFrame"); ++ scheduledWork.cancel("checkFinishedScrollRetryFrame"); ++ scheduledWork.cancel("checkFinishedScrollFallback"); ++ scheduledWork.cancel("platformScrollCompletion"); ++} ++function settlePendingImperativeScroll(state) { ++ var _a3, _b; ++ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; ++ state.pendingScrollResolve = void 0; ++ state.pendingScrollToEnd = void 0; ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); ++} ++function cancelImperativeScroll(state) { ++ cancelScrollCompletionChecks(state); ++ state.scheduledWork.cancel("imperativeScrollReady"); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ clearScrollTargetSettle(state); ++ settlePendingImperativeScroll(state); ++} ++ ++// src/core/deferredPublicOnScroll.ts ++function withResolvedContentOffset(state, event, resolvedOffset) { ++ return { ++ ...event, ++ nativeEvent: { ++ ...event.nativeEvent, ++ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } ++ } ++ }; ++} ++function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { ++ var _a3, _b, _c, _d; ++ const state = ctx.state; ++ const deferredEvent = state.deferredPublicOnScrollEvent; ++ state.deferredPublicOnScrollEvent = void 0; ++ if (deferredEvent) { ++ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( ++ _c, ++ withResolvedContentOffset( ++ state, ++ deferredEvent, ++ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 ++ ) ++ ); ++ } ++} ++ ++// src/utils/setInitialRenderState.ts ++function resetInitialRenderState(ctx, { ++ resetLayout, ++ resetInitialScroll ++}) { ++ const { state } = ctx; ++ if (resetLayout) { ++ state.didContainersLayout = false; ++ state.queuedInitialLayout = false; ++ } ++ if (resetInitialScroll) { ++ state.didFinishInitialScroll = false; ++ } ++ set$(ctx, "readyToRender", false); ++ resetAdaptiveRender(ctx); ++} ++function setInitialRenderState(ctx, { ++ didLayout, ++ didInitialScroll ++}) { ++ const { state } = ctx; ++ const { ++ loadStartTime, ++ props: { onLoad } ++ } = state; ++ if (didLayout) { ++ state.didContainersLayout = true; ++ } ++ if (didInitialScroll) { ++ state.didFinishInitialScroll = true; ++ } ++ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); ++ if (isReadyToRender && !peek$(ctx, "readyToRender")) { ++ set$(ctx, "readyToRender", true); ++ setAdaptiveRender(ctx, "normal", "ready"); ++ if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { ++ scheduleFullDrawDistancePrewarm(ctx); ++ } ++ if (!state.didLoad) { ++ state.didLoad = true; ++ if (onLoad) { ++ onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); ++ } ++ } ++ } ++} ++ ++// src/core/finishInitialScroll.ts ++var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; ++function syncInitialScrollOffset(state, offset) { ++ state.scroll = offset; ++ state.scrollPending = offset; ++ state.scrollPrev = offset; ++} ++function clearPreservedInitialScrollTargetTimeout(state) { ++ state.scheduledWork.cancel("preservedInitialScroll"); ++} ++function clearPreservedInitialScrollTarget(state) { ++ clearPreservedInitialScrollTargetTimeout(state); ++ state.clearPreservedInitialScrollOnNextFinish = void 0; ++ state.initialScroll = void 0; ++ setInitialScrollSession(state); ++} ++function supersedeInitialScroll(ctx) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; ++ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { ++ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { ++ cancelAnimationFrame(bootstrapInitialScroll.frameHandle); ++ } ++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { ++ cancelScrollCompletionChecks(state); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ } ++ initialScrollCompletion.resetFlags(state); ++ setInitialScrollSession(state, { bootstrap: null }); ++ finishInitialScroll(ctx); ++ } ++} ++function finishInitialScroll(ctx, options) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { ++ syncInitialScrollOffset(state, options.resolvedOffset); ++ } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { ++ const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); ++ if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { ++ syncInitialScrollOffset(state, observedOffset); ++ } ++ } ++ const complete = () => { ++ var _a4, _b2, _c2, _d, _e; ++ const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; ++ const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0; ++ initialScrollWatchdog.clear(state); ++ if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) { ++ state.clearPreservedInitialScrollOnNextFinish = void 0; ++ setInitialScrollSession(state); ++ clearPreservedInitialScrollTargetTimeout(state); ++ if (options == null ? void 0 : options.schedulePreservedTargetClear) { ++ state.scheduledWork.timeout( ++ () => { ++ var _a5; ++ if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) { ++ return; ++ } ++ clearPreservedInitialScrollTarget(state); ++ }, ++ PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, ++ "preservedInitialScroll" ++ ); ++ } ++ } else { ++ clearPreservedInitialScrollTarget(state); ++ } ++ if (options == null ? void 0 : options.recalculateItems) { ++ recalculateSettledScroll(ctx); ++ } ++ setInitialRenderState(ctx, { didInitialScroll: true }); ++ if (shouldReleaseDeferredPublicOnScroll) { ++ releaseDeferredPublicOnScroll(ctx, finalScrollOffset); ++ } ++ (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); ++ }; ++ if (options == null ? void 0 : options.waitForCompletionFrame) { ++ requestAnimationFrame(complete); ++ return; ++ } ++ complete(); ++} ++ ++// src/core/calculateOffsetForIndex.ts ++function calculateOffsetForIndex(ctx, index) { ++ const state = ctx.state; ++ return index !== void 0 ? state.positions[index] || 0 : 0; ++} ++ + // src/core/scrollToIndex.ts + function clampScrollIndex(index, dataLength) { + if (dataLength <= 0) { +@@ -4346,7 +4441,10 @@ function calculateItemsInView(ctx, params = {}) { + const scrollBeforeMVCP = state.scroll; + const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; + checkMVCP == null ? void 0 : checkMVCP(); +- const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); ++ const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; ++ const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); ++ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx, didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0); ++ const didMVCPAdjustScroll = didMVCPAdjust || didSettleScrollTarget; + if (didMVCPAdjustScroll) { + updateScroll2(state.scroll); + updateScrollRange(); +@@ -6111,6 +6209,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + return (_a4 = scrollRef.current) == null ? void 0 : _a4.getBoundingClientRect(); + }, + getCurrentScrollOffset, ++ getMaxScrollOffset, + getScrollableNode: () => resolveScrollableNode(scrollRef.current, isWindowScroll), + getScrollEventTarget: () => getScrollTarget(), + getScrollResponder: () => resolveScrollableNode(scrollRef.current, isWindowScroll), +@@ -7184,6 +7283,22 @@ function createColumnWrapperStyle(contentContainerStyle) { + } + } + ++// src/core/isScrollExtentSynced.ts ++var SCROLL_EXTENT_EPSILON = 1; ++function isScrollExtentSynced(ctx) { ++ var _a3, _b; ++ const state = ctx.state; ++ const platformMaxOffset = (_b = (_a3 = state.refScroller.current) == null ? void 0 : _a3.getMaxScrollOffset) == null ? void 0 : _b.call(_a3); ++ if (platformMaxOffset === void 0) { ++ return true; ++ } ++ if (state.scrollLength <= 0) { ++ return true; ++ } ++ const contentMaxOffset = Math.max(0, getContentSize(ctx) - state.scrollLength); ++ return platformMaxOffset >= contentMaxOffset - SCROLL_EXTENT_EPSILON; ++} ++ + // src/core/scrollToEnd.ts + function scrollToEnd(ctx, options) { + const state = ctx.state; +@@ -7229,6 +7344,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { + const IMPERATIVE_SCROLL_SETTLE_STABLE_FRAMES = 2; + let imperativeScrollToken = 0; + const isSettlingAfterDataChange = () => !!state.didDataChange || !!state.didColumnsChange || state.scheduledWork.has("mvcpRecalculate") || state.ignoreScrollFromMVCP !== void 0; ++ const isScrollBlocked = () => isSettlingAfterDataChange() || !isScrollExtentSynced(ctx); + const isScrollToIndexReady = (targetIndex, allowEmpty = false) => { + var _a3; + const props = state.props; +@@ -7252,7 +7368,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { + if (token !== imperativeScrollToken) { + return; + } +- if (isSettlingAfterDataChange() || !isReady()) { ++ if (isScrollBlocked() || !isReady()) { + stableFrames = 0; + } else { + stableFrames += 1; +@@ -7279,7 +7395,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { + resolve(); + } + }; +- if (isSettlingAfterDataChange() || !isReady()) { ++ if (isScrollBlocked() || !isReady()) { + runWhenReady(token, runNow, isReady); + } else { + runNow(); +@@ -8267,6 +8383,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + onScrollBeginDrag: (event) => { + var _a4, _b2; + prepareReachedEdgeForNextUserScroll(ctx); ++ clearScrollTargetSettle(state); + (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); + }, + onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) From 3effec366e183299c244060024ae28202adc9b86 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 11 Aug 2026 12:34:55 -0400 Subject: [PATCH 04/38] fix(chat): rework the legend-list scroll fixes after review Review of the patch found three user-visible problems with the first cut, all fixed upstream in the fork and regenerated here. - The web fix gated every imperative scroll on the document extent matching the list's content size. Those disagree permanently whenever contentInset is in play, since web never applies the base inset to layout, so every scroll would have waited out the full 800ms readiness timeout. It is now a bounded re-issue inside the web scroll view, which also covers the initial scroll, scroll adjustments and completion checks rather than only the imperative path. - Settling a scroll target cancelled on onScrollBeginDrag, which is not wired on web. Scrolling within a second of jumping to a search hit yanked the list back. Cancellation is now driven by a scroll event landing somewhere other than where it was asked to, which covers wheel, trackpad, scrollbar and keyboard. - Settling armed for animated scrolls too, turning them into an instant jump, and corrected from inside the layout pass rather than after it. Also bumps @legendapp/list to 3.3.5 and drops the stale 3.3.4 patch, matching what #29526 did on master. The patch here targets 3.3.5, so without the bump patch-package would silently skip it on a clean install. --- shared/patches/@legendapp+list+3.3.5.patch | 1258 ++++++++++++-------- 1 file changed, 762 insertions(+), 496 deletions(-) diff --git a/shared/patches/@legendapp+list+3.3.5.patch b/shared/patches/@legendapp+list+3.3.5.patch index dbbe69f5ec97..e69e63c04aa3 100644 --- a/shared/patches/@legendapp+list+3.3.5.patch +++ b/shared/patches/@legendapp+list+3.3.5.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js -index b3c5a30..d256c30 100644 +index b3c5a30..220444d 100644 --- a/node_modules/@legendapp/list/react-native.js +++ b/node_modules/@legendapp/list/react-native.js @@ -457,611 +457,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -1135,7 +1135,15 @@ index b3c5a30..d256c30 100644 // src/core/doMaintainScrollAtEnd.ts function doMaintainScrollAtEnd(ctx) { const state = ctx.state; -@@ -1977,6 +1774,27 @@ var flushSync = (fn) => { +@@ -1580,6 +1377,7 @@ function doMaintainScrollAtEnd(ctx) { + const didScrollSinceRequest = state.scroll !== scrollAtRequest; + if (isStillWithinThreshold || !didScrollSinceRequest) { + state.maintainingScrollAtEnd = activeState; ++ clearScrollTargetSettle(state); + const scroller = refScroller.current; + if (state.props.horizontal && isHorizontalRTL(state)) { + const currentContentSize = getContentSize(ctx); +@@ -1977,6 +1775,27 @@ var flushSync = (fn) => { fn(); }; @@ -1163,7 +1171,25 @@ index b3c5a30..d256c30 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2224,7 +2042,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -2026,6 +1845,7 @@ function isInMVCPActiveMode(state) { + } + + // src/core/updateScroll.ts ++var SETTLE_RELEASE_EPSILON = 2; + function updateScroll(ctx, newScroll, forceUpdate, options) { + var _a3; + const state = ctx.state; +@@ -2061,6 +1881,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { + state.scrollPrevTime = state.scrollTime; + state.scroll = newScroll; + state.scrollTime = currentTime; ++ if (state.scrollTargetSettle && (options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust && Math.abs(newScroll - state.scrollPending) > SETTLE_RELEASE_EPSILON) { ++ clearScrollTargetSettle(state); ++ } + const scrollDelta = Math.abs(newScroll - prevScroll); + const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; + const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; +@@ -2224,7 +2047,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } function scrollTo(ctx, params) { @@ -1172,11 +1198,11 @@ index b3c5a30..d256c30 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2256,6 +2074,15 @@ function scrollTo(ctx, params) { +@@ -2256,6 +2079,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); -+ if (scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { ++ if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { + beginScrollTargetSettle(ctx, { + index: scrollTarget.index, + viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, @@ -1188,7 +1214,7 @@ index b3c5a30..d256c30 100644 } } state.scrollPending = targetOffset; -@@ -2263,7 +2090,7 @@ function scrollTo(ctx, params) { +@@ -2263,7 +2095,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -1197,7 +1223,7 @@ index b3c5a30..d256c30 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2276,6 +2103,274 @@ function scrollTo(ctx, params) { +@@ -2276,6 +2108,289 @@ function scrollTo(ctx, params) { } } @@ -1209,6 +1235,7 @@ index b3c5a30..d256c30 100644 +var SETTLE_MAX_CORRECTIONS = 8; +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; ++ state.scheduledWork.cancel("scrollTargetSettle"); +} +function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; @@ -1237,6 +1264,27 @@ index b3c5a30..d256c30 100644 + const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; + return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} ++function applyScrollTargetCorrection(ctx, id) { ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle || settle.id !== id) { ++ return; ++ } ++ const index = state.indexByKey.get(id); ++ const position = index === void 0 ? void 0 : state.positions[index]; ++ if (index === void 0 || position === void 0 || Date.now() > settle.deadline) { ++ clearScrollTargetSettle(state); ++ return; ++ } ++ scrollTo(ctx, { ++ animated: false, ++ index, ++ itemSize: getItemSizeAtIndex(ctx, index), ++ offset: position, ++ viewOffset: settle.viewOffset, ++ viewPosition: settle.viewPosition ++ }); ++} +function settleScrollTarget(ctx, isCompensating) { + const state = ctx.state; + const settle = state.scrollTargetSettle; @@ -1275,14 +1323,7 @@ index b3c5a30..d256c30 100644 + settle.quietPasses = 0; + settle.corrections++; + settle.expiresAt = now + SETTLE_TTL_MS; -+ scrollTo(ctx, { -+ animated: false, -+ index, -+ itemSize: getItemSizeAtIndex(ctx, index), -+ offset: position, -+ viewOffset: settle.viewOffset, -+ viewPosition: settle.viewPosition -+ }); ++ state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} + @@ -1472,68 +1513,22 @@ index b3c5a30..d256c30 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4337,7 +4432,10 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4337,7 +4452,13 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); - const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); -+ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx, didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0); -+ const didMVCPAdjustScroll = didMVCPAdjust || didSettleScrollTarget; ++ const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; ++ if (!suppressInitialScrollSideEffects) { ++ settleScrollTarget(ctx, isCompensating); ++ } ++ const didMVCPAdjustScroll = didMVCPAdjust; if (didMVCPAdjustScroll) { updateScroll2(state.scroll); updateScrollRange(); -@@ -6557,6 +6655,22 @@ function createColumnWrapperStyle(contentContainerStyle) { - } - } - -+// src/core/isScrollExtentSynced.ts -+var SCROLL_EXTENT_EPSILON = 1; -+function isScrollExtentSynced(ctx) { -+ var _a3, _b; -+ const state = ctx.state; -+ const platformMaxOffset = (_b = (_a3 = state.refScroller.current) == null ? void 0 : _a3.getMaxScrollOffset) == null ? void 0 : _b.call(_a3); -+ if (platformMaxOffset === void 0) { -+ return true; -+ } -+ if (state.scrollLength <= 0) { -+ return true; -+ } -+ const contentMaxOffset = Math.max(0, getContentSize(ctx) - state.scrollLength); -+ return platformMaxOffset >= contentMaxOffset - SCROLL_EXTENT_EPSILON; -+} -+ - // src/core/scrollToEnd.ts - function scrollToEnd(ctx, options) { - const state = ctx.state; -@@ -6606,6 +6720,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { - const IMPERATIVE_SCROLL_SETTLE_STABLE_FRAMES = 2; - let imperativeScrollToken = 0; - const isSettlingAfterDataChange = () => !!state.didDataChange || !!state.didColumnsChange || state.scheduledWork.has("mvcpRecalculate") || state.ignoreScrollFromMVCP !== void 0; -+ const isScrollBlocked = () => isSettlingAfterDataChange() || !isScrollExtentSynced(ctx); - const isScrollToIndexReady = (targetIndex, allowEmpty = false) => { - var _a3; - const props = state.props; -@@ -6629,7 +6744,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { - if (token !== imperativeScrollToken) { - return; - } -- if (isSettlingAfterDataChange() || !isReady()) { -+ if (isScrollBlocked() || !isReady()) { - stableFrames = 0; - } else { - stableFrames += 1; -@@ -6656,7 +6771,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { - resolve(); - } - }; -- if (isSettlingAfterDataChange() || !isReady()) { -+ if (isScrollBlocked() || !isReady()) { - runWhenReady(token, runNow, isReady); - } else { - runNow(); -@@ -7652,6 +7767,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7652,6 +7773,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -1542,7 +1537,7 @@ index b3c5a30..d256c30 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs -index 40e87cd..af27abe 100644 +index 40e87cd..1581522 100644 --- a/node_modules/@legendapp/list/react-native.mjs +++ b/node_modules/@legendapp/list/react-native.mjs @@ -436,611 +436,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -2678,7 +2673,15 @@ index 40e87cd..af27abe 100644 // src/core/doMaintainScrollAtEnd.ts function doMaintainScrollAtEnd(ctx) { const state = ctx.state; -@@ -1956,6 +1753,27 @@ var flushSync = (fn) => { +@@ -1559,6 +1356,7 @@ function doMaintainScrollAtEnd(ctx) { + const didScrollSinceRequest = state.scroll !== scrollAtRequest; + if (isStillWithinThreshold || !didScrollSinceRequest) { + state.maintainingScrollAtEnd = activeState; ++ clearScrollTargetSettle(state); + const scroller = refScroller.current; + if (state.props.horizontal && isHorizontalRTL(state)) { + const currentContentSize = getContentSize(ctx); +@@ -1956,6 +1754,27 @@ var flushSync = (fn) => { fn(); }; @@ -2706,7 +2709,25 @@ index 40e87cd..af27abe 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2203,7 +2021,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -2005,6 +1824,7 @@ function isInMVCPActiveMode(state) { + } + + // src/core/updateScroll.ts ++var SETTLE_RELEASE_EPSILON = 2; + function updateScroll(ctx, newScroll, forceUpdate, options) { + var _a3; + const state = ctx.state; +@@ -2040,6 +1860,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { + state.scrollPrevTime = state.scrollTime; + state.scroll = newScroll; + state.scrollTime = currentTime; ++ if (state.scrollTargetSettle && (options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust && Math.abs(newScroll - state.scrollPending) > SETTLE_RELEASE_EPSILON) { ++ clearScrollTargetSettle(state); ++ } + const scrollDelta = Math.abs(newScroll - prevScroll); + const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; + const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; +@@ -2203,7 +2026,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } function scrollTo(ctx, params) { @@ -2715,11 +2736,11 @@ index 40e87cd..af27abe 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2235,6 +2053,15 @@ function scrollTo(ctx, params) { +@@ -2235,6 +2058,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); -+ if (scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { ++ if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { + beginScrollTargetSettle(ctx, { + index: scrollTarget.index, + viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, @@ -2731,7 +2752,7 @@ index 40e87cd..af27abe 100644 } } state.scrollPending = targetOffset; -@@ -2242,7 +2069,7 @@ function scrollTo(ctx, params) { +@@ -2242,7 +2074,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -2740,7 +2761,7 @@ index 40e87cd..af27abe 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2255,6 +2082,274 @@ function scrollTo(ctx, params) { +@@ -2255,6 +2087,289 @@ function scrollTo(ctx, params) { } } @@ -2752,6 +2773,7 @@ index 40e87cd..af27abe 100644 +var SETTLE_MAX_CORRECTIONS = 8; +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; ++ state.scheduledWork.cancel("scrollTargetSettle"); +} +function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; @@ -2780,6 +2802,27 @@ index 40e87cd..af27abe 100644 + const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; + return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} ++function applyScrollTargetCorrection(ctx, id) { ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle || settle.id !== id) { ++ return; ++ } ++ const index = state.indexByKey.get(id); ++ const position = index === void 0 ? void 0 : state.positions[index]; ++ if (index === void 0 || position === void 0 || Date.now() > settle.deadline) { ++ clearScrollTargetSettle(state); ++ return; ++ } ++ scrollTo(ctx, { ++ animated: false, ++ index, ++ itemSize: getItemSizeAtIndex(ctx, index), ++ offset: position, ++ viewOffset: settle.viewOffset, ++ viewPosition: settle.viewPosition ++ }); ++} +function settleScrollTarget(ctx, isCompensating) { + const state = ctx.state; + const settle = state.scrollTargetSettle; @@ -2818,14 +2861,7 @@ index 40e87cd..af27abe 100644 + settle.quietPasses = 0; + settle.corrections++; + settle.expiresAt = now + SETTLE_TTL_MS; -+ scrollTo(ctx, { -+ animated: false, -+ index, -+ itemSize: getItemSizeAtIndex(ctx, index), -+ offset: position, -+ viewOffset: settle.viewOffset, -+ viewPosition: settle.viewPosition -+ }); ++ state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} + @@ -3015,68 +3051,22 @@ index 40e87cd..af27abe 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4316,7 +4411,10 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4316,7 +4431,13 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); - const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); -+ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx, didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0); -+ const didMVCPAdjustScroll = didMVCPAdjust || didSettleScrollTarget; ++ const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; ++ if (!suppressInitialScrollSideEffects) { ++ settleScrollTarget(ctx, isCompensating); ++ } ++ const didMVCPAdjustScroll = didMVCPAdjust; if (didMVCPAdjustScroll) { updateScroll2(state.scroll); updateScrollRange(); -@@ -6536,6 +6634,22 @@ function createColumnWrapperStyle(contentContainerStyle) { - } - } - -+// src/core/isScrollExtentSynced.ts -+var SCROLL_EXTENT_EPSILON = 1; -+function isScrollExtentSynced(ctx) { -+ var _a3, _b; -+ const state = ctx.state; -+ const platformMaxOffset = (_b = (_a3 = state.refScroller.current) == null ? void 0 : _a3.getMaxScrollOffset) == null ? void 0 : _b.call(_a3); -+ if (platformMaxOffset === void 0) { -+ return true; -+ } -+ if (state.scrollLength <= 0) { -+ return true; -+ } -+ const contentMaxOffset = Math.max(0, getContentSize(ctx) - state.scrollLength); -+ return platformMaxOffset >= contentMaxOffset - SCROLL_EXTENT_EPSILON; -+} -+ - // src/core/scrollToEnd.ts - function scrollToEnd(ctx, options) { - const state = ctx.state; -@@ -6585,6 +6699,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { - const IMPERATIVE_SCROLL_SETTLE_STABLE_FRAMES = 2; - let imperativeScrollToken = 0; - const isSettlingAfterDataChange = () => !!state.didDataChange || !!state.didColumnsChange || state.scheduledWork.has("mvcpRecalculate") || state.ignoreScrollFromMVCP !== void 0; -+ const isScrollBlocked = () => isSettlingAfterDataChange() || !isScrollExtentSynced(ctx); - const isScrollToIndexReady = (targetIndex, allowEmpty = false) => { - var _a3; - const props = state.props; -@@ -6608,7 +6723,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { - if (token !== imperativeScrollToken) { - return; - } -- if (isSettlingAfterDataChange() || !isReady()) { -+ if (isScrollBlocked() || !isReady()) { - stableFrames = 0; - } else { - stableFrames += 1; -@@ -6635,7 +6750,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { - resolve(); - } - }; -- if (isSettlingAfterDataChange() || !isReady()) { -+ if (isScrollBlocked() || !isReady()) { - runWhenReady(token, runNow, isReady); - } else { - runNow(); -@@ -7631,6 +7746,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7631,6 +7752,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -3084,20 +3074,8 @@ index 40e87cd..af27abe 100644 (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) -diff --git a/node_modules/@legendapp/list/react-native.web.d.ts b/node_modules/@legendapp/list/react-native.web.d.ts -index b6c7481..0ec3d1e 100644 ---- a/node_modules/@legendapp/list/react-native.web.d.ts -+++ b/node_modules/@legendapp/list/react-native.web.d.ts -@@ -6,6 +6,7 @@ type ScrollEventTarget = Window | HTMLElement; - interface ScrollViewMethods { - getBoundingClientRect(): DOMRect | null | undefined; - getCurrentScrollOffset(): number; -+ getMaxScrollOffset(): number; - getScrollableNode(): HTMLElement; - getScrollEventTarget(): ScrollEventTarget | null; - getScrollResponder(): HTMLElement | null; diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js -index 914d2da..e11dd28 100644 +index 914d2da..da09324 100644 --- a/node_modules/@legendapp/list/react-native.web.js +++ b/node_modules/@legendapp/list/react-native.web.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -4026,14 +4004,10 @@ index 914d2da..e11dd28 100644 } // src/core/finishScrollTo.ts -@@ -1422,7 +1038,182 @@ function listenForScrollEnd(ctx, params) { - } else { - idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); - } -- scheduledWork.register("platformScrollCompletion", cancel); -+ scheduledWork.register("platformScrollCompletion", cancel); -+} -+ +@@ -1425,6 +1041,181 @@ function listenForScrollEnd(ctx, params) { + scheduledWork.register("platformScrollCompletion", cancel); + } + +// src/core/initialScrollSession.ts +var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +function hasInitialScrollSessionCompletion(completion) { @@ -4207,10 +4181,20 @@ index 914d2da..e11dd28 100644 + resetAdaptiveRender(ctx); + } + } - } - ++} ++ // src/core/doMaintainScrollAtEnd.ts -@@ -1809,6 +1600,27 @@ function prepareMVCP(ctx, dataChanged) { + function doMaintainScrollAtEnd(ctx) { + const state = ctx.state; +@@ -1456,6 +1247,7 @@ function doMaintainScrollAtEnd(ctx) { + const didScrollSinceRequest = state.scroll !== scrollAtRequest; + if (isStillWithinThreshold || !didScrollSinceRequest) { + state.maintainingScrollAtEnd = activeState; ++ clearScrollTargetSettle(state); + const scroller = refScroller.current; + if (state.props.horizontal && isHorizontalRTL(state)) { + const currentContentSize = getContentSize(ctx); +@@ -1809,6 +1601,27 @@ function prepareMVCP(ctx, dataChanged) { } } @@ -4238,7 +4222,25 @@ index 914d2da..e11dd28 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2069,7 +1881,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -1871,6 +1684,7 @@ function isInMVCPActiveMode(state) { + } + + // src/core/updateScroll.ts ++var SETTLE_RELEASE_EPSILON = 2; + function updateScroll(ctx, newScroll, forceUpdate, options) { + var _a3; + const state = ctx.state; +@@ -1906,6 +1720,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { + state.scrollPrevTime = state.scrollTime; + state.scroll = newScroll; + state.scrollTime = currentTime; ++ if (state.scrollTargetSettle && (options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust && Math.abs(newScroll - state.scrollPending) > SETTLE_RELEASE_EPSILON) { ++ clearScrollTargetSettle(state); ++ } + const scrollDelta = Math.abs(newScroll - prevScroll); + const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; + const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; +@@ -2069,7 +1886,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } function scrollTo(ctx, params) { @@ -4247,11 +4249,11 @@ index 914d2da..e11dd28 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2101,6 +1913,15 @@ function scrollTo(ctx, params) { +@@ -2101,6 +1918,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); -+ if (scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { ++ if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { + beginScrollTargetSettle(ctx, { + index: scrollTarget.index, + viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, @@ -4263,7 +4265,7 @@ index 914d2da..e11dd28 100644 } } state.scrollPending = targetOffset; -@@ -2108,7 +1929,7 @@ function scrollTo(ctx, params) { +@@ -2108,7 +1934,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -4272,7 +4274,7 @@ index 914d2da..e11dd28 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2121,6 +1942,280 @@ function scrollTo(ctx, params) { +@@ -2121,6 +1947,295 @@ function scrollTo(ctx, params) { } } @@ -4284,6 +4286,7 @@ index 914d2da..e11dd28 100644 +var SETTLE_MAX_CORRECTIONS = 8; +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; ++ state.scheduledWork.cancel("scrollTargetSettle"); +} +function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; @@ -4312,6 +4315,27 @@ index 914d2da..e11dd28 100644 + const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; + return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} ++function applyScrollTargetCorrection(ctx, id) { ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle || settle.id !== id) { ++ return; ++ } ++ const index = state.indexByKey.get(id); ++ const position = index === void 0 ? void 0 : state.positions[index]; ++ if (index === void 0 || position === void 0 || Date.now() > settle.deadline) { ++ clearScrollTargetSettle(state); ++ return; ++ } ++ scrollTo(ctx, { ++ animated: false, ++ index, ++ itemSize: getItemSizeAtIndex(ctx, index), ++ offset: position, ++ viewOffset: settle.viewOffset, ++ viewPosition: settle.viewPosition ++ }); ++} +function settleScrollTarget(ctx, isCompensating) { + const state = ctx.state; + const settle = state.scrollTargetSettle; @@ -4350,14 +4374,7 @@ index 914d2da..e11dd28 100644 + settle.quietPasses = 0; + settle.corrections++; + settle.expiresAt = now + SETTLE_TTL_MS; -+ scrollTo(ctx, { -+ animated: false, -+ index, -+ itemSize: getItemSizeAtIndex(ctx, index), -+ offset: position, -+ viewOffset: settle.viewOffset, -+ viewPosition: settle.viewPosition -+ }); ++ state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} + @@ -4553,76 +4570,112 @@ index 914d2da..e11dd28 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4367,7 +4462,10 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4367,7 +4482,13 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); - const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); -+ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx, didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0); -+ const didMVCPAdjustScroll = didMVCPAdjust || didSettleScrollTarget; ++ const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; ++ if (!suppressInitialScrollSideEffects) { ++ settleScrollTarget(ctx, isCompensating); ++ } ++ const didMVCPAdjustScroll = didMVCPAdjust; if (didMVCPAdjustScroll) { updateScroll2(state.scroll); updateScrollRange(); -@@ -6132,6 +6230,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - return (_a4 = scrollRef.current) == null ? void 0 : _a4.getBoundingClientRect(); - }, - getCurrentScrollOffset, -+ getMaxScrollOffset, - getScrollableNode: () => resolveScrollableNode(scrollRef.current, isWindowScroll), - getScrollEventTarget: () => getScrollTarget(), - getScrollResponder: () => resolveScrollableNode(scrollRef.current, isWindowScroll), -@@ -7205,6 +7304,22 @@ function createColumnWrapperStyle(contentContainerStyle) { - } - } - -+// src/core/isScrollExtentSynced.ts -+var SCROLL_EXTENT_EPSILON = 1; -+function isScrollExtentSynced(ctx) { -+ var _a3, _b; -+ const state = ctx.state; -+ const platformMaxOffset = (_b = (_a3 = state.refScroller.current) == null ? void 0 : _a3.getMaxScrollOffset) == null ? void 0 : _b.call(_a3); -+ if (platformMaxOffset === void 0) { -+ return true; -+ } -+ if (state.scrollLength <= 0) { -+ return true; -+ } -+ const contentMaxOffset = Math.max(0, getContentSize(ctx) - state.scrollLength); -+ return platformMaxOffset >= contentMaxOffset - SCROLL_EXTENT_EPSILON; -+} -+ - // src/core/scrollToEnd.ts - function scrollToEnd(ctx, options) { - const state = ctx.state; -@@ -7250,6 +7365,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { - const IMPERATIVE_SCROLL_SETTLE_STABLE_FRAMES = 2; - let imperativeScrollToken = 0; - const isSettlingAfterDataChange = () => !!state.didDataChange || !!state.didColumnsChange || state.scheduledWork.has("mvcpRecalculate") || state.ignoreScrollFromMVCP !== void 0; -+ const isScrollBlocked = () => isSettlingAfterDataChange() || !isScrollExtentSynced(ctx); - const isScrollToIndexReady = (targetIndex, allowEmpty = false) => { - var _a3; - const props = state.props; -@@ -7273,7 +7389,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { - if (token !== imperativeScrollToken) { +@@ -6006,6 +6127,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll + // src/components/ListComponentScrollView.tsx + var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; + var SCROLL_END_FALLBACK_MS = 200; ++var SCROLL_CLAMP_EPSILON = 1; ++var SCROLL_CLAMP_RETRY_FRAMES = 10; + var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; + function ensureScrollbarHiddenStyle() { + if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { +@@ -6094,36 +6217,59 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + } + return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; + }, [getMaxScrollOffset, horizontal, isWindowScroll]); ++ const scrollRetryToken = React3.useRef(0); + const scrollToLocalOffset = React3.useCallback( + (offset, animated) => { +- const scrollElement = scrollRef.current; + const target = getScrollTarget(); + if (!target || typeof target.scrollTo !== "function") { return; } -- if (isSettlingAfterDataChange() || !isReady()) { -+ if (isScrollBlocked() || !isReady()) { - stableFrames = 0; - } else { - stableFrames += 1; -@@ -7300,7 +7416,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { - resolve(); - } - }; -- if (isSettlingAfterDataChange() || !isReady()) { -+ if (isScrollBlocked() || !isReady()) { - runWhenReady(token, runNow, isReady); - } else { - runNow(); -@@ -8288,6 +8404,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +- const maxOffset = getMaxScrollOffset(); +- const clampedOffset = clampOffset(offset, maxOffset); + const behavior = animated ? "smooth" : "auto"; +- const options = { behavior }; +- if (isWindowScroll) { +- const scroll = getWindowScrollPosition(); +- const listPos = getElementDocumentPosition(scrollElement, scroll); +- const { left, top } = resolveWindowScrollTarget({ +- clampedOffset, +- horizontal, +- listPos, +- scroll ++ const token = ++scrollRetryToken.current; ++ const apply = (attempt) => { ++ if (token !== scrollRetryToken.current) { ++ return; ++ } ++ const scrollElement = scrollRef.current; ++ const maxOffset = getMaxScrollOffset(); ++ const clampedOffset = clampOffset(offset, maxOffset); ++ const options = { behavior }; ++ if (isWindowScroll) { ++ const scroll = getWindowScrollPosition(); ++ const listPos = getElementDocumentPosition(scrollElement, scroll); ++ const { left, top } = resolveWindowScrollTarget({ ++ clampedOffset, ++ horizontal, ++ listPos, ++ scroll ++ }); ++ options.left = left; ++ options.top = top; ++ } else if (horizontal) { ++ options.left = clampedOffset; ++ } else { ++ options.top = clampedOffset; ++ } ++ target.scrollTo(options); ++ if (animated || clampedOffset >= offset - SCROLL_CLAMP_EPSILON) { ++ return; ++ } ++ if (attempt >= SCROLL_CLAMP_RETRY_FRAMES) { ++ return; ++ } ++ requestAnimationFrame(() => { ++ if (token !== scrollRetryToken.current) { ++ return; ++ } ++ if (Math.abs(getCurrentScrollOffset() - clampedOffset) > SCROLL_CLAMP_EPSILON) { ++ return; ++ } ++ apply(attempt + 1); + }); +- options.left = left; +- options.top = top; +- } else if (horizontal) { +- options.left = clampedOffset; +- } else { +- options.top = clampedOffset; +- } +- target.scrollTo(options); ++ }; ++ apply(0); + }, +- [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] ++ [getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] + ); + React3.useImperativeHandle(ref, () => { + const api = { +@@ -8288,6 +8434,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -4631,7 +4684,7 @@ index 914d2da..e11dd28 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs -index 95465f2..a3e870c 100644 +index 95465f2..d2ebd71 100644 --- a/node_modules/@legendapp/list/react-native.web.mjs +++ b/node_modules/@legendapp/list/react-native.web.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -5560,14 +5613,10 @@ index 95465f2..a3e870c 100644 } // src/core/finishScrollTo.ts -@@ -1401,7 +1017,182 @@ function listenForScrollEnd(ctx, params) { - } else { - idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); - } -- scheduledWork.register("platformScrollCompletion", cancel); -+ scheduledWork.register("platformScrollCompletion", cancel); -+} -+ +@@ -1404,6 +1020,181 @@ function listenForScrollEnd(ctx, params) { + scheduledWork.register("platformScrollCompletion", cancel); + } + +// src/core/initialScrollSession.ts +var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +function hasInitialScrollSessionCompletion(completion) { @@ -5741,10 +5790,20 @@ index 95465f2..a3e870c 100644 + resetAdaptiveRender(ctx); + } + } - } - ++} ++ // src/core/doMaintainScrollAtEnd.ts -@@ -1788,6 +1579,27 @@ function prepareMVCP(ctx, dataChanged) { + function doMaintainScrollAtEnd(ctx) { + const state = ctx.state; +@@ -1435,6 +1226,7 @@ function doMaintainScrollAtEnd(ctx) { + const didScrollSinceRequest = state.scroll !== scrollAtRequest; + if (isStillWithinThreshold || !didScrollSinceRequest) { + state.maintainingScrollAtEnd = activeState; ++ clearScrollTargetSettle(state); + const scroller = refScroller.current; + if (state.props.horizontal && isHorizontalRTL(state)) { + const currentContentSize = getContentSize(ctx); +@@ -1788,6 +1580,27 @@ function prepareMVCP(ctx, dataChanged) { } } @@ -5772,7 +5831,25 @@ index 95465f2..a3e870c 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2048,7 +1860,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -1850,6 +1663,7 @@ function isInMVCPActiveMode(state) { + } + + // src/core/updateScroll.ts ++var SETTLE_RELEASE_EPSILON = 2; + function updateScroll(ctx, newScroll, forceUpdate, options) { + var _a3; + const state = ctx.state; +@@ -1885,6 +1699,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { + state.scrollPrevTime = state.scrollTime; + state.scroll = newScroll; + state.scrollTime = currentTime; ++ if (state.scrollTargetSettle && (options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust && Math.abs(newScroll - state.scrollPending) > SETTLE_RELEASE_EPSILON) { ++ clearScrollTargetSettle(state); ++ } + const scrollDelta = Math.abs(newScroll - prevScroll); + const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; + const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; +@@ -2048,7 +1865,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } function scrollTo(ctx, params) { @@ -5781,11 +5858,11 @@ index 95465f2..a3e870c 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2080,6 +1892,15 @@ function scrollTo(ctx, params) { +@@ -2080,6 +1897,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); -+ if (scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { ++ if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { + beginScrollTargetSettle(ctx, { + index: scrollTarget.index, + viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, @@ -5797,7 +5874,7 @@ index 95465f2..a3e870c 100644 } } state.scrollPending = targetOffset; -@@ -2087,7 +1908,7 @@ function scrollTo(ctx, params) { +@@ -2087,7 +1913,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -5806,7 +5883,7 @@ index 95465f2..a3e870c 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2100,6 +1921,280 @@ function scrollTo(ctx, params) { +@@ -2100,6 +1926,295 @@ function scrollTo(ctx, params) { } } @@ -5818,6 +5895,7 @@ index 95465f2..a3e870c 100644 +var SETTLE_MAX_CORRECTIONS = 8; +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; ++ state.scheduledWork.cancel("scrollTargetSettle"); +} +function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; @@ -5846,6 +5924,27 @@ index 95465f2..a3e870c 100644 + const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; + return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} ++function applyScrollTargetCorrection(ctx, id) { ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle || settle.id !== id) { ++ return; ++ } ++ const index = state.indexByKey.get(id); ++ const position = index === void 0 ? void 0 : state.positions[index]; ++ if (index === void 0 || position === void 0 || Date.now() > settle.deadline) { ++ clearScrollTargetSettle(state); ++ return; ++ } ++ scrollTo(ctx, { ++ animated: false, ++ index, ++ itemSize: getItemSizeAtIndex(ctx, index), ++ offset: position, ++ viewOffset: settle.viewOffset, ++ viewPosition: settle.viewPosition ++ }); ++} +function settleScrollTarget(ctx, isCompensating) { + const state = ctx.state; + const settle = state.scrollTargetSettle; @@ -5884,14 +5983,7 @@ index 95465f2..a3e870c 100644 + settle.quietPasses = 0; + settle.corrections++; + settle.expiresAt = now + SETTLE_TTL_MS; -+ scrollTo(ctx, { -+ animated: false, -+ index, -+ itemSize: getItemSizeAtIndex(ctx, index), -+ offset: position, -+ viewOffset: settle.viewOffset, -+ viewPosition: settle.viewPosition -+ }); ++ state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} + @@ -6087,76 +6179,112 @@ index 95465f2..a3e870c 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4346,7 +4441,10 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4346,7 +4461,13 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); - const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); -+ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx, didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0); -+ const didMVCPAdjustScroll = didMVCPAdjust || didSettleScrollTarget; ++ const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; ++ if (!suppressInitialScrollSideEffects) { ++ settleScrollTarget(ctx, isCompensating); ++ } ++ const didMVCPAdjustScroll = didMVCPAdjust; if (didMVCPAdjustScroll) { updateScroll2(state.scroll); updateScrollRange(); -@@ -6111,6 +6209,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - return (_a4 = scrollRef.current) == null ? void 0 : _a4.getBoundingClientRect(); - }, - getCurrentScrollOffset, -+ getMaxScrollOffset, - getScrollableNode: () => resolveScrollableNode(scrollRef.current, isWindowScroll), - getScrollEventTarget: () => getScrollTarget(), - getScrollResponder: () => resolveScrollableNode(scrollRef.current, isWindowScroll), -@@ -7184,6 +7283,22 @@ function createColumnWrapperStyle(contentContainerStyle) { - } - } - -+// src/core/isScrollExtentSynced.ts -+var SCROLL_EXTENT_EPSILON = 1; -+function isScrollExtentSynced(ctx) { -+ var _a3, _b; -+ const state = ctx.state; -+ const platformMaxOffset = (_b = (_a3 = state.refScroller.current) == null ? void 0 : _a3.getMaxScrollOffset) == null ? void 0 : _b.call(_a3); -+ if (platformMaxOffset === void 0) { -+ return true; -+ } -+ if (state.scrollLength <= 0) { -+ return true; -+ } -+ const contentMaxOffset = Math.max(0, getContentSize(ctx) - state.scrollLength); -+ return platformMaxOffset >= contentMaxOffset - SCROLL_EXTENT_EPSILON; -+} -+ - // src/core/scrollToEnd.ts - function scrollToEnd(ctx, options) { - const state = ctx.state; -@@ -7229,6 +7344,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { - const IMPERATIVE_SCROLL_SETTLE_STABLE_FRAMES = 2; - let imperativeScrollToken = 0; - const isSettlingAfterDataChange = () => !!state.didDataChange || !!state.didColumnsChange || state.scheduledWork.has("mvcpRecalculate") || state.ignoreScrollFromMVCP !== void 0; -+ const isScrollBlocked = () => isSettlingAfterDataChange() || !isScrollExtentSynced(ctx); - const isScrollToIndexReady = (targetIndex, allowEmpty = false) => { - var _a3; - const props = state.props; -@@ -7252,7 +7368,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { - if (token !== imperativeScrollToken) { +@@ -5985,6 +6106,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll + // src/components/ListComponentScrollView.tsx + var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; + var SCROLL_END_FALLBACK_MS = 200; ++var SCROLL_CLAMP_EPSILON = 1; ++var SCROLL_CLAMP_RETRY_FRAMES = 10; + var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; + function ensureScrollbarHiddenStyle() { + if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { +@@ -6073,36 +6196,59 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + } + return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; + }, [getMaxScrollOffset, horizontal, isWindowScroll]); ++ const scrollRetryToken = useRef(0); + const scrollToLocalOffset = useCallback( + (offset, animated) => { +- const scrollElement = scrollRef.current; + const target = getScrollTarget(); + if (!target || typeof target.scrollTo !== "function") { return; } -- if (isSettlingAfterDataChange() || !isReady()) { -+ if (isScrollBlocked() || !isReady()) { - stableFrames = 0; - } else { - stableFrames += 1; -@@ -7279,7 +7395,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { - resolve(); - } - }; -- if (isSettlingAfterDataChange() || !isReady()) { -+ if (isScrollBlocked() || !isReady()) { - runWhenReady(token, runNow, isReady); - } else { - runNow(); -@@ -8267,6 +8383,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +- const maxOffset = getMaxScrollOffset(); +- const clampedOffset = clampOffset(offset, maxOffset); + const behavior = animated ? "smooth" : "auto"; +- const options = { behavior }; +- if (isWindowScroll) { +- const scroll = getWindowScrollPosition(); +- const listPos = getElementDocumentPosition(scrollElement, scroll); +- const { left, top } = resolveWindowScrollTarget({ +- clampedOffset, +- horizontal, +- listPos, +- scroll ++ const token = ++scrollRetryToken.current; ++ const apply = (attempt) => { ++ if (token !== scrollRetryToken.current) { ++ return; ++ } ++ const scrollElement = scrollRef.current; ++ const maxOffset = getMaxScrollOffset(); ++ const clampedOffset = clampOffset(offset, maxOffset); ++ const options = { behavior }; ++ if (isWindowScroll) { ++ const scroll = getWindowScrollPosition(); ++ const listPos = getElementDocumentPosition(scrollElement, scroll); ++ const { left, top } = resolveWindowScrollTarget({ ++ clampedOffset, ++ horizontal, ++ listPos, ++ scroll ++ }); ++ options.left = left; ++ options.top = top; ++ } else if (horizontal) { ++ options.left = clampedOffset; ++ } else { ++ options.top = clampedOffset; ++ } ++ target.scrollTo(options); ++ if (animated || clampedOffset >= offset - SCROLL_CLAMP_EPSILON) { ++ return; ++ } ++ if (attempt >= SCROLL_CLAMP_RETRY_FRAMES) { ++ return; ++ } ++ requestAnimationFrame(() => { ++ if (token !== scrollRetryToken.current) { ++ return; ++ } ++ if (Math.abs(getCurrentScrollOffset() - clampedOffset) > SCROLL_CLAMP_EPSILON) { ++ return; ++ } ++ apply(attempt + 1); + }); +- options.left = left; +- options.top = top; +- } else if (horizontal) { +- options.left = clampedOffset; +- } else { +- options.top = clampedOffset; +- } +- target.scrollTo(options); ++ }; ++ apply(0); + }, +- [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] ++ [getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] + ); + useImperativeHandle(ref, () => { + const api = { +@@ -8267,6 +8413,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -6164,20 +6292,8 @@ index 95465f2..a3e870c 100644 (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) -diff --git a/node_modules/@legendapp/list/react.d.ts b/node_modules/@legendapp/list/react.d.ts -index b6c7481..0ec3d1e 100644 ---- a/node_modules/@legendapp/list/react.d.ts -+++ b/node_modules/@legendapp/list/react.d.ts -@@ -6,6 +6,7 @@ type ScrollEventTarget = Window | HTMLElement; - interface ScrollViewMethods { - getBoundingClientRect(): DOMRect | null | undefined; - getCurrentScrollOffset(): number; -+ getMaxScrollOffset(): number; - getScrollableNode(): HTMLElement; - getScrollEventTarget(): ScrollEventTarget | null; - getScrollResponder(): HTMLElement | null; diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js -index 914d2da..e11dd28 100644 +index 914d2da..da09324 100644 --- a/node_modules/@legendapp/list/react.js +++ b/node_modules/@legendapp/list/react.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -7106,14 +7222,10 @@ index 914d2da..e11dd28 100644 } // src/core/finishScrollTo.ts -@@ -1422,7 +1038,182 @@ function listenForScrollEnd(ctx, params) { - } else { - idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); - } -- scheduledWork.register("platformScrollCompletion", cancel); -+ scheduledWork.register("platformScrollCompletion", cancel); -+} -+ +@@ -1425,6 +1041,181 @@ function listenForScrollEnd(ctx, params) { + scheduledWork.register("platformScrollCompletion", cancel); + } + +// src/core/initialScrollSession.ts +var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +function hasInitialScrollSessionCompletion(completion) { @@ -7287,10 +7399,20 @@ index 914d2da..e11dd28 100644 + resetAdaptiveRender(ctx); + } + } - } - ++} ++ // src/core/doMaintainScrollAtEnd.ts -@@ -1809,6 +1600,27 @@ function prepareMVCP(ctx, dataChanged) { + function doMaintainScrollAtEnd(ctx) { + const state = ctx.state; +@@ -1456,6 +1247,7 @@ function doMaintainScrollAtEnd(ctx) { + const didScrollSinceRequest = state.scroll !== scrollAtRequest; + if (isStillWithinThreshold || !didScrollSinceRequest) { + state.maintainingScrollAtEnd = activeState; ++ clearScrollTargetSettle(state); + const scroller = refScroller.current; + if (state.props.horizontal && isHorizontalRTL(state)) { + const currentContentSize = getContentSize(ctx); +@@ -1809,6 +1601,27 @@ function prepareMVCP(ctx, dataChanged) { } } @@ -7318,7 +7440,25 @@ index 914d2da..e11dd28 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2069,7 +1881,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -1871,6 +1684,7 @@ function isInMVCPActiveMode(state) { + } + + // src/core/updateScroll.ts ++var SETTLE_RELEASE_EPSILON = 2; + function updateScroll(ctx, newScroll, forceUpdate, options) { + var _a3; + const state = ctx.state; +@@ -1906,6 +1720,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { + state.scrollPrevTime = state.scrollTime; + state.scroll = newScroll; + state.scrollTime = currentTime; ++ if (state.scrollTargetSettle && (options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust && Math.abs(newScroll - state.scrollPending) > SETTLE_RELEASE_EPSILON) { ++ clearScrollTargetSettle(state); ++ } + const scrollDelta = Math.abs(newScroll - prevScroll); + const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; + const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; +@@ -2069,7 +1886,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } function scrollTo(ctx, params) { @@ -7327,11 +7467,11 @@ index 914d2da..e11dd28 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2101,6 +1913,15 @@ function scrollTo(ctx, params) { +@@ -2101,6 +1918,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); -+ if (scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { ++ if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { + beginScrollTargetSettle(ctx, { + index: scrollTarget.index, + viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, @@ -7343,7 +7483,7 @@ index 914d2da..e11dd28 100644 } } state.scrollPending = targetOffset; -@@ -2108,7 +1929,7 @@ function scrollTo(ctx, params) { +@@ -2108,7 +1934,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -7352,7 +7492,7 @@ index 914d2da..e11dd28 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2121,6 +1942,280 @@ function scrollTo(ctx, params) { +@@ -2121,6 +1947,295 @@ function scrollTo(ctx, params) { } } @@ -7364,6 +7504,7 @@ index 914d2da..e11dd28 100644 +var SETTLE_MAX_CORRECTIONS = 8; +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; ++ state.scheduledWork.cancel("scrollTargetSettle"); +} +function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; @@ -7392,6 +7533,27 @@ index 914d2da..e11dd28 100644 + const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; + return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} ++function applyScrollTargetCorrection(ctx, id) { ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle || settle.id !== id) { ++ return; ++ } ++ const index = state.indexByKey.get(id); ++ const position = index === void 0 ? void 0 : state.positions[index]; ++ if (index === void 0 || position === void 0 || Date.now() > settle.deadline) { ++ clearScrollTargetSettle(state); ++ return; ++ } ++ scrollTo(ctx, { ++ animated: false, ++ index, ++ itemSize: getItemSizeAtIndex(ctx, index), ++ offset: position, ++ viewOffset: settle.viewOffset, ++ viewPosition: settle.viewPosition ++ }); ++} +function settleScrollTarget(ctx, isCompensating) { + const state = ctx.state; + const settle = state.scrollTargetSettle; @@ -7430,14 +7592,7 @@ index 914d2da..e11dd28 100644 + settle.quietPasses = 0; + settle.corrections++; + settle.expiresAt = now + SETTLE_TTL_MS; -+ scrollTo(ctx, { -+ animated: false, -+ index, -+ itemSize: getItemSizeAtIndex(ctx, index), -+ offset: position, -+ viewOffset: settle.viewOffset, -+ viewPosition: settle.viewPosition -+ }); ++ state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} + @@ -7633,76 +7788,112 @@ index 914d2da..e11dd28 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4367,7 +4462,10 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4367,7 +4482,13 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); - const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); -+ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx, didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0); -+ const didMVCPAdjustScroll = didMVCPAdjust || didSettleScrollTarget; ++ const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; ++ if (!suppressInitialScrollSideEffects) { ++ settleScrollTarget(ctx, isCompensating); ++ } ++ const didMVCPAdjustScroll = didMVCPAdjust; if (didMVCPAdjustScroll) { updateScroll2(state.scroll); updateScrollRange(); -@@ -6132,6 +6230,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - return (_a4 = scrollRef.current) == null ? void 0 : _a4.getBoundingClientRect(); - }, - getCurrentScrollOffset, -+ getMaxScrollOffset, - getScrollableNode: () => resolveScrollableNode(scrollRef.current, isWindowScroll), - getScrollEventTarget: () => getScrollTarget(), - getScrollResponder: () => resolveScrollableNode(scrollRef.current, isWindowScroll), -@@ -7205,6 +7304,22 @@ function createColumnWrapperStyle(contentContainerStyle) { - } - } - -+// src/core/isScrollExtentSynced.ts -+var SCROLL_EXTENT_EPSILON = 1; -+function isScrollExtentSynced(ctx) { -+ var _a3, _b; -+ const state = ctx.state; -+ const platformMaxOffset = (_b = (_a3 = state.refScroller.current) == null ? void 0 : _a3.getMaxScrollOffset) == null ? void 0 : _b.call(_a3); -+ if (platformMaxOffset === void 0) { -+ return true; -+ } -+ if (state.scrollLength <= 0) { -+ return true; -+ } -+ const contentMaxOffset = Math.max(0, getContentSize(ctx) - state.scrollLength); -+ return platformMaxOffset >= contentMaxOffset - SCROLL_EXTENT_EPSILON; -+} -+ - // src/core/scrollToEnd.ts - function scrollToEnd(ctx, options) { - const state = ctx.state; -@@ -7250,6 +7365,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { - const IMPERATIVE_SCROLL_SETTLE_STABLE_FRAMES = 2; - let imperativeScrollToken = 0; - const isSettlingAfterDataChange = () => !!state.didDataChange || !!state.didColumnsChange || state.scheduledWork.has("mvcpRecalculate") || state.ignoreScrollFromMVCP !== void 0; -+ const isScrollBlocked = () => isSettlingAfterDataChange() || !isScrollExtentSynced(ctx); - const isScrollToIndexReady = (targetIndex, allowEmpty = false) => { - var _a3; - const props = state.props; -@@ -7273,7 +7389,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { - if (token !== imperativeScrollToken) { +@@ -6006,6 +6127,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll + // src/components/ListComponentScrollView.tsx + var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; + var SCROLL_END_FALLBACK_MS = 200; ++var SCROLL_CLAMP_EPSILON = 1; ++var SCROLL_CLAMP_RETRY_FRAMES = 10; + var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; + function ensureScrollbarHiddenStyle() { + if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { +@@ -6094,36 +6217,59 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + } + return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; + }, [getMaxScrollOffset, horizontal, isWindowScroll]); ++ const scrollRetryToken = React3.useRef(0); + const scrollToLocalOffset = React3.useCallback( + (offset, animated) => { +- const scrollElement = scrollRef.current; + const target = getScrollTarget(); + if (!target || typeof target.scrollTo !== "function") { return; } -- if (isSettlingAfterDataChange() || !isReady()) { -+ if (isScrollBlocked() || !isReady()) { - stableFrames = 0; - } else { - stableFrames += 1; -@@ -7300,7 +7416,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { - resolve(); - } - }; -- if (isSettlingAfterDataChange() || !isReady()) { -+ if (isScrollBlocked() || !isReady()) { - runWhenReady(token, runNow, isReady); - } else { - runNow(); -@@ -8288,6 +8404,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +- const maxOffset = getMaxScrollOffset(); +- const clampedOffset = clampOffset(offset, maxOffset); + const behavior = animated ? "smooth" : "auto"; +- const options = { behavior }; +- if (isWindowScroll) { +- const scroll = getWindowScrollPosition(); +- const listPos = getElementDocumentPosition(scrollElement, scroll); +- const { left, top } = resolveWindowScrollTarget({ +- clampedOffset, +- horizontal, +- listPos, +- scroll ++ const token = ++scrollRetryToken.current; ++ const apply = (attempt) => { ++ if (token !== scrollRetryToken.current) { ++ return; ++ } ++ const scrollElement = scrollRef.current; ++ const maxOffset = getMaxScrollOffset(); ++ const clampedOffset = clampOffset(offset, maxOffset); ++ const options = { behavior }; ++ if (isWindowScroll) { ++ const scroll = getWindowScrollPosition(); ++ const listPos = getElementDocumentPosition(scrollElement, scroll); ++ const { left, top } = resolveWindowScrollTarget({ ++ clampedOffset, ++ horizontal, ++ listPos, ++ scroll ++ }); ++ options.left = left; ++ options.top = top; ++ } else if (horizontal) { ++ options.left = clampedOffset; ++ } else { ++ options.top = clampedOffset; ++ } ++ target.scrollTo(options); ++ if (animated || clampedOffset >= offset - SCROLL_CLAMP_EPSILON) { ++ return; ++ } ++ if (attempt >= SCROLL_CLAMP_RETRY_FRAMES) { ++ return; ++ } ++ requestAnimationFrame(() => { ++ if (token !== scrollRetryToken.current) { ++ return; ++ } ++ if (Math.abs(getCurrentScrollOffset() - clampedOffset) > SCROLL_CLAMP_EPSILON) { ++ return; ++ } ++ apply(attempt + 1); + }); +- options.left = left; +- options.top = top; +- } else if (horizontal) { +- options.left = clampedOffset; +- } else { +- options.top = clampedOffset; +- } +- target.scrollTo(options); ++ }; ++ apply(0); + }, +- [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] ++ [getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] + ); + React3.useImperativeHandle(ref, () => { + const api = { +@@ -8288,6 +8434,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -7711,7 +7902,7 @@ index 914d2da..e11dd28 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs -index 95465f2..a3e870c 100644 +index 95465f2..d2ebd71 100644 --- a/node_modules/@legendapp/list/react.mjs +++ b/node_modules/@legendapp/list/react.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -8640,14 +8831,10 @@ index 95465f2..a3e870c 100644 } // src/core/finishScrollTo.ts -@@ -1401,7 +1017,182 @@ function listenForScrollEnd(ctx, params) { - } else { - idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); - } -- scheduledWork.register("platformScrollCompletion", cancel); -+ scheduledWork.register("platformScrollCompletion", cancel); -+} -+ +@@ -1404,6 +1020,181 @@ function listenForScrollEnd(ctx, params) { + scheduledWork.register("platformScrollCompletion", cancel); + } + +// src/core/initialScrollSession.ts +var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +function hasInitialScrollSessionCompletion(completion) { @@ -8821,10 +9008,20 @@ index 95465f2..a3e870c 100644 + resetAdaptiveRender(ctx); + } + } - } - ++} ++ // src/core/doMaintainScrollAtEnd.ts -@@ -1788,6 +1579,27 @@ function prepareMVCP(ctx, dataChanged) { + function doMaintainScrollAtEnd(ctx) { + const state = ctx.state; +@@ -1435,6 +1226,7 @@ function doMaintainScrollAtEnd(ctx) { + const didScrollSinceRequest = state.scroll !== scrollAtRequest; + if (isStillWithinThreshold || !didScrollSinceRequest) { + state.maintainingScrollAtEnd = activeState; ++ clearScrollTargetSettle(state); + const scroller = refScroller.current; + if (state.props.horizontal && isHorizontalRTL(state)) { + const currentContentSize = getContentSize(ctx); +@@ -1788,6 +1580,27 @@ function prepareMVCP(ctx, dataChanged) { } } @@ -8852,7 +9049,25 @@ index 95465f2..a3e870c 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2048,7 +1860,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -1850,6 +1663,7 @@ function isInMVCPActiveMode(state) { + } + + // src/core/updateScroll.ts ++var SETTLE_RELEASE_EPSILON = 2; + function updateScroll(ctx, newScroll, forceUpdate, options) { + var _a3; + const state = ctx.state; +@@ -1885,6 +1699,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { + state.scrollPrevTime = state.scrollTime; + state.scroll = newScroll; + state.scrollTime = currentTime; ++ if (state.scrollTargetSettle && (options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust && Math.abs(newScroll - state.scrollPending) > SETTLE_RELEASE_EPSILON) { ++ clearScrollTargetSettle(state); ++ } + const scrollDelta = Math.abs(newScroll - prevScroll); + const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; + const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; +@@ -2048,7 +1865,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } function scrollTo(ctx, params) { @@ -8861,11 +9076,11 @@ index 95465f2..a3e870c 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2080,6 +1892,15 @@ function scrollTo(ctx, params) { +@@ -2080,6 +1897,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); -+ if (scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { ++ if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { + beginScrollTargetSettle(ctx, { + index: scrollTarget.index, + viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, @@ -8877,7 +9092,7 @@ index 95465f2..a3e870c 100644 } } state.scrollPending = targetOffset; -@@ -2087,7 +1908,7 @@ function scrollTo(ctx, params) { +@@ -2087,7 +1913,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -8886,7 +9101,7 @@ index 95465f2..a3e870c 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2100,6 +1921,280 @@ function scrollTo(ctx, params) { +@@ -2100,6 +1926,295 @@ function scrollTo(ctx, params) { } } @@ -8898,6 +9113,7 @@ index 95465f2..a3e870c 100644 +var SETTLE_MAX_CORRECTIONS = 8; +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; ++ state.scheduledWork.cancel("scrollTargetSettle"); +} +function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; @@ -8926,6 +9142,27 @@ index 95465f2..a3e870c 100644 + const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; + return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} ++function applyScrollTargetCorrection(ctx, id) { ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle || settle.id !== id) { ++ return; ++ } ++ const index = state.indexByKey.get(id); ++ const position = index === void 0 ? void 0 : state.positions[index]; ++ if (index === void 0 || position === void 0 || Date.now() > settle.deadline) { ++ clearScrollTargetSettle(state); ++ return; ++ } ++ scrollTo(ctx, { ++ animated: false, ++ index, ++ itemSize: getItemSizeAtIndex(ctx, index), ++ offset: position, ++ viewOffset: settle.viewOffset, ++ viewPosition: settle.viewPosition ++ }); ++} +function settleScrollTarget(ctx, isCompensating) { + const state = ctx.state; + const settle = state.scrollTargetSettle; @@ -8964,14 +9201,7 @@ index 95465f2..a3e870c 100644 + settle.quietPasses = 0; + settle.corrections++; + settle.expiresAt = now + SETTLE_TTL_MS; -+ scrollTo(ctx, { -+ animated: false, -+ index, -+ itemSize: getItemSizeAtIndex(ctx, index), -+ offset: position, -+ viewOffset: settle.viewOffset, -+ viewPosition: settle.viewPosition -+ }); ++ state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} + @@ -9167,76 +9397,112 @@ index 95465f2..a3e870c 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4346,7 +4441,10 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4346,7 +4461,13 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); - const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); -+ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx, didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0); -+ const didMVCPAdjustScroll = didMVCPAdjust || didSettleScrollTarget; ++ const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; ++ if (!suppressInitialScrollSideEffects) { ++ settleScrollTarget(ctx, isCompensating); ++ } ++ const didMVCPAdjustScroll = didMVCPAdjust; if (didMVCPAdjustScroll) { updateScroll2(state.scroll); updateScrollRange(); -@@ -6111,6 +6209,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - return (_a4 = scrollRef.current) == null ? void 0 : _a4.getBoundingClientRect(); - }, - getCurrentScrollOffset, -+ getMaxScrollOffset, - getScrollableNode: () => resolveScrollableNode(scrollRef.current, isWindowScroll), - getScrollEventTarget: () => getScrollTarget(), - getScrollResponder: () => resolveScrollableNode(scrollRef.current, isWindowScroll), -@@ -7184,6 +7283,22 @@ function createColumnWrapperStyle(contentContainerStyle) { - } - } - -+// src/core/isScrollExtentSynced.ts -+var SCROLL_EXTENT_EPSILON = 1; -+function isScrollExtentSynced(ctx) { -+ var _a3, _b; -+ const state = ctx.state; -+ const platformMaxOffset = (_b = (_a3 = state.refScroller.current) == null ? void 0 : _a3.getMaxScrollOffset) == null ? void 0 : _b.call(_a3); -+ if (platformMaxOffset === void 0) { -+ return true; -+ } -+ if (state.scrollLength <= 0) { -+ return true; -+ } -+ const contentMaxOffset = Math.max(0, getContentSize(ctx) - state.scrollLength); -+ return platformMaxOffset >= contentMaxOffset - SCROLL_EXTENT_EPSILON; -+} -+ - // src/core/scrollToEnd.ts - function scrollToEnd(ctx, options) { - const state = ctx.state; -@@ -7229,6 +7344,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { - const IMPERATIVE_SCROLL_SETTLE_STABLE_FRAMES = 2; - let imperativeScrollToken = 0; - const isSettlingAfterDataChange = () => !!state.didDataChange || !!state.didColumnsChange || state.scheduledWork.has("mvcpRecalculate") || state.ignoreScrollFromMVCP !== void 0; -+ const isScrollBlocked = () => isSettlingAfterDataChange() || !isScrollExtentSynced(ctx); - const isScrollToIndexReady = (targetIndex, allowEmpty = false) => { - var _a3; - const props = state.props; -@@ -7252,7 +7368,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { - if (token !== imperativeScrollToken) { +@@ -5985,6 +6106,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll + // src/components/ListComponentScrollView.tsx + var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; + var SCROLL_END_FALLBACK_MS = 200; ++var SCROLL_CLAMP_EPSILON = 1; ++var SCROLL_CLAMP_RETRY_FRAMES = 10; + var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; + function ensureScrollbarHiddenStyle() { + if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { +@@ -6073,36 +6196,59 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + } + return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; + }, [getMaxScrollOffset, horizontal, isWindowScroll]); ++ const scrollRetryToken = useRef(0); + const scrollToLocalOffset = useCallback( + (offset, animated) => { +- const scrollElement = scrollRef.current; + const target = getScrollTarget(); + if (!target || typeof target.scrollTo !== "function") { return; } -- if (isSettlingAfterDataChange() || !isReady()) { -+ if (isScrollBlocked() || !isReady()) { - stableFrames = 0; - } else { - stableFrames += 1; -@@ -7279,7 +7395,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { - resolve(); - } - }; -- if (isSettlingAfterDataChange() || !isReady()) { -+ if (isScrollBlocked() || !isReady()) { - runWhenReady(token, runNow, isReady); - } else { - runNow(); -@@ -8267,6 +8383,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +- const maxOffset = getMaxScrollOffset(); +- const clampedOffset = clampOffset(offset, maxOffset); + const behavior = animated ? "smooth" : "auto"; +- const options = { behavior }; +- if (isWindowScroll) { +- const scroll = getWindowScrollPosition(); +- const listPos = getElementDocumentPosition(scrollElement, scroll); +- const { left, top } = resolveWindowScrollTarget({ +- clampedOffset, +- horizontal, +- listPos, +- scroll ++ const token = ++scrollRetryToken.current; ++ const apply = (attempt) => { ++ if (token !== scrollRetryToken.current) { ++ return; ++ } ++ const scrollElement = scrollRef.current; ++ const maxOffset = getMaxScrollOffset(); ++ const clampedOffset = clampOffset(offset, maxOffset); ++ const options = { behavior }; ++ if (isWindowScroll) { ++ const scroll = getWindowScrollPosition(); ++ const listPos = getElementDocumentPosition(scrollElement, scroll); ++ const { left, top } = resolveWindowScrollTarget({ ++ clampedOffset, ++ horizontal, ++ listPos, ++ scroll ++ }); ++ options.left = left; ++ options.top = top; ++ } else if (horizontal) { ++ options.left = clampedOffset; ++ } else { ++ options.top = clampedOffset; ++ } ++ target.scrollTo(options); ++ if (animated || clampedOffset >= offset - SCROLL_CLAMP_EPSILON) { ++ return; ++ } ++ if (attempt >= SCROLL_CLAMP_RETRY_FRAMES) { ++ return; ++ } ++ requestAnimationFrame(() => { ++ if (token !== scrollRetryToken.current) { ++ return; ++ } ++ if (Math.abs(getCurrentScrollOffset() - clampedOffset) > SCROLL_CLAMP_EPSILON) { ++ return; ++ } ++ apply(attempt + 1); + }); +- options.left = left; +- options.top = top; +- } else if (horizontal) { +- options.left = clampedOffset; +- } else { +- options.top = clampedOffset; +- } +- target.scrollTo(options); ++ }; ++ apply(0); + }, +- [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] ++ [getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] + ); + useImperativeHandle(ref, () => { + const api = { +@@ -8267,6 +8413,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); From d7b9fb214bd5e1afb74b7120d304abd085f499a5 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 11 Aug 2026 12:50:49 -0400 Subject: [PATCH 05/38] fix(chat): address second-round review of the legend-list patch Both fixes had blockers a review caught, so the patch is regenerated from the reworked fork branches. Web scroll re-issue: - it kept scrolling after the list unmounted, which in window scroll mode meant a dead component scrolling the page for up to ten frames - scrollToEnd never re-issued at all, because it asks for the extent it just measured and so was never out of reach - the one path that matters most when content has just grown - retries now stop as soon as the extent stops growing rather than always burning the full frame budget, and interference is judged against where the scroll actually landed so a platform that clamps differently is not mistaken for the user Scroll target settling: - the user-scroll release was dead code: onScroll assigns state.scrollPending to the incoming offset immediately before calling updateScroll, so the comparison was always zero. Corrections no longer claim the scroll session, which lets the existing user-scroll detection do the work - the correction budget was spent per layout pass rather than per correction, and layout runs many passes per frame while items measure, so on the very lists this targets it could spend the whole budget and issue nothing - maintainScrollAtEnd now releases the target when it requests the end anchor rather than a frame later, so a queued correction cannot slip in first --- shared/patches/@legendapp+list+3.3.5.patch | 558 +++++++++++---------- 1 file changed, 306 insertions(+), 252 deletions(-) diff --git a/shared/patches/@legendapp+list+3.3.5.patch b/shared/patches/@legendapp+list+3.3.5.patch index e69e63c04aa3..66d3a4edf4ad 100644 --- a/shared/patches/@legendapp+list+3.3.5.patch +++ b/shared/patches/@legendapp+list+3.3.5.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js -index b3c5a30..220444d 100644 +index b3c5a30..160d3a1 100644 --- a/node_modules/@legendapp/list/react-native.js +++ b/node_modules/@legendapp/list/react-native.js @@ -457,611 +457,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -1135,14 +1135,14 @@ index b3c5a30..220444d 100644 // src/core/doMaintainScrollAtEnd.ts function doMaintainScrollAtEnd(ctx) { const state = ctx.state; -@@ -1580,6 +1377,7 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1575,6 +1372,7 @@ function doMaintainScrollAtEnd(ctx) { + const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; + const scrollAtRequest = state.scroll; + state.maintainingScrollAtEnd = pendingState; ++ clearScrollTargetSettle(state); + requestAnimationFrame(() => { + const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; - if (isStillWithinThreshold || !didScrollSinceRequest) { - state.maintainingScrollAtEnd = activeState; -+ clearScrollTargetSettle(state); - const scroller = refScroller.current; - if (state.props.horizontal && isHorizontalRTL(state)) { - const currentContentSize = getContentSize(ctx); @@ -1977,6 +1775,27 @@ var flushSync = (fn) => { fn(); }; @@ -1171,25 +1171,17 @@ index b3c5a30..220444d 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2026,6 +1845,7 @@ function isInMVCPActiveMode(state) { - } - - // src/core/updateScroll.ts -+var SETTLE_RELEASE_EPSILON = 2; - function updateScroll(ctx, newScroll, forceUpdate, options) { - var _a3; - const state = ctx.state; -@@ -2061,6 +1881,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { - state.scrollPrevTime = state.scrollTime; - state.scroll = newScroll; +@@ -2063,6 +1882,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { state.scrollTime = currentTime; -+ if (state.scrollTargetSettle && (options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust && Math.abs(newScroll - state.scrollPending) > SETTLE_RELEASE_EPSILON) { -+ clearScrollTargetSettle(state); -+ } const scrollDelta = Math.abs(newScroll - prevScroll); const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; ++ if (isUserScrollEvent) { ++ clearScrollTargetSettle(state); ++ } const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; -@@ -2224,7 +2047,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); + const scrollLength = state.scrollLength; +@@ -2224,7 +2046,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } function scrollTo(ctx, params) { @@ -1198,7 +1190,7 @@ index b3c5a30..220444d 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2256,6 +2079,15 @@ function scrollTo(ctx, params) { +@@ -2256,6 +2078,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); @@ -1214,7 +1206,7 @@ index b3c5a30..220444d 100644 } } state.scrollPending = targetOffset; -@@ -2263,7 +2095,7 @@ function scrollTo(ctx, params) { +@@ -2263,7 +2094,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -1223,7 +1215,7 @@ index b3c5a30..220444d 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2276,6 +2108,289 @@ function scrollTo(ctx, params) { +@@ -2276,6 +2107,293 @@ function scrollTo(ctx, params) { } } @@ -1249,7 +1241,7 @@ index b3c5a30..220444d 100644 + const now = Date.now(); + const id = getId(state, index); + const existing = state.scrollTargetSettle; -+ const isSameTarget = (existing == null ? void 0 : existing.id) === id; ++ const isSameTarget = (existing == null ? void 0 : existing.id) === id && existing.viewPosition === viewPosition && existing.viewOffset === viewOffset; + state.scrollTargetSettle = { + corrections: isSameTarget ? existing.corrections : 0, + deadline: isSameTarget ? existing.deadline : now + SETTLE_MAX_MS, @@ -1276,10 +1268,19 @@ index b3c5a30..220444d 100644 + clearScrollTargetSettle(state); + return; + } ++ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { ++ clearScrollTargetSettle(state); ++ return; ++ } ++ settle.corrections++; + scrollTo(ctx, { + animated: false, + index, + itemSize: getItemSizeAtIndex(ctx, index), ++ // Correcting where a scroll already landed is not a new imperative scroll: leaving ++ // state.scrollingTo alone keeps the list treating subsequent scroll events as the user's, ++ // which is what releases this settle, and keeps edge-reached callbacks firing. ++ noScrollingTo: true, + offset: position, + viewOffset: settle.viewOffset, + viewPosition: settle.viewPosition @@ -1316,12 +1317,7 @@ index b3c5a30..220444d 100644 + } + return false; + } -+ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { -+ clearScrollTargetSettle(state); -+ return false; -+ } + settle.quietPasses = 0; -+ settle.corrections++; + settle.expiresAt = now + SETTLE_TTL_MS; + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; @@ -1513,22 +1509,23 @@ index b3c5a30..220444d 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4337,7 +4452,13 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4337,8 +4455,13 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); - const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); +- if (didMVCPAdjustScroll) { + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { + settleScrollTarget(ctx, isCompensating); + } -+ const didMVCPAdjustScroll = didMVCPAdjust; - if (didMVCPAdjustScroll) { ++ if (didMVCPAdjust) { updateScroll2(state.scroll); updateScrollRange(); -@@ -7652,6 +7773,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + } +@@ -7652,6 +7775,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -1537,7 +1534,7 @@ index b3c5a30..220444d 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs -index 40e87cd..1581522 100644 +index 40e87cd..11a3c24 100644 --- a/node_modules/@legendapp/list/react-native.mjs +++ b/node_modules/@legendapp/list/react-native.mjs @@ -436,611 +436,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -2673,14 +2670,14 @@ index 40e87cd..1581522 100644 // src/core/doMaintainScrollAtEnd.ts function doMaintainScrollAtEnd(ctx) { const state = ctx.state; -@@ -1559,6 +1356,7 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1554,6 +1351,7 @@ function doMaintainScrollAtEnd(ctx) { + const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; + const scrollAtRequest = state.scroll; + state.maintainingScrollAtEnd = pendingState; ++ clearScrollTargetSettle(state); + requestAnimationFrame(() => { + const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; - if (isStillWithinThreshold || !didScrollSinceRequest) { - state.maintainingScrollAtEnd = activeState; -+ clearScrollTargetSettle(state); - const scroller = refScroller.current; - if (state.props.horizontal && isHorizontalRTL(state)) { - const currentContentSize = getContentSize(ctx); @@ -1956,6 +1754,27 @@ var flushSync = (fn) => { fn(); }; @@ -2709,25 +2706,17 @@ index 40e87cd..1581522 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2005,6 +1824,7 @@ function isInMVCPActiveMode(state) { - } - - // src/core/updateScroll.ts -+var SETTLE_RELEASE_EPSILON = 2; - function updateScroll(ctx, newScroll, forceUpdate, options) { - var _a3; - const state = ctx.state; -@@ -2040,6 +1860,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { - state.scrollPrevTime = state.scrollTime; - state.scroll = newScroll; +@@ -2042,6 +1861,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { state.scrollTime = currentTime; -+ if (state.scrollTargetSettle && (options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust && Math.abs(newScroll - state.scrollPending) > SETTLE_RELEASE_EPSILON) { -+ clearScrollTargetSettle(state); -+ } const scrollDelta = Math.abs(newScroll - prevScroll); const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; ++ if (isUserScrollEvent) { ++ clearScrollTargetSettle(state); ++ } const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; -@@ -2203,7 +2026,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); + const scrollLength = state.scrollLength; +@@ -2203,7 +2025,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } function scrollTo(ctx, params) { @@ -2736,7 +2725,7 @@ index 40e87cd..1581522 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2235,6 +2058,15 @@ function scrollTo(ctx, params) { +@@ -2235,6 +2057,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); @@ -2752,7 +2741,7 @@ index 40e87cd..1581522 100644 } } state.scrollPending = targetOffset; -@@ -2242,7 +2074,7 @@ function scrollTo(ctx, params) { +@@ -2242,7 +2073,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -2761,7 +2750,7 @@ index 40e87cd..1581522 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2255,6 +2087,289 @@ function scrollTo(ctx, params) { +@@ -2255,6 +2086,293 @@ function scrollTo(ctx, params) { } } @@ -2787,7 +2776,7 @@ index 40e87cd..1581522 100644 + const now = Date.now(); + const id = getId(state, index); + const existing = state.scrollTargetSettle; -+ const isSameTarget = (existing == null ? void 0 : existing.id) === id; ++ const isSameTarget = (existing == null ? void 0 : existing.id) === id && existing.viewPosition === viewPosition && existing.viewOffset === viewOffset; + state.scrollTargetSettle = { + corrections: isSameTarget ? existing.corrections : 0, + deadline: isSameTarget ? existing.deadline : now + SETTLE_MAX_MS, @@ -2814,10 +2803,19 @@ index 40e87cd..1581522 100644 + clearScrollTargetSettle(state); + return; + } ++ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { ++ clearScrollTargetSettle(state); ++ return; ++ } ++ settle.corrections++; + scrollTo(ctx, { + animated: false, + index, + itemSize: getItemSizeAtIndex(ctx, index), ++ // Correcting where a scroll already landed is not a new imperative scroll: leaving ++ // state.scrollingTo alone keeps the list treating subsequent scroll events as the user's, ++ // which is what releases this settle, and keeps edge-reached callbacks firing. ++ noScrollingTo: true, + offset: position, + viewOffset: settle.viewOffset, + viewPosition: settle.viewPosition @@ -2854,12 +2852,7 @@ index 40e87cd..1581522 100644 + } + return false; + } -+ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { -+ clearScrollTargetSettle(state); -+ return false; -+ } + settle.quietPasses = 0; -+ settle.corrections++; + settle.expiresAt = now + SETTLE_TTL_MS; + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; @@ -3051,22 +3044,23 @@ index 40e87cd..1581522 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4316,7 +4431,13 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4316,8 +4434,13 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); - const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); +- if (didMVCPAdjustScroll) { + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { + settleScrollTarget(ctx, isCompensating); + } -+ const didMVCPAdjustScroll = didMVCPAdjust; - if (didMVCPAdjustScroll) { ++ if (didMVCPAdjust) { updateScroll2(state.scroll); updateScrollRange(); -@@ -7631,6 +7752,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + } +@@ -7631,6 +7754,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -3075,7 +3069,7 @@ index 40e87cd..1581522 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js -index 914d2da..da09324 100644 +index 914d2da..66763c2 100644 --- a/node_modules/@legendapp/list/react-native.web.js +++ b/node_modules/@legendapp/list/react-native.web.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -4186,14 +4180,14 @@ index 914d2da..da09324 100644 // src/core/doMaintainScrollAtEnd.ts function doMaintainScrollAtEnd(ctx) { const state = ctx.state; -@@ -1456,6 +1247,7 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1451,6 +1242,7 @@ function doMaintainScrollAtEnd(ctx) { + const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; + const scrollAtRequest = state.scroll; + state.maintainingScrollAtEnd = pendingState; ++ clearScrollTargetSettle(state); + requestAnimationFrame(() => { + const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; - if (isStillWithinThreshold || !didScrollSinceRequest) { - state.maintainingScrollAtEnd = activeState; -+ clearScrollTargetSettle(state); - const scroller = refScroller.current; - if (state.props.horizontal && isHorizontalRTL(state)) { - const currentContentSize = getContentSize(ctx); @@ -1809,6 +1601,27 @@ function prepareMVCP(ctx, dataChanged) { } } @@ -4222,25 +4216,17 @@ index 914d2da..da09324 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -1871,6 +1684,7 @@ function isInMVCPActiveMode(state) { - } - - // src/core/updateScroll.ts -+var SETTLE_RELEASE_EPSILON = 2; - function updateScroll(ctx, newScroll, forceUpdate, options) { - var _a3; - const state = ctx.state; -@@ -1906,6 +1720,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { - state.scrollPrevTime = state.scrollTime; - state.scroll = newScroll; +@@ -1908,6 +1721,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { state.scrollTime = currentTime; -+ if (state.scrollTargetSettle && (options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust && Math.abs(newScroll - state.scrollPending) > SETTLE_RELEASE_EPSILON) { -+ clearScrollTargetSettle(state); -+ } const scrollDelta = Math.abs(newScroll - prevScroll); const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; ++ if (isUserScrollEvent) { ++ clearScrollTargetSettle(state); ++ } const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; -@@ -2069,7 +1886,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); + const scrollLength = state.scrollLength; +@@ -2069,7 +1885,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } function scrollTo(ctx, params) { @@ -4249,7 +4235,7 @@ index 914d2da..da09324 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2101,6 +1918,15 @@ function scrollTo(ctx, params) { +@@ -2101,6 +1917,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); @@ -4265,7 +4251,7 @@ index 914d2da..da09324 100644 } } state.scrollPending = targetOffset; -@@ -2108,7 +1934,7 @@ function scrollTo(ctx, params) { +@@ -2108,7 +1933,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -4274,7 +4260,7 @@ index 914d2da..da09324 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2121,6 +1947,295 @@ function scrollTo(ctx, params) { +@@ -2121,6 +1946,299 @@ function scrollTo(ctx, params) { } } @@ -4300,7 +4286,7 @@ index 914d2da..da09324 100644 + const now = Date.now(); + const id = getId(state, index); + const existing = state.scrollTargetSettle; -+ const isSameTarget = (existing == null ? void 0 : existing.id) === id; ++ const isSameTarget = (existing == null ? void 0 : existing.id) === id && existing.viewPosition === viewPosition && existing.viewOffset === viewOffset; + state.scrollTargetSettle = { + corrections: isSameTarget ? existing.corrections : 0, + deadline: isSameTarget ? existing.deadline : now + SETTLE_MAX_MS, @@ -4327,10 +4313,19 @@ index 914d2da..da09324 100644 + clearScrollTargetSettle(state); + return; + } ++ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { ++ clearScrollTargetSettle(state); ++ return; ++ } ++ settle.corrections++; + scrollTo(ctx, { + animated: false, + index, + itemSize: getItemSizeAtIndex(ctx, index), ++ // Correcting where a scroll already landed is not a new imperative scroll: leaving ++ // state.scrollingTo alone keeps the list treating subsequent scroll events as the user's, ++ // which is what releases this settle, and keeps edge-reached callbacks firing. ++ noScrollingTo: true, + offset: position, + viewOffset: settle.viewOffset, + viewPosition: settle.viewPosition @@ -4367,12 +4362,7 @@ index 914d2da..da09324 100644 + } + return false; + } -+ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { -+ clearScrollTargetSettle(state); -+ return false; -+ } + settle.quietPasses = 0; -+ settle.corrections++; + settle.expiresAt = now + SETTLE_TTL_MS; + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; @@ -4570,22 +4560,23 @@ index 914d2da..da09324 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4367,7 +4482,13 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4367,8 +4485,13 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); - const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); +- if (didMVCPAdjustScroll) { + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { + settleScrollTarget(ctx, isCompensating); + } -+ const didMVCPAdjustScroll = didMVCPAdjust; - if (didMVCPAdjustScroll) { ++ if (didMVCPAdjust) { updateScroll2(state.scroll); updateScrollRange(); -@@ -6006,6 +6127,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll + } +@@ -6006,6 +6129,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; @@ -4594,11 +4585,17 @@ index 914d2da..da09324 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6094,36 +6217,59 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,36 +6219,67 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); + const scrollRetryToken = React3.useRef(0); ++ React3.useEffect( ++ () => () => { ++ scrollRetryToken.current++; ++ }, ++ [] ++ ); const scrollToLocalOffset = React3.useCallback( (offset, animated) => { - const scrollElement = scrollRef.current; @@ -4619,12 +4616,15 @@ index 914d2da..da09324 100644 - listPos, - scroll + const token = ++scrollRetryToken.current; ++ let previousMaxOffset = -1; + const apply = (attempt) => { -+ if (token !== scrollRetryToken.current) { ++ if (token !== scrollRetryToken.current || !scrollRef.current) { + return; + } + const scrollElement = scrollRef.current; + const maxOffset = getMaxScrollOffset(); ++ const didExtentGrow = maxOffset > previousMaxOffset; ++ previousMaxOffset = maxOffset; + const clampedOffset = clampOffset(offset, maxOffset); + const options = { behavior }; + if (isWindowScroll) { @@ -4644,17 +4644,16 @@ index 914d2da..da09324 100644 + options.top = clampedOffset; + } + target.scrollTo(options); -+ if (animated || clampedOffset >= offset - SCROLL_CLAMP_EPSILON) { -+ return; -+ } -+ if (attempt >= SCROLL_CLAMP_RETRY_FRAMES) { ++ const landedOffset = getCurrentScrollOffset(); ++ const isRequestOutOfReach = clampedOffset < offset - SCROLL_CLAMP_EPSILON; ++ if (animated || !isRequestOutOfReach || !didExtentGrow || attempt >= SCROLL_CLAMP_RETRY_FRAMES) { + return; + } + requestAnimationFrame(() => { -+ if (token !== scrollRetryToken.current) { ++ if (token !== scrollRetryToken.current || !scrollRef.current) { + return; + } -+ if (Math.abs(getCurrentScrollOffset() - clampedOffset) > SCROLL_CLAMP_EPSILON) { ++ if (Math.abs(getCurrentScrollOffset() - landedOffset) > SCROLL_CLAMP_EPSILON) { + return; + } + apply(attempt + 1); @@ -4675,7 +4674,17 @@ index 914d2da..da09324 100644 ); React3.useImperativeHandle(ref, () => { const api = { -@@ -8288,6 +8434,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6149,8 +6305,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + }, + scrollToEnd: (options = {}) => { + const { animated = true } = options; +- const endOffset = getMaxScrollOffset(); +- scrollToLocalOffset(endOffset, animated); ++ scrollToLocalOffset(Number.POSITIVE_INFINITY, animated); + }, + scrollToOffset: (params) => { + const { offset, animated = true } = params; +@@ -8288,6 +8443,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -4684,7 +4693,7 @@ index 914d2da..da09324 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs -index 95465f2..d2ebd71 100644 +index 95465f2..5669ad7 100644 --- a/node_modules/@legendapp/list/react-native.web.mjs +++ b/node_modules/@legendapp/list/react-native.web.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -5795,14 +5804,14 @@ index 95465f2..d2ebd71 100644 // src/core/doMaintainScrollAtEnd.ts function doMaintainScrollAtEnd(ctx) { const state = ctx.state; -@@ -1435,6 +1226,7 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1430,6 +1221,7 @@ function doMaintainScrollAtEnd(ctx) { + const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; + const scrollAtRequest = state.scroll; + state.maintainingScrollAtEnd = pendingState; ++ clearScrollTargetSettle(state); + requestAnimationFrame(() => { + const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; - if (isStillWithinThreshold || !didScrollSinceRequest) { - state.maintainingScrollAtEnd = activeState; -+ clearScrollTargetSettle(state); - const scroller = refScroller.current; - if (state.props.horizontal && isHorizontalRTL(state)) { - const currentContentSize = getContentSize(ctx); @@ -1788,6 +1580,27 @@ function prepareMVCP(ctx, dataChanged) { } } @@ -5831,25 +5840,17 @@ index 95465f2..d2ebd71 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -1850,6 +1663,7 @@ function isInMVCPActiveMode(state) { - } - - // src/core/updateScroll.ts -+var SETTLE_RELEASE_EPSILON = 2; - function updateScroll(ctx, newScroll, forceUpdate, options) { - var _a3; - const state = ctx.state; -@@ -1885,6 +1699,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { - state.scrollPrevTime = state.scrollTime; - state.scroll = newScroll; +@@ -1887,6 +1700,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { state.scrollTime = currentTime; -+ if (state.scrollTargetSettle && (options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust && Math.abs(newScroll - state.scrollPending) > SETTLE_RELEASE_EPSILON) { -+ clearScrollTargetSettle(state); -+ } const scrollDelta = Math.abs(newScroll - prevScroll); const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; ++ if (isUserScrollEvent) { ++ clearScrollTargetSettle(state); ++ } const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; -@@ -2048,7 +1865,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); + const scrollLength = state.scrollLength; +@@ -2048,7 +1864,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } function scrollTo(ctx, params) { @@ -5858,7 +5859,7 @@ index 95465f2..d2ebd71 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2080,6 +1897,15 @@ function scrollTo(ctx, params) { +@@ -2080,6 +1896,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); @@ -5874,7 +5875,7 @@ index 95465f2..d2ebd71 100644 } } state.scrollPending = targetOffset; -@@ -2087,7 +1913,7 @@ function scrollTo(ctx, params) { +@@ -2087,7 +1912,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -5883,7 +5884,7 @@ index 95465f2..d2ebd71 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2100,6 +1926,295 @@ function scrollTo(ctx, params) { +@@ -2100,6 +1925,299 @@ function scrollTo(ctx, params) { } } @@ -5909,7 +5910,7 @@ index 95465f2..d2ebd71 100644 + const now = Date.now(); + const id = getId(state, index); + const existing = state.scrollTargetSettle; -+ const isSameTarget = (existing == null ? void 0 : existing.id) === id; ++ const isSameTarget = (existing == null ? void 0 : existing.id) === id && existing.viewPosition === viewPosition && existing.viewOffset === viewOffset; + state.scrollTargetSettle = { + corrections: isSameTarget ? existing.corrections : 0, + deadline: isSameTarget ? existing.deadline : now + SETTLE_MAX_MS, @@ -5936,10 +5937,19 @@ index 95465f2..d2ebd71 100644 + clearScrollTargetSettle(state); + return; + } ++ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { ++ clearScrollTargetSettle(state); ++ return; ++ } ++ settle.corrections++; + scrollTo(ctx, { + animated: false, + index, + itemSize: getItemSizeAtIndex(ctx, index), ++ // Correcting where a scroll already landed is not a new imperative scroll: leaving ++ // state.scrollingTo alone keeps the list treating subsequent scroll events as the user's, ++ // which is what releases this settle, and keeps edge-reached callbacks firing. ++ noScrollingTo: true, + offset: position, + viewOffset: settle.viewOffset, + viewPosition: settle.viewPosition @@ -5976,12 +5986,7 @@ index 95465f2..d2ebd71 100644 + } + return false; + } -+ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { -+ clearScrollTargetSettle(state); -+ return false; -+ } + settle.quietPasses = 0; -+ settle.corrections++; + settle.expiresAt = now + SETTLE_TTL_MS; + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; @@ -6179,22 +6184,23 @@ index 95465f2..d2ebd71 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4346,7 +4461,13 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4346,8 +4464,13 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); - const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); +- if (didMVCPAdjustScroll) { + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { + settleScrollTarget(ctx, isCompensating); + } -+ const didMVCPAdjustScroll = didMVCPAdjust; - if (didMVCPAdjustScroll) { ++ if (didMVCPAdjust) { updateScroll2(state.scroll); updateScrollRange(); -@@ -5985,6 +6106,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll + } +@@ -5985,6 +6108,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; @@ -6203,11 +6209,17 @@ index 95465f2..d2ebd71 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6073,36 +6196,59 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,36 +6198,67 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); + const scrollRetryToken = useRef(0); ++ useEffect( ++ () => () => { ++ scrollRetryToken.current++; ++ }, ++ [] ++ ); const scrollToLocalOffset = useCallback( (offset, animated) => { - const scrollElement = scrollRef.current; @@ -6228,12 +6240,15 @@ index 95465f2..d2ebd71 100644 - listPos, - scroll + const token = ++scrollRetryToken.current; ++ let previousMaxOffset = -1; + const apply = (attempt) => { -+ if (token !== scrollRetryToken.current) { ++ if (token !== scrollRetryToken.current || !scrollRef.current) { + return; + } + const scrollElement = scrollRef.current; + const maxOffset = getMaxScrollOffset(); ++ const didExtentGrow = maxOffset > previousMaxOffset; ++ previousMaxOffset = maxOffset; + const clampedOffset = clampOffset(offset, maxOffset); + const options = { behavior }; + if (isWindowScroll) { @@ -6253,17 +6268,16 @@ index 95465f2..d2ebd71 100644 + options.top = clampedOffset; + } + target.scrollTo(options); -+ if (animated || clampedOffset >= offset - SCROLL_CLAMP_EPSILON) { -+ return; -+ } -+ if (attempt >= SCROLL_CLAMP_RETRY_FRAMES) { ++ const landedOffset = getCurrentScrollOffset(); ++ const isRequestOutOfReach = clampedOffset < offset - SCROLL_CLAMP_EPSILON; ++ if (animated || !isRequestOutOfReach || !didExtentGrow || attempt >= SCROLL_CLAMP_RETRY_FRAMES) { + return; + } + requestAnimationFrame(() => { -+ if (token !== scrollRetryToken.current) { ++ if (token !== scrollRetryToken.current || !scrollRef.current) { + return; + } -+ if (Math.abs(getCurrentScrollOffset() - clampedOffset) > SCROLL_CLAMP_EPSILON) { ++ if (Math.abs(getCurrentScrollOffset() - landedOffset) > SCROLL_CLAMP_EPSILON) { + return; + } + apply(attempt + 1); @@ -6284,7 +6298,17 @@ index 95465f2..d2ebd71 100644 ); useImperativeHandle(ref, () => { const api = { -@@ -8267,6 +8413,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6128,8 +6284,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + }, + scrollToEnd: (options = {}) => { + const { animated = true } = options; +- const endOffset = getMaxScrollOffset(); +- scrollToLocalOffset(endOffset, animated); ++ scrollToLocalOffset(Number.POSITIVE_INFINITY, animated); + }, + scrollToOffset: (params) => { + const { offset, animated = true } = params; +@@ -8267,6 +8422,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -6293,7 +6317,7 @@ index 95465f2..d2ebd71 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js -index 914d2da..da09324 100644 +index 914d2da..66763c2 100644 --- a/node_modules/@legendapp/list/react.js +++ b/node_modules/@legendapp/list/react.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -7404,14 +7428,14 @@ index 914d2da..da09324 100644 // src/core/doMaintainScrollAtEnd.ts function doMaintainScrollAtEnd(ctx) { const state = ctx.state; -@@ -1456,6 +1247,7 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1451,6 +1242,7 @@ function doMaintainScrollAtEnd(ctx) { + const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; + const scrollAtRequest = state.scroll; + state.maintainingScrollAtEnd = pendingState; ++ clearScrollTargetSettle(state); + requestAnimationFrame(() => { + const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; - if (isStillWithinThreshold || !didScrollSinceRequest) { - state.maintainingScrollAtEnd = activeState; -+ clearScrollTargetSettle(state); - const scroller = refScroller.current; - if (state.props.horizontal && isHorizontalRTL(state)) { - const currentContentSize = getContentSize(ctx); @@ -1809,6 +1601,27 @@ function prepareMVCP(ctx, dataChanged) { } } @@ -7440,25 +7464,17 @@ index 914d2da..da09324 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -1871,6 +1684,7 @@ function isInMVCPActiveMode(state) { - } - - // src/core/updateScroll.ts -+var SETTLE_RELEASE_EPSILON = 2; - function updateScroll(ctx, newScroll, forceUpdate, options) { - var _a3; - const state = ctx.state; -@@ -1906,6 +1720,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { - state.scrollPrevTime = state.scrollTime; - state.scroll = newScroll; +@@ -1908,6 +1721,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { state.scrollTime = currentTime; -+ if (state.scrollTargetSettle && (options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust && Math.abs(newScroll - state.scrollPending) > SETTLE_RELEASE_EPSILON) { -+ clearScrollTargetSettle(state); -+ } const scrollDelta = Math.abs(newScroll - prevScroll); const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; ++ if (isUserScrollEvent) { ++ clearScrollTargetSettle(state); ++ } const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; -@@ -2069,7 +1886,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); + const scrollLength = state.scrollLength; +@@ -2069,7 +1885,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } function scrollTo(ctx, params) { @@ -7467,7 +7483,7 @@ index 914d2da..da09324 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2101,6 +1918,15 @@ function scrollTo(ctx, params) { +@@ -2101,6 +1917,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); @@ -7483,7 +7499,7 @@ index 914d2da..da09324 100644 } } state.scrollPending = targetOffset; -@@ -2108,7 +1934,7 @@ function scrollTo(ctx, params) { +@@ -2108,7 +1933,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -7492,7 +7508,7 @@ index 914d2da..da09324 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2121,6 +1947,295 @@ function scrollTo(ctx, params) { +@@ -2121,6 +1946,299 @@ function scrollTo(ctx, params) { } } @@ -7518,7 +7534,7 @@ index 914d2da..da09324 100644 + const now = Date.now(); + const id = getId(state, index); + const existing = state.scrollTargetSettle; -+ const isSameTarget = (existing == null ? void 0 : existing.id) === id; ++ const isSameTarget = (existing == null ? void 0 : existing.id) === id && existing.viewPosition === viewPosition && existing.viewOffset === viewOffset; + state.scrollTargetSettle = { + corrections: isSameTarget ? existing.corrections : 0, + deadline: isSameTarget ? existing.deadline : now + SETTLE_MAX_MS, @@ -7545,10 +7561,19 @@ index 914d2da..da09324 100644 + clearScrollTargetSettle(state); + return; + } ++ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { ++ clearScrollTargetSettle(state); ++ return; ++ } ++ settle.corrections++; + scrollTo(ctx, { + animated: false, + index, + itemSize: getItemSizeAtIndex(ctx, index), ++ // Correcting where a scroll already landed is not a new imperative scroll: leaving ++ // state.scrollingTo alone keeps the list treating subsequent scroll events as the user's, ++ // which is what releases this settle, and keeps edge-reached callbacks firing. ++ noScrollingTo: true, + offset: position, + viewOffset: settle.viewOffset, + viewPosition: settle.viewPosition @@ -7585,12 +7610,7 @@ index 914d2da..da09324 100644 + } + return false; + } -+ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { -+ clearScrollTargetSettle(state); -+ return false; -+ } + settle.quietPasses = 0; -+ settle.corrections++; + settle.expiresAt = now + SETTLE_TTL_MS; + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; @@ -7788,22 +7808,23 @@ index 914d2da..da09324 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4367,7 +4482,13 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4367,8 +4485,13 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); - const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); +- if (didMVCPAdjustScroll) { + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { + settleScrollTarget(ctx, isCompensating); + } -+ const didMVCPAdjustScroll = didMVCPAdjust; - if (didMVCPAdjustScroll) { ++ if (didMVCPAdjust) { updateScroll2(state.scroll); updateScrollRange(); -@@ -6006,6 +6127,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll + } +@@ -6006,6 +6129,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; @@ -7812,11 +7833,17 @@ index 914d2da..da09324 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6094,36 +6217,59 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,36 +6219,67 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); + const scrollRetryToken = React3.useRef(0); ++ React3.useEffect( ++ () => () => { ++ scrollRetryToken.current++; ++ }, ++ [] ++ ); const scrollToLocalOffset = React3.useCallback( (offset, animated) => { - const scrollElement = scrollRef.current; @@ -7837,12 +7864,15 @@ index 914d2da..da09324 100644 - listPos, - scroll + const token = ++scrollRetryToken.current; ++ let previousMaxOffset = -1; + const apply = (attempt) => { -+ if (token !== scrollRetryToken.current) { ++ if (token !== scrollRetryToken.current || !scrollRef.current) { + return; + } + const scrollElement = scrollRef.current; + const maxOffset = getMaxScrollOffset(); ++ const didExtentGrow = maxOffset > previousMaxOffset; ++ previousMaxOffset = maxOffset; + const clampedOffset = clampOffset(offset, maxOffset); + const options = { behavior }; + if (isWindowScroll) { @@ -7862,17 +7892,16 @@ index 914d2da..da09324 100644 + options.top = clampedOffset; + } + target.scrollTo(options); -+ if (animated || clampedOffset >= offset - SCROLL_CLAMP_EPSILON) { -+ return; -+ } -+ if (attempt >= SCROLL_CLAMP_RETRY_FRAMES) { ++ const landedOffset = getCurrentScrollOffset(); ++ const isRequestOutOfReach = clampedOffset < offset - SCROLL_CLAMP_EPSILON; ++ if (animated || !isRequestOutOfReach || !didExtentGrow || attempt >= SCROLL_CLAMP_RETRY_FRAMES) { + return; + } + requestAnimationFrame(() => { -+ if (token !== scrollRetryToken.current) { ++ if (token !== scrollRetryToken.current || !scrollRef.current) { + return; + } -+ if (Math.abs(getCurrentScrollOffset() - clampedOffset) > SCROLL_CLAMP_EPSILON) { ++ if (Math.abs(getCurrentScrollOffset() - landedOffset) > SCROLL_CLAMP_EPSILON) { + return; + } + apply(attempt + 1); @@ -7893,7 +7922,17 @@ index 914d2da..da09324 100644 ); React3.useImperativeHandle(ref, () => { const api = { -@@ -8288,6 +8434,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6149,8 +6305,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + }, + scrollToEnd: (options = {}) => { + const { animated = true } = options; +- const endOffset = getMaxScrollOffset(); +- scrollToLocalOffset(endOffset, animated); ++ scrollToLocalOffset(Number.POSITIVE_INFINITY, animated); + }, + scrollToOffset: (params) => { + const { offset, animated = true } = params; +@@ -8288,6 +8443,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -7902,7 +7941,7 @@ index 914d2da..da09324 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs -index 95465f2..d2ebd71 100644 +index 95465f2..5669ad7 100644 --- a/node_modules/@legendapp/list/react.mjs +++ b/node_modules/@legendapp/list/react.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -9013,14 +9052,14 @@ index 95465f2..d2ebd71 100644 // src/core/doMaintainScrollAtEnd.ts function doMaintainScrollAtEnd(ctx) { const state = ctx.state; -@@ -1435,6 +1226,7 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1430,6 +1221,7 @@ function doMaintainScrollAtEnd(ctx) { + const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; + const scrollAtRequest = state.scroll; + state.maintainingScrollAtEnd = pendingState; ++ clearScrollTargetSettle(state); + requestAnimationFrame(() => { + const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; - if (isStillWithinThreshold || !didScrollSinceRequest) { - state.maintainingScrollAtEnd = activeState; -+ clearScrollTargetSettle(state); - const scroller = refScroller.current; - if (state.props.horizontal && isHorizontalRTL(state)) { - const currentContentSize = getContentSize(ctx); @@ -1788,6 +1580,27 @@ function prepareMVCP(ctx, dataChanged) { } } @@ -9049,25 +9088,17 @@ index 95465f2..d2ebd71 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -1850,6 +1663,7 @@ function isInMVCPActiveMode(state) { - } - - // src/core/updateScroll.ts -+var SETTLE_RELEASE_EPSILON = 2; - function updateScroll(ctx, newScroll, forceUpdate, options) { - var _a3; - const state = ctx.state; -@@ -1885,6 +1699,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { - state.scrollPrevTime = state.scrollTime; - state.scroll = newScroll; +@@ -1887,6 +1700,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { state.scrollTime = currentTime; -+ if (state.scrollTargetSettle && (options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust && Math.abs(newScroll - state.scrollPending) > SETTLE_RELEASE_EPSILON) { -+ clearScrollTargetSettle(state); -+ } const scrollDelta = Math.abs(newScroll - prevScroll); const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; ++ if (isUserScrollEvent) { ++ clearScrollTargetSettle(state); ++ } const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; -@@ -2048,7 +1865,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); + const scrollLength = state.scrollLength; +@@ -2048,7 +1864,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } function scrollTo(ctx, params) { @@ -9076,7 +9107,7 @@ index 95465f2..d2ebd71 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2080,6 +1897,15 @@ function scrollTo(ctx, params) { +@@ -2080,6 +1896,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); @@ -9092,7 +9123,7 @@ index 95465f2..d2ebd71 100644 } } state.scrollPending = targetOffset; -@@ -2087,7 +1913,7 @@ function scrollTo(ctx, params) { +@@ -2087,7 +1912,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -9101,7 +9132,7 @@ index 95465f2..d2ebd71 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2100,6 +1926,295 @@ function scrollTo(ctx, params) { +@@ -2100,6 +1925,299 @@ function scrollTo(ctx, params) { } } @@ -9127,7 +9158,7 @@ index 95465f2..d2ebd71 100644 + const now = Date.now(); + const id = getId(state, index); + const existing = state.scrollTargetSettle; -+ const isSameTarget = (existing == null ? void 0 : existing.id) === id; ++ const isSameTarget = (existing == null ? void 0 : existing.id) === id && existing.viewPosition === viewPosition && existing.viewOffset === viewOffset; + state.scrollTargetSettle = { + corrections: isSameTarget ? existing.corrections : 0, + deadline: isSameTarget ? existing.deadline : now + SETTLE_MAX_MS, @@ -9154,10 +9185,19 @@ index 95465f2..d2ebd71 100644 + clearScrollTargetSettle(state); + return; + } ++ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { ++ clearScrollTargetSettle(state); ++ return; ++ } ++ settle.corrections++; + scrollTo(ctx, { + animated: false, + index, + itemSize: getItemSizeAtIndex(ctx, index), ++ // Correcting where a scroll already landed is not a new imperative scroll: leaving ++ // state.scrollingTo alone keeps the list treating subsequent scroll events as the user's, ++ // which is what releases this settle, and keeps edge-reached callbacks firing. ++ noScrollingTo: true, + offset: position, + viewOffset: settle.viewOffset, + viewPosition: settle.viewPosition @@ -9194,12 +9234,7 @@ index 95465f2..d2ebd71 100644 + } + return false; + } -+ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { -+ clearScrollTargetSettle(state); -+ return false; -+ } + settle.quietPasses = 0; -+ settle.corrections++; + settle.expiresAt = now + SETTLE_TTL_MS; + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; @@ -9397,22 +9432,23 @@ index 95465f2..d2ebd71 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4346,7 +4461,13 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4346,8 +4464,13 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); - const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); +- if (didMVCPAdjustScroll) { + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { + settleScrollTarget(ctx, isCompensating); + } -+ const didMVCPAdjustScroll = didMVCPAdjust; - if (didMVCPAdjustScroll) { ++ if (didMVCPAdjust) { updateScroll2(state.scroll); updateScrollRange(); -@@ -5985,6 +6106,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll + } +@@ -5985,6 +6108,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; @@ -9421,11 +9457,17 @@ index 95465f2..d2ebd71 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6073,36 +6196,59 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,36 +6198,67 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); + const scrollRetryToken = useRef(0); ++ useEffect( ++ () => () => { ++ scrollRetryToken.current++; ++ }, ++ [] ++ ); const scrollToLocalOffset = useCallback( (offset, animated) => { - const scrollElement = scrollRef.current; @@ -9446,12 +9488,15 @@ index 95465f2..d2ebd71 100644 - listPos, - scroll + const token = ++scrollRetryToken.current; ++ let previousMaxOffset = -1; + const apply = (attempt) => { -+ if (token !== scrollRetryToken.current) { ++ if (token !== scrollRetryToken.current || !scrollRef.current) { + return; + } + const scrollElement = scrollRef.current; + const maxOffset = getMaxScrollOffset(); ++ const didExtentGrow = maxOffset > previousMaxOffset; ++ previousMaxOffset = maxOffset; + const clampedOffset = clampOffset(offset, maxOffset); + const options = { behavior }; + if (isWindowScroll) { @@ -9471,17 +9516,16 @@ index 95465f2..d2ebd71 100644 + options.top = clampedOffset; + } + target.scrollTo(options); -+ if (animated || clampedOffset >= offset - SCROLL_CLAMP_EPSILON) { -+ return; -+ } -+ if (attempt >= SCROLL_CLAMP_RETRY_FRAMES) { ++ const landedOffset = getCurrentScrollOffset(); ++ const isRequestOutOfReach = clampedOffset < offset - SCROLL_CLAMP_EPSILON; ++ if (animated || !isRequestOutOfReach || !didExtentGrow || attempt >= SCROLL_CLAMP_RETRY_FRAMES) { + return; + } + requestAnimationFrame(() => { -+ if (token !== scrollRetryToken.current) { ++ if (token !== scrollRetryToken.current || !scrollRef.current) { + return; + } -+ if (Math.abs(getCurrentScrollOffset() - clampedOffset) > SCROLL_CLAMP_EPSILON) { ++ if (Math.abs(getCurrentScrollOffset() - landedOffset) > SCROLL_CLAMP_EPSILON) { + return; + } + apply(attempt + 1); @@ -9502,7 +9546,17 @@ index 95465f2..d2ebd71 100644 ); useImperativeHandle(ref, () => { const api = { -@@ -8267,6 +8413,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6128,8 +6284,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + }, + scrollToEnd: (options = {}) => { + const { animated = true } = options; +- const endOffset = getMaxScrollOffset(); +- scrollToLocalOffset(endOffset, animated); ++ scrollToLocalOffset(Number.POSITIVE_INFINITY, animated); + }, + scrollToOffset: (params) => { + const { offset, animated = true } = params; +@@ -8267,6 +8422,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); From cb8883fea1f532c3d5e49c6087ecda3bb8610c3e Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 11 Aug 2026 15:12:08 -0400 Subject: [PATCH 06/38] fix(chat): scroll to a search hit once, not on every index shift Jumping to a hit, waiting, then scrolling pulled the list back to the hit. Scrolling up is what asks for older messages, the prepend shifts every index, and the centering effect treated a shifted index as a new target to scroll to - so reading around a hit re-centered the list out from under you. The index tracking was there because a prepend does move the target out from under an already-issued scroll. That is now handled where it belongs: the list holds an imperative scroll target in place while rows measure, and maintainVisibleContentPosition holds it across prepends. So this scrolls once per target on both platforms; desktop had the same flaw and popped back on wheel-triggered pagination. Also picks up the reworked legend-list patch: the web scroll fix now borrows the room a request needs through bookkeeping shared with ScrollAdjust, rather than gating scrolls on a readiness check that could stall them. --- shared/chat/conversation/list-area/index.tsx | 34 +- shared/patches/@legendapp+list+3.3.5.patch | 3230 +++++++++++++----- 2 files changed, 2441 insertions(+), 823 deletions(-) diff --git a/shared/chat/conversation/list-area/index.tsx b/shared/chat/conversation/list-area/index.tsx index 9b5f2f7d64ca..b944e15e3c03 100644 --- a/shared/chat/conversation/list-area/index.tsx +++ b/shared/chat/conversation/list-area/index.tsx @@ -331,12 +331,11 @@ const DesktopThreadWrapper = function DesktopThreadWrapper() { // scroll when loaded becomes true after centeredOrdinal was already set. // Reset per dataset, not per conversation: re-centering on the ordinal we are already parked // on still reloads the thread, so the list has to scroll to it again. - // Records the index the target sat at when we last scrolled to it, not just the ordinal: a - // load-older prepend moves the target down by however many messages arrived above it, so the - // scroll we already issued no longer points at it and has to be re-issued. - const lastScrolledCenteredRef = React.useRef<{index: number; ordinal: T.Chat.Ordinal} | undefined>( - undefined - ) + // Scrolls once per target and no more. Re-issuing when the target's index moves looks reasonable + // - a prepend does shift it - but scrolling is what triggers that prepend, so it re-centers the + // list out from under someone reading around the hit. The list itself keeps the target in place + // while rows measure, and maintainVisibleContentPosition holds it across prepends. + const lastScrolledCenteredRef = React.useRef(undefined) React.useLayoutEffect(() => { lastScrolledCenteredRef.current = undefined }, [datasetKey]) @@ -349,9 +348,8 @@ const DesktopThreadWrapper = function DesktopThreadWrapper() { centeredOrdinal as unknown as number ) if (idx < 0) return - const last = lastScrolledCenteredRef.current - if (last?.ordinal === centeredOrdinal && last.index === idx) return - lastScrolledCenteredRef.current = {index: idx, ordinal: centeredOrdinal} + if (lastScrolledCenteredRef.current === centeredOrdinal) return + lastScrolledCenteredRef.current = centeredOrdinal void listRef.current?.scrollToIndex({animated: false, index: idx, viewPosition: 0.5}) } else if (lastScrolledCenteredRef.current !== undefined) { lastScrolledCenteredRef.current = undefined @@ -671,23 +669,23 @@ const NativeConversationList = function NativeConversationList() { // Center on the search hit once it actually appears in the loaded list. Centering on the raw // centeredOrdinal change is unreliable: navigating to a hit reloads the thread centered on it, // so messageOrdinals is briefly empty (the target not yet present) when the ordinal changes. - // Tracks the index the target sat at, not just the ordinal: the centered load streams older - // messages in above it afterwards, which moves it out from under the scroll we already issued. - const lastCentered = React.useRef<{index: number; ordinal: T.Chat.Ordinal} | undefined>(undefined) + // Scrolls once per target and no more. Re-issuing when the target's index moves looks reasonable + // - a prepend does shift it - but scrolling up is what triggers that prepend, so it re-centers + // the list out from under someone reading around the hit. The list itself keeps the target in + // place while rows measure, and maintainVisibleContentPosition holds it across prepends. + const lastCenteredOrdinal = React.useRef(undefined) React.useEffect(() => { if (centeredOrdinalOrNone <= 0) { - lastCentered.current = undefined + lastCenteredOrdinal.current = undefined return } - const index = messageOrdinals.indexOf(centeredOrdinalOrNone) - if (index < 0) { + if (lastCenteredOrdinal.current === centeredOrdinalOrNone) { return } - const last = lastCentered.current - if (last?.ordinal === centeredOrdinalOrNone && last.index === index) { + if (!messageOrdinals.includes(centeredOrdinalOrNone)) { return } - lastCentered.current = {index, ordinal: centeredOrdinalOrNone} + lastCenteredOrdinal.current = centeredOrdinalOrNone scrollToCentered() }, [centeredOrdinalOrNone, messageOrdinals, scrollToCentered]) diff --git a/shared/patches/@legendapp+list+3.3.5.patch b/shared/patches/@legendapp+list+3.3.5.patch index 66d3a4edf4ad..b9a06de8cceb 100644 --- a/shared/patches/@legendapp+list+3.3.5.patch +++ b/shared/patches/@legendapp+list+3.3.5.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js -index b3c5a30..160d3a1 100644 +index b3c5a30..ae7108d 100644 --- a/node_modules/@legendapp/list/react-native.js +++ b/node_modules/@legendapp/list/react-native.js @@ -457,611 +457,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -1143,10 +1143,20 @@ index b3c5a30..160d3a1 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1977,6 +1775,27 @@ var flushSync = (fn) => { - fn(); - }; +@@ -1972,11 +1770,32 @@ function prepareMVCP(ctx, dataChanged) { + } + } +-// src/platform/flushSync.native.ts +-var flushSync = (fn) => { +- fn(); +-}; +- ++// src/platform/flushSync.native.ts ++var flushSync = (fn) => { ++ fn(); ++}; ++ +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { @@ -1171,16 +1181,16 @@ index b3c5a30..160d3a1 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2063,6 +1882,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { +@@ -2061,6 +1880,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { + state.scrollPrevTime = state.scrollTime; + state.scroll = newScroll; state.scrollTime = currentTime; ++ if ((options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust) { ++ releaseScrollTargetSettleIfMoved(ctx); ++ } const scrollDelta = Math.abs(newScroll - prevScroll); const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; -+ if (isUserScrollEvent) { -+ clearScrollTargetSettle(state); -+ } const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; - const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); - const scrollLength = state.scrollLength; @@ -2224,7 +2046,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } @@ -1215,7 +1225,7 @@ index b3c5a30..160d3a1 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2276,6 +2107,293 @@ function scrollTo(ctx, params) { +@@ -2276,6 +2107,309 @@ function scrollTo(ctx, params) { } } @@ -1239,15 +1249,13 @@ index b3c5a30..160d3a1 100644 + return; + } + const now = Date.now(); -+ const id = getId(state, index); -+ const existing = state.scrollTargetSettle; -+ const isSameTarget = (existing == null ? void 0 : existing.id) === id && existing.viewPosition === viewPosition && existing.viewOffset === viewOffset; + state.scrollTargetSettle = { -+ corrections: isSameTarget ? existing.corrections : 0, -+ deadline: isSameTarget ? existing.deadline : now + SETTLE_MAX_MS, ++ corrections: 0, ++ deadline: now + SETTLE_MAX_MS, + expiresAt: now + SETTLE_TTL_MS, -+ id, ++ id: getId(state, index), + quietPasses: 0, ++ requestedAt: now, + viewOffset, + viewPosition + }; @@ -1257,6 +1265,7 @@ index b3c5a30..160d3a1 100644 + return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} +function applyScrollTargetCorrection(ctx, id) { ++ var _a3; + const state = ctx.state; + const settle = state.scrollTargetSettle; + if (!settle || settle.id !== id) { @@ -1273,18 +1282,20 @@ index b3c5a30..160d3a1 100644 + return; + } + settle.corrections++; ++ settle.requestedAt = Date.now(); + scrollTo(ctx, { + animated: false, + index, + itemSize: getItemSizeAtIndex(ctx, index), -+ // Correcting where a scroll already landed is not a new imperative scroll: leaving -+ // state.scrollingTo alone keeps the list treating subsequent scroll events as the user's, -+ // which is what releases this settle, and keeps edge-reached callbacks firing. ++ // Correcting where a scroll already landed is not a new imperative scroll: claiming the ++ // session would suppress the list's own handling of subsequent scroll events and re-arm ++ // this settle from inside its own correction. + noScrollingTo: true, + offset: position, + viewOffset: settle.viewOffset, + viewPosition: settle.viewPosition + }); ++ (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +} +function settleScrollTarget(ctx, isCompensating) { + const state = ctx.state; @@ -1293,6 +1304,10 @@ index b3c5a30..160d3a1 100644 + return false; + } + if (isCompensating) { ++ if (Date.now() > settle.deadline) { ++ clearScrollTargetSettle(state); ++ return false; ++ } + settle.quietPasses = 0; + settle.expiresAt = Date.now() + SETTLE_TTL_MS; + return false; @@ -1322,6 +1337,17 @@ index b3c5a30..160d3a1 100644 + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} ++var SETTLE_ECHO_WINDOW_MS = 150; ++function releaseScrollTargetSettleIfMoved(ctx, _scrollOffset) { ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ return; ++ } ++ if (Date.now() - settle.requestedAt > SETTLE_ECHO_WINDOW_MS) { ++ clearScrollTargetSettle(state); ++ } ++} + +// src/core/cancelImperativeScroll.ts +function cancelScrollCompletionChecks({ scheduledWork }) { @@ -1509,7 +1535,7 @@ index b3c5a30..160d3a1 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4337,8 +4455,13 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4337,8 +4471,17 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -1517,6 +1543,10 @@ index b3c5a30..160d3a1 100644 - if (didMVCPAdjustScroll) { + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); ++ const mvcp = state.props.maintainVisibleContentPosition; ++ if (dataChanged && !mvcp.data && !mvcp.size) { ++ clearScrollTargetSettle(state); ++ } + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { + settleScrollTarget(ctx, isCompensating); @@ -1525,7 +1555,7 @@ index b3c5a30..160d3a1 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -7652,6 +7775,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7652,6 +7795,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -1534,7 +1564,7 @@ index b3c5a30..160d3a1 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs -index 40e87cd..11a3c24 100644 +index 40e87cd..7ed3902 100644 --- a/node_modules/@legendapp/list/react-native.mjs +++ b/node_modules/@legendapp/list/react-native.mjs @@ -436,611 +436,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -2678,10 +2708,20 @@ index 40e87cd..11a3c24 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1956,6 +1754,27 @@ var flushSync = (fn) => { - fn(); - }; +@@ -1951,11 +1749,32 @@ function prepareMVCP(ctx, dataChanged) { + } + } +-// src/platform/flushSync.native.ts +-var flushSync = (fn) => { +- fn(); +-}; +- ++// src/platform/flushSync.native.ts ++var flushSync = (fn) => { ++ fn(); ++}; ++ +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { @@ -2706,16 +2746,16 @@ index 40e87cd..11a3c24 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2042,6 +1861,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { +@@ -2040,6 +1859,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { + state.scrollPrevTime = state.scrollTime; + state.scroll = newScroll; state.scrollTime = currentTime; ++ if ((options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust) { ++ releaseScrollTargetSettleIfMoved(ctx); ++ } const scrollDelta = Math.abs(newScroll - prevScroll); const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; -+ if (isUserScrollEvent) { -+ clearScrollTargetSettle(state); -+ } const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; - const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); - const scrollLength = state.scrollLength; @@ -2203,7 +2025,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } @@ -2750,7 +2790,7 @@ index 40e87cd..11a3c24 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2255,6 +2086,293 @@ function scrollTo(ctx, params) { +@@ -2255,6 +2086,309 @@ function scrollTo(ctx, params) { } } @@ -2774,15 +2814,13 @@ index 40e87cd..11a3c24 100644 + return; + } + const now = Date.now(); -+ const id = getId(state, index); -+ const existing = state.scrollTargetSettle; -+ const isSameTarget = (existing == null ? void 0 : existing.id) === id && existing.viewPosition === viewPosition && existing.viewOffset === viewOffset; + state.scrollTargetSettle = { -+ corrections: isSameTarget ? existing.corrections : 0, -+ deadline: isSameTarget ? existing.deadline : now + SETTLE_MAX_MS, ++ corrections: 0, ++ deadline: now + SETTLE_MAX_MS, + expiresAt: now + SETTLE_TTL_MS, -+ id, ++ id: getId(state, index), + quietPasses: 0, ++ requestedAt: now, + viewOffset, + viewPosition + }; @@ -2792,6 +2830,7 @@ index 40e87cd..11a3c24 100644 + return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} +function applyScrollTargetCorrection(ctx, id) { ++ var _a3; + const state = ctx.state; + const settle = state.scrollTargetSettle; + if (!settle || settle.id !== id) { @@ -2808,18 +2847,20 @@ index 40e87cd..11a3c24 100644 + return; + } + settle.corrections++; ++ settle.requestedAt = Date.now(); + scrollTo(ctx, { + animated: false, + index, + itemSize: getItemSizeAtIndex(ctx, index), -+ // Correcting where a scroll already landed is not a new imperative scroll: leaving -+ // state.scrollingTo alone keeps the list treating subsequent scroll events as the user's, -+ // which is what releases this settle, and keeps edge-reached callbacks firing. ++ // Correcting where a scroll already landed is not a new imperative scroll: claiming the ++ // session would suppress the list's own handling of subsequent scroll events and re-arm ++ // this settle from inside its own correction. + noScrollingTo: true, + offset: position, + viewOffset: settle.viewOffset, + viewPosition: settle.viewPosition + }); ++ (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +} +function settleScrollTarget(ctx, isCompensating) { + const state = ctx.state; @@ -2828,6 +2869,10 @@ index 40e87cd..11a3c24 100644 + return false; + } + if (isCompensating) { ++ if (Date.now() > settle.deadline) { ++ clearScrollTargetSettle(state); ++ return false; ++ } + settle.quietPasses = 0; + settle.expiresAt = Date.now() + SETTLE_TTL_MS; + return false; @@ -2857,6 +2902,17 @@ index 40e87cd..11a3c24 100644 + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} ++var SETTLE_ECHO_WINDOW_MS = 150; ++function releaseScrollTargetSettleIfMoved(ctx, _scrollOffset) { ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ return; ++ } ++ if (Date.now() - settle.requestedAt > SETTLE_ECHO_WINDOW_MS) { ++ clearScrollTargetSettle(state); ++ } ++} + +// src/core/cancelImperativeScroll.ts +function cancelScrollCompletionChecks({ scheduledWork }) { @@ -3044,7 +3100,7 @@ index 40e87cd..11a3c24 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4316,8 +4434,13 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4316,8 +4450,17 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -3052,6 +3108,10 @@ index 40e87cd..11a3c24 100644 - if (didMVCPAdjustScroll) { + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); ++ const mvcp = state.props.maintainVisibleContentPosition; ++ if (dataChanged && !mvcp.data && !mvcp.size) { ++ clearScrollTargetSettle(state); ++ } + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { + settleScrollTarget(ctx, isCompensating); @@ -3060,7 +3120,7 @@ index 40e87cd..11a3c24 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -7631,6 +7754,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7631,6 +7774,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -3069,7 +3129,7 @@ index 40e87cd..11a3c24 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js -index 914d2da..66763c2 100644 +index 914d2da..ce6a0d7 100644 --- a/node_modules/@legendapp/list/react-native.web.js +++ b/node_modules/@legendapp/list/react-native.web.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -3884,14 +3944,27 @@ index 914d2da..66763c2 100644 + ); + } + } -+} -+ + } + +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +// src/utils/checkAtTop.ts +function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { + const state = ctx == null ? void 0 : ctx.state; + if (!state) { + return; -+ } + } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } + const { + isStartReached, + props: { data, onStartReachedThreshold }, @@ -3906,7 +3979,22 @@ index 914d2da..66763c2 100644 + if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { + state.isStartReached = false; + state.startReachedSnapshot = void 0; -+ } + } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } + set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); + set$(ctx, "isNearStart", scroll <= threshold); + const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; @@ -3933,50 +4021,19 @@ index 914d2da..66763c2 100644 + state.startReachedSnapshot = snapshot; + } + ); -+ } + } +- return offset; } --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; -- } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } -- } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } -- } -- return offset; +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); - } - --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { ++} ++ +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { + var _a3, _b; @@ -3998,10 +4055,30 @@ index 914d2da..66763c2 100644 } // src/core/finishScrollTo.ts -@@ -1425,6 +1041,181 @@ function listenForScrollEnd(ctx, params) { - scheduledWork.register("platformScrollCompletion", cancel); - } - +@@ -1413,16 +1029,191 @@ function listenForScrollEnd(ctx, params) { + if (idleTimeout !== void 0) { + clearTimeout(idleTimeout); + } +- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); +- }; +- const onScrollEnd = () => finish("scrollend"); +- target.addEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.addEventListener("scrollend", onScrollEnd); +- } else { +- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); ++ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); ++ }; ++ const onScrollEnd = () => finish("scrollend"); ++ target.addEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.addEventListener("scrollend", onScrollEnd); ++ } else { ++ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); ++ } ++ scheduledWork.register("platformScrollCompletion", cancel); ++} ++ +// src/core/initialScrollSession.ts +var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +function hasInitialScrollSessionCompletion(completion) { @@ -4174,12 +4251,11 @@ index 914d2da..66763c2 100644 + } else { + resetAdaptiveRender(ctx); + } -+ } -+} -+ + } +- scheduledWork.register("platformScrollCompletion", cancel); + } + // src/core/doMaintainScrollAtEnd.ts - function doMaintainScrollAtEnd(ctx) { - const state = ctx.state; @@ -1451,6 +1242,7 @@ function doMaintainScrollAtEnd(ctx) { const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; @@ -4216,29 +4292,97 @@ index 914d2da..66763c2 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -1908,6 +1721,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { +@@ -1906,6 +1719,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { + state.scrollPrevTime = state.scrollTime; + state.scroll = newScroll; state.scrollTime = currentTime; ++ if ((options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust) { ++ releaseScrollTargetSettleIfMoved(ctx); ++ } const scrollDelta = Math.abs(newScroll - prevScroll); const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; -+ if (isUserScrollEvent) { -+ clearScrollTargetSettle(state); -+ } const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; - const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); - const scrollLength = state.scrollLength; -@@ -2069,7 +1885,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -2026,99 +1842,417 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { + if (start === void 0) { + return void 0; } - } - function scrollTo(ctx, params) { -- var _a3, _b; +- if (targetIndex !== void 0 && state.positions[start] === void 0) { +- return { end: start, start }; ++ if (targetIndex !== void 0 && state.positions[start] === void 0) { ++ return { end: start, start }; ++ } ++ if (targetIndex === void 0) { ++ const startBottom = getItemBottom(ctx, start); ++ if (startBottom === void 0 || startBottom <= viewportStart) { ++ return void 0; ++ } ++ } ++ while (start > 0) { ++ const top = state.positions[start]; ++ if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { ++ break; ++ } ++ start--; ++ } ++ while (start > 0) { ++ const previousBottom = getItemBottom(ctx, start - 1); ++ if (previousBottom === void 0 || previousBottom <= viewportStart) { ++ break; ++ } ++ start--; ++ } ++ let end = start; ++ while (end + 1 < dataLength) { ++ const nextTop = state.positions[end + 1]; ++ if (nextTop === void 0 || nextTop > viewportEnd) { ++ break; ++ } ++ end++; ++ } ++ return { end, start }; ++} ++function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { ++ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); ++ if (range) { ++ ctx.state.scrollTargetPinnedRange = range; ++ ctx.state.scrollForNextCalculateItemsInView = void 0; ++ } else { ++ ctx.state.scrollTargetPinnedRange = void 0; ++ } ++} ++function scrollTo(ctx, params) { + var _a3, _b, _c; - const state = ctx.state; - const { noScrollingTo, forceScroll, ...scrollTarget } = params; - const { -@@ -2101,6 +1917,15 @@ function scrollTo(ctx, params) { - }; - if (!isInitialScroll) { - pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); ++ const state = ctx.state; ++ const { noScrollingTo, forceScroll, ...scrollTarget } = params; ++ const { ++ animated, ++ isInitialScroll, ++ offset: scrollTargetOffset, ++ precomputedWithViewOffset, ++ waitForInitialScrollCompletionFrame ++ } = scrollTarget; ++ const { ++ props: { horizontal } ++ } = state; ++ cancelScrollCompletionChecks(state); ++ const requestedOffset = precomputedWithViewOffset ? scrollTargetOffset : calculateOffsetWithOffsetPosition(ctx, scrollTargetOffset, scrollTarget); ++ const shouldPreserveRawInitialOffsetRequest = !!isInitialScroll && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; ++ const targetOffset = clampScrollOffset(ctx, requestedOffset, scrollTarget); ++ const offset = shouldPreserveRawInitialOffsetRequest ? requestedOffset : targetOffset; ++ state.scrollHistory.length = 0; ++ if (!noScrollingTo) { ++ if (isInitialScroll) { ++ initialScrollCompletion.resetFlags(state); ++ } ++ const averageSizeSnapshot = getAverageSizeSnapshot(state); ++ state.scrollingTo = { ++ ...scrollTarget, ++ ...averageSizeSnapshot ? { averageSizeSnapshot } : {}, ++ targetOffset, ++ waitForInitialScrollCompletionFrame ++ }; ++ if (!isInitialScroll) { ++ pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); + if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { + beginScrollTargetSettle(ctx, { + index: scrollTarget.index, @@ -4248,22 +4392,26 @@ index 914d2da..66763c2 100644 + } else { + clearScrollTargetSettle(state); + } - } - } - state.scrollPending = targetOffset; -@@ -2108,7 +1933,7 @@ function scrollTo(ctx, params) { - if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { - if (animated) { - if (state.scrollTargetPinnedRange) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++ } ++ } ++ state.scrollPending = targetOffset; ++ syncInitialScrollNativeWatchdog(state, { isInitialScroll, requestedOffset: offset, targetOffset }); ++ if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { ++ if (animated) { ++ if (state.scrollTargetPinnedRange) { + (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); - } - } else { - updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2121,6 +1946,299 @@ function scrollTo(ctx, params) { - } - } - ++ } ++ } else { ++ updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); ++ } ++ } ++ if (forceScroll || !isInitialScroll || Platform.OS === "android") { ++ doScrollTo(ctx, { animated, horizontal, offset }); ++ } else { ++ state.scroll = offset; ++ } ++} ++ +// src/core/scrollTargetSettle.ts +var SETTLE_POSITION_EPSILON = 0.5; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; @@ -4284,15 +4432,13 @@ index 914d2da..66763c2 100644 + return; + } + const now = Date.now(); -+ const id = getId(state, index); -+ const existing = state.scrollTargetSettle; -+ const isSameTarget = (existing == null ? void 0 : existing.id) === id && existing.viewPosition === viewPosition && existing.viewOffset === viewOffset; + state.scrollTargetSettle = { -+ corrections: isSameTarget ? existing.corrections : 0, -+ deadline: isSameTarget ? existing.deadline : now + SETTLE_MAX_MS, ++ corrections: 0, ++ deadline: now + SETTLE_MAX_MS, + expiresAt: now + SETTLE_TTL_MS, -+ id, ++ id: getId(state, index), + quietPasses: 0, ++ requestedAt: now, + viewOffset, + viewPosition + }; @@ -4302,6 +4448,7 @@ index 914d2da..66763c2 100644 + return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} +function applyScrollTargetCorrection(ctx, id) { ++ var _a3; + const state = ctx.state; + const settle = state.scrollTargetSettle; + if (!settle || settle.id !== id) { @@ -4318,18 +4465,20 @@ index 914d2da..66763c2 100644 + return; + } + settle.corrections++; ++ settle.requestedAt = Date.now(); + scrollTo(ctx, { + animated: false, + index, + itemSize: getItemSizeAtIndex(ctx, index), -+ // Correcting where a scroll already landed is not a new imperative scroll: leaving -+ // state.scrollingTo alone keeps the list treating subsequent scroll events as the user's, -+ // which is what releases this settle, and keeps edge-reached callbacks firing. ++ // Correcting where a scroll already landed is not a new imperative scroll: claiming the ++ // session would suppress the list's own handling of subsequent scroll events and re-arm ++ // this settle from inside its own correction. + noScrollingTo: true, + offset: position, + viewOffset: settle.viewOffset, + viewPosition: settle.viewPosition + }); ++ (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +} +function settleScrollTarget(ctx, isCompensating) { + const state = ctx.state; @@ -4338,6 +4487,10 @@ index 914d2da..66763c2 100644 + return false; + } + if (isCompensating) { ++ if (Date.now() > settle.deadline) { ++ clearScrollTargetSettle(state); ++ return false; ++ } + settle.quietPasses = 0; + settle.expiresAt = Date.now() + SETTLE_TTL_MS; + return false; @@ -4367,6 +4520,17 @@ index 914d2da..66763c2 100644 + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} ++var SETTLE_ECHO_WINDOW_MS = 150; ++function releaseScrollTargetSettleIfMoved(ctx, _scrollOffset) { ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ return; ++ } ++ if (Date.now() - settle.requestedAt > SETTLE_ECHO_WINDOW_MS) { ++ clearScrollTargetSettle(state); ++ } ++} + +// src/core/cancelImperativeScroll.ts +function cancelScrollCompletionChecks({ scheduledWork }) { @@ -4430,7 +4594,12 @@ index 914d2da..66763c2 100644 + } + if (resetInitialScroll) { + state.didFinishInitialScroll = false; -+ } + } +- if (targetIndex === void 0) { +- const startBottom = getItemBottom(ctx, start); +- if (startBottom === void 0 || startBottom <= viewportStart) { +- return void 0; +- } + set$(ctx, "readyToRender", false); + resetAdaptiveRender(ctx); +} @@ -4445,25 +4614,52 @@ index 914d2da..66763c2 100644 + } = state; + if (didLayout) { + state.didContainersLayout = true; -+ } + } +- while (start > 0) { +- const top = state.positions[start]; +- if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { +- break; +- } +- start--; + if (didInitialScroll) { + state.didFinishInitialScroll = true; -+ } + } +- while (start > 0) { +- const previousBottom = getItemBottom(ctx, start - 1); +- if (previousBottom === void 0 || previousBottom <= viewportStart) { +- break; + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); + if (isReadyToRender && !peek$(ctx, "readyToRender")) { + set$(ctx, "readyToRender", true); + setAdaptiveRender(ctx, "normal", "ready"); + if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { + scheduleFullDrawDistancePrewarm(ctx); -+ } + } +- start--; +- } +- let end = start; +- while (end + 1 < dataLength) { +- const nextTop = state.positions[end + 1]; +- if (nextTop === void 0 || nextTop > viewportEnd) { +- break; + if (!state.didLoad) { + state.didLoad = true; + if (onLoad) { + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); + } -+ } -+ } -+} + } +- end++; + } +- return { end, start }; + } +-function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); +- if (range) { +- ctx.state.scrollTargetPinnedRange = range; +- ctx.state.scrollForNextCalculateItemsInView = void 0; +- } else { +- ctx.state.scrollTargetPinnedRange = void 0; +- } + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -4471,7 +4667,9 @@ index 914d2da..66763c2 100644 + state.scroll = offset; + state.scrollPending = offset; + state.scrollPrev = offset; -+} + } +-function scrollTo(ctx, params) { +- var _a3, _b; +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -4483,12 +4681,41 @@ index 914d2da..66763c2 100644 +} +function supersedeInitialScroll(ctx) { + var _a3, _b, _c; -+ const state = ctx.state; + const state = ctx.state; +- const { noScrollingTo, forceScroll, ...scrollTarget } = params; +- const { +- animated, +- isInitialScroll, +- offset: scrollTargetOffset, +- precomputedWithViewOffset, +- waitForInitialScrollCompletionFrame +- } = scrollTarget; +- const { +- props: { horizontal } +- } = state; +- cancelScrollCompletionChecks(state); +- const requestedOffset = precomputedWithViewOffset ? scrollTargetOffset : calculateOffsetWithOffsetPosition(ctx, scrollTargetOffset, scrollTarget); +- const shouldPreserveRawInitialOffsetRequest = !!isInitialScroll && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; +- const targetOffset = clampScrollOffset(ctx, requestedOffset, scrollTarget); +- const offset = shouldPreserveRawInitialOffsetRequest ? requestedOffset : targetOffset; +- state.scrollHistory.length = 0; +- if (!noScrollingTo) { +- if (isInitialScroll) { +- initialScrollCompletion.resetFlags(state); + const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; + if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { + if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { + cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -+ } + } +- const averageSizeSnapshot = getAverageSizeSnapshot(state); +- state.scrollingTo = { +- ...scrollTarget, +- ...averageSizeSnapshot ? { averageSizeSnapshot } : {}, +- targetOffset, +- waitForInitialScrollCompletionFrame +- }; +- if (!isInitialScroll) { +- pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); + if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { + cancelScrollCompletionChecks(state); + state.scrollingTo = void 0; @@ -4508,8 +4735,14 @@ index 914d2da..66763c2 100644 + const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); + if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { + syncInitialScrollOffset(state, observedOffset); -+ } -+ } + } + } +- state.scrollPending = targetOffset; +- syncInitialScrollNativeWatchdog(state, { isInitialScroll, requestedOffset: offset, targetOffset }); +- if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { +- if (animated) { +- if (state.scrollTargetPinnedRange) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); + const complete = () => { + var _a4, _b2, _c2, _d, _e; + const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; @@ -4531,10 +4764,11 @@ index 914d2da..66763c2 100644 + PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, + "preservedInitialScroll" + ); -+ } -+ } else { + } + } else { +- updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); + clearPreservedInitialScrollTarget(state); -+ } + } + if (options == null ? void 0 : options.recalculateItems) { + recalculateSettledScroll(ctx); + } @@ -4547,7 +4781,12 @@ index 914d2da..66763c2 100644 + if (options == null ? void 0 : options.waitForCompletionFrame) { + requestAnimationFrame(complete); + return; -+ } + } +- if (forceScroll || !isInitialScroll || Platform.OS === "android") { +- doScrollTo(ctx, { animated, horizontal, offset }); +- } else { +- state.scroll = offset; +- } + complete(); +} + @@ -4555,12 +4794,10 @@ index 914d2da..66763c2 100644 +function calculateOffsetForIndex(ctx, index) { + const state = ctx.state; + return index !== void 0 ? state.positions[index] || 0 : 0; -+} -+ + } + // src/core/scrollToIndex.ts - function clampScrollIndex(index, dataLength) { - if (dataLength <= 0) { -@@ -4367,8 +4485,13 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4367,8 +4501,17 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -4568,6 +4805,10 @@ index 914d2da..66763c2 100644 - if (didMVCPAdjustScroll) { + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); ++ const mvcp = state.props.maintainVisibleContentPosition; ++ if (dataChanged && !mvcp.data && !mvcp.size) { ++ clearScrollTargetSettle(state); ++ } + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { + settleScrollTarget(ctx, isCompensating); @@ -4576,115 +4817,324 @@ index 914d2da..66763c2 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -6006,6 +6129,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -5915,6 +6058,119 @@ function useRafCoalescer(callback) { + return coalescer; + } + ++// src/components/temporaryEndPadding.ts ++var entriesByNode = /* @__PURE__ */ new WeakMap(); ++var nextRequestId = 1; ++function readResolvedPadding(node, prop) { ++ return Number.parseFloat(window.getComputedStyle(node)[prop]) || 0; ++} ++function totalRequested(entry) { ++ let total = 0; ++ for (const size of entry.requests.values()) { ++ total += size; ++ } ++ return total; ++} ++function isOwnedByUs(node, prop, entry) { ++ return entry.lastApplied === "" || node.style[prop] === entry.lastApplied; ++} ++function applyPadding(node, prop, entry) { ++ const total = totalRequested(entry); ++ node.style[prop] = total === 0 ? entry.baseline : `${entry.baselineSize + total}px`; ++ entry.lastApplied = node.style[prop]; ++} ++function releaseEntry(node, prop, requestId) { ++ const entries = entriesByNode.get(node); ++ const entry = entries == null ? void 0 : entries[prop]; ++ if (!entry || !entry.requests.delete(requestId)) { ++ return; ++ } ++ if (isOwnedByUs(node, prop, entry)) { ++ applyPadding(node, prop, entry); ++ } ++ if (entry.requests.size === 0) { ++ if (entry.resetHandle !== void 0) { ++ cancelAnimationFrame(entry.resetHandle); ++ } ++ entries == null ? true : delete entries[prop]; ++ } ++} ++function addTemporaryEndPadding(node, prop, extraSize) { ++ var _a3; ++ const entries = (_a3 = entriesByNode.get(node)) != null ? _a3 : {}; ++ let entry = entries[prop]; ++ if (entry && !isOwnedByUs(node, prop, entry)) { ++ entry.baseline = node.style[prop]; ++ entry.baselineSize = readResolvedPadding(node, prop); ++ } ++ if (!entry) { ++ entry = { ++ baseline: node.style[prop], ++ baselineSize: readResolvedPadding(node, prop), ++ lastApplied: "", ++ pendingReleases: /* @__PURE__ */ new Set(), ++ requests: /* @__PURE__ */ new Map(), ++ resetHandle: void 0 ++ }; ++ entries[prop] = entry; ++ entriesByNode.set(node, entries); ++ } ++ const requestId = nextRequestId++; ++ entry.requests.set(requestId, extraSize); ++ applyPadding(node, prop, entry); ++ void node.offsetHeight; ++ return function releaseTemporaryEndPadding() { ++ releaseEntry(node, prop, requestId); ++ }; ++} ++function scheduleTemporaryEndPaddingRelease(node, prop, release) { ++ var _a3; ++ const entry = (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop]; ++ if (!entry) { ++ release(); ++ return; ++ } ++ entry.pendingReleases.add(release); ++ if (entry.resetHandle !== void 0) { ++ return; ++ } ++ entry.resetHandle = requestAnimationFrame(() => { ++ entry.resetHandle = void 0; ++ const releases = [...entry.pendingReleases]; ++ entry.pendingReleases.clear(); ++ for (const pending of releases) { ++ pending(); ++ } ++ }); ++} ++function getTemporaryEndPadding(node, prop) { ++ var _a3; ++ const entry = node ? (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop] : void 0; ++ if (!entry || !isOwnedByUs(node, prop, entry)) { ++ return 0; ++ } ++ return totalRequested(entry); ++} ++function releaseAllTemporaryEndPadding(node) { ++ const entries = entriesByNode.get(node); ++ if (!entries) { ++ return; ++ } ++ for (const prop of Object.keys(entries)) { ++ const entry = entries[prop]; ++ if (!entry) { ++ continue; ++ } ++ if (entry.resetHandle !== void 0) { ++ cancelAnimationFrame(entry.resetHandle); ++ } ++ if (isOwnedByUs(node, prop, entry)) { ++ node.style[prop] = entry.baseline; ++ } ++ delete entries[prop]; ++ } ++} ++ + // src/components/webConstants.ts + var LEGEND_LIST_CONTENT_CONTAINER_CLASS = "legend-list-content-container"; + var LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS = "legend-list-scrollbar-x-hidden"; +@@ -6006,6 +6262,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; -+var SCROLL_CLAMP_EPSILON = 1; -+var SCROLL_CLAMP_RETRY_FRAMES = 10; ++var SCROLL_EXTENT_EPSILON = 1; ++var SMOOTH_SCROLL_MAX_MS = 2e3; var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6094,36 +6219,67 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6074,6 +6332,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), + [isWindowScroll] + ); ++ const paddingEndProp = horizontal ? isHorizontalRTL(ctx.state) ? "paddingLeft" : "paddingRight" : "paddingBottom"; ++ const getCommittedMaxScrollOffset = React3.useCallback( ++ (maxOffset) => Math.max(0, maxOffset - getTemporaryEndPadding(contentRef.current, paddingEndProp)), ++ [paddingEndProp] ++ ); + const getMaxScrollOffset = React3.useCallback(() => { + const scrollElement = scrollRef.current; + const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); +@@ -6094,6 +6357,59 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); -+ const scrollRetryToken = React3.useRef(0); ++ const paddedNodeRef = React3.useRef(null); ++ const animatedPaddingReleaseRef = React3.useRef(void 0); ++ const withReachableExtent = React3.useCallback( ++ (offset, maxOffset, animated, run) => { ++ var _a4; ++ const contentNode = contentRef.current; ++ const committedMaxOffset = getCommittedMaxScrollOffset(maxOffset); ++ if (!contentNode || offset <= committedMaxOffset + SCROLL_EXTENT_EPSILON) { ++ run(clampOffset(offset, maxOffset)); ++ return; ++ } ++ const release = addTemporaryEndPadding( ++ contentNode, ++ paddingEndProp, ++ offset - committedMaxOffset + SCROLL_EXTENT_EPSILON ++ ); ++ paddedNodeRef.current = contentNode; ++ run(offset); ++ if (!animated) { ++ scheduleTemporaryEndPaddingRelease(contentNode, paddingEndProp, release); ++ return; ++ } ++ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); ++ const scrollTarget = getScrollTarget(); ++ const supportsScrollEnd = !!scrollTarget && "onscrollend" in scrollTarget; ++ const finish = () => { ++ if (animatedPaddingReleaseRef.current !== finish) { ++ return; ++ } ++ animatedPaddingReleaseRef.current = void 0; ++ clearTimeout(settleTimeout); ++ scrollTarget == null ? void 0 : scrollTarget.removeEventListener("scrollend", finish); ++ release(); ++ }; ++ animatedPaddingReleaseRef.current = finish; ++ const settleTimeout = setTimeout(finish, SMOOTH_SCROLL_MAX_MS); ++ if (supportsScrollEnd) { ++ scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finish); ++ } ++ }, ++ [getCommittedMaxScrollOffset, getScrollTarget, paddingEndProp] ++ ); + React3.useEffect( + () => () => { -+ scrollRetryToken.current++; ++ var _a4; ++ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); ++ const paddedNode = paddedNodeRef.current; ++ if (paddedNode) { ++ releaseAllTemporaryEndPadding(paddedNode); ++ } + }, + [] + ); const scrollToLocalOffset = React3.useCallback( (offset, animated) => { -- const scrollElement = scrollRef.current; - const target = getScrollTarget(); - if (!target || typeof target.scrollTo !== "function") { - return; - } -- const maxOffset = getMaxScrollOffset(); -- const clampedOffset = clampOffset(offset, maxOffset); - const behavior = animated ? "smooth" : "auto"; -- const options = { behavior }; -- if (isWindowScroll) { -- const scroll = getWindowScrollPosition(); -- const listPos = getElementDocumentPosition(scrollElement, scroll); -- const { left, top } = resolveWindowScrollTarget({ -- clampedOffset, -- horizontal, -- listPos, -- scroll -+ const token = ++scrollRetryToken.current; -+ let previousMaxOffset = -1; -+ const apply = (attempt) => { -+ if (token !== scrollRetryToken.current || !scrollRef.current) { -+ return; -+ } -+ const scrollElement = scrollRef.current; -+ const maxOffset = getMaxScrollOffset(); -+ const didExtentGrow = maxOffset > previousMaxOffset; -+ previousMaxOffset = maxOffset; -+ const clampedOffset = clampOffset(offset, maxOffset); -+ const options = { behavior }; -+ if (isWindowScroll) { -+ const scroll = getWindowScrollPosition(); -+ const listPos = getElementDocumentPosition(scrollElement, scroll); -+ const { left, top } = resolveWindowScrollTarget({ -+ clampedOffset, -+ horizontal, -+ listPos, -+ scroll -+ }); -+ options.left = left; -+ options.top = top; -+ } else if (horizontal) { -+ options.left = clampedOffset; -+ } else { -+ options.top = clampedOffset; -+ } -+ target.scrollTo(options); -+ const landedOffset = getCurrentScrollOffset(); -+ const isRequestOutOfReach = clampedOffset < offset - SCROLL_CLAMP_EPSILON; -+ if (animated || !isRequestOutOfReach || !didExtentGrow || attempt >= SCROLL_CLAMP_RETRY_FRAMES) { -+ return; -+ } -+ requestAnimationFrame(() => { -+ if (token !== scrollRetryToken.current || !scrollRef.current) { -+ return; -+ } -+ if (Math.abs(getCurrentScrollOffset() - landedOffset) > SCROLL_CLAMP_EPSILON) { -+ return; -+ } -+ apply(attempt + 1); + const scrollElement = scrollRef.current; +@@ -6116,14 +6432,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); -- options.left = left; -- options.top = top; + options.left = left; + options.top = top; - } else if (horizontal) { - options.left = clampedOffset; -- } else { ++ } ++ if (isWindowScroll) { ++ target.scrollTo(options); + } else { - options.top = clampedOffset; -- } ++ withReachableExtent(offset, maxOffset, animated, (reachableOffset) => { ++ if (horizontal) { ++ options.left = reachableOffset; ++ } else { ++ options.top = reachableOffset; ++ } ++ target.scrollTo(options); ++ }); + } - target.scrollTo(options); -+ }; -+ apply(0); }, - [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] -+ [getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] ++ [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, withReachableExtent] ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6305,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6149,8 +6472,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; - const endOffset = getMaxScrollOffset(); - scrollToLocalOffset(endOffset, animated); -+ scrollToLocalOffset(Number.POSITIVE_INFINITY, animated); ++ scrollToLocalOffset(getCommittedMaxScrollOffset(getMaxScrollOffset()), animated); }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -8288,6 +8443,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6163,7 +6485,12 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + if (!onScroll2 || !scrollRef.current) { + return; + } +- const contentSize = getContentSize2(contentRef.current); ++ const rawContentSize = getContentSize2(contentRef.current); ++ const temporaryPadding = getTemporaryEndPadding(contentRef.current, paddingEndProp); ++ const contentSize = temporaryPadding ? { ++ height: horizontal ? rawContentSize.height : rawContentSize.height - temporaryPadding, ++ width: horizontal ? rawContentSize.width - temporaryPadding : rawContentSize.width ++ } : rawContentSize; + const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); + const offset = getCurrentScrollOffset(); + const scrollEvent = { +@@ -6379,10 +6706,12 @@ function useValueListener$(key, callback) { + } + + // src/components/ScrollAdjust.tsx +-function getScrollAdjustAxis(horizontal) { ++function getScrollAdjustAxis(horizontal, rtl = false) { + return horizontal ? { + contentSizeKey: "scrollWidth", +- paddingEndProp: "paddingRight", ++ // The end side under RTL is the left, matching how the list pads its own content and ++ // which property the scroll view borrows room on. ++ paddingEndProp: rtl ? "paddingLeft" : "paddingRight", + viewportSizeKey: "clientWidth", + x: 1, + y: 0 +@@ -6411,8 +6740,6 @@ function ScrollAdjust() { + const ctx = useStateContext(); + const lastScrollOffsetRef = React3__namespace.useRef(0); + const lastScrollAdjustUserOffsetRef = React3__namespace.useRef(0); +- const resetPaddingRafRef = React3__namespace.useRef(void 0); +- const temporaryPaddingRef = React3__namespace.useRef(void 0); + const contentNodeRef = React3__namespace.useRef(null); + const callback = React3__namespace.useCallback(() => { + const scrollAdjust = peek$(ctx, "scrollAdjust"); +@@ -6423,7 +6750,7 @@ function ScrollAdjust() { + const target = getScrollAdjustTarget(ctx, contentNodeRef.current); + if (target) { + const horizontal = !!ctx.state.props.horizontal; +- const axis = getScrollAdjustAxis(horizontal); ++ const axis = getScrollAdjustAxis(horizontal, isHorizontalRTL(ctx.state)); + const { contentNode, scrollElement: el } = target; + const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; + const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; +@@ -6438,29 +6765,10 @@ function ScrollAdjust() { + const nextScroll = currentScroll + scrollDelta; + const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; + if (needsTemporaryPadding) { +- const currentInlinePaddingEnd = contentNode.style[axis.paddingEndProp]; +- const previousTemporaryPadding = temporaryPaddingRef.current; +- const baselinePaddingEnd = (previousTemporaryPadding == null ? void 0 : previousTemporaryPadding.value) === currentInlinePaddingEnd ? previousTemporaryPadding.baseline : currentInlinePaddingEnd; + const pad = (nextScroll + viewportSize - totalSize) * 2; +- const currentPaddingEnd = Number.parseFloat( +- window.getComputedStyle(contentNode)[axis.paddingEndProp] +- ); +- const temporaryPaddingEnd = `${(currentPaddingEnd || 0) + pad}px`; +- temporaryPaddingRef.current = { baseline: baselinePaddingEnd, value: temporaryPaddingEnd }; +- contentNode.style[axis.paddingEndProp] = temporaryPaddingEnd; +- void contentNode.offsetHeight; ++ const release = addTemporaryEndPadding(contentNode, axis.paddingEndProp, pad); + scrollBy(); +- if (resetPaddingRafRef.current !== void 0) { +- cancelAnimationFrame(resetPaddingRafRef.current); +- } +- resetPaddingRafRef.current = requestAnimationFrame(() => { +- const temporaryPadding = temporaryPaddingRef.current; +- resetPaddingRafRef.current = void 0; +- temporaryPaddingRef.current = void 0; +- if (contentNode.style[axis.paddingEndProp] === (temporaryPadding == null ? void 0 : temporaryPadding.value)) { +- contentNode.style[axis.paddingEndProp] = temporaryPadding.baseline; +- } +- }); ++ scheduleTemporaryEndPaddingRelease(contentNode, axis.paddingEndProp, release); + } else { + scrollBy(); + } +@@ -8288,6 +8596,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -4693,7 +5143,7 @@ index 914d2da..66763c2 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs -index 95465f2..5669ad7 100644 +index 95465f2..b028ddc 100644 --- a/node_modules/@legendapp/list/react-native.web.mjs +++ b/node_modules/@legendapp/list/react-native.web.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -5508,14 +5958,27 @@ index 95465f2..5669ad7 100644 + ); + } + } -+} -+ + } + +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +// src/utils/checkAtTop.ts +function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { + const state = ctx == null ? void 0 : ctx.state; + if (!state) { + return; -+ } + } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } + const { + isStartReached, + props: { data, onStartReachedThreshold }, @@ -5530,7 +5993,22 @@ index 95465f2..5669ad7 100644 + if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { + state.isStartReached = false; + state.startReachedSnapshot = void 0; -+ } + } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } + set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); + set$(ctx, "isNearStart", scroll <= threshold); + const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; @@ -5557,50 +6035,19 @@ index 95465f2..5669ad7 100644 + state.startReachedSnapshot = snapshot; + } + ); -+ } + } +- return offset; } --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; -- } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } -- } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } -- } -- return offset; +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); - } - --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { ++} ++ +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { + var _a3, _b; @@ -5622,10 +6069,30 @@ index 95465f2..5669ad7 100644 } // src/core/finishScrollTo.ts -@@ -1404,6 +1020,181 @@ function listenForScrollEnd(ctx, params) { - scheduledWork.register("platformScrollCompletion", cancel); - } - +@@ -1392,16 +1008,191 @@ function listenForScrollEnd(ctx, params) { + if (idleTimeout !== void 0) { + clearTimeout(idleTimeout); + } +- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); +- }; +- const onScrollEnd = () => finish("scrollend"); +- target.addEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.addEventListener("scrollend", onScrollEnd); +- } else { +- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); ++ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); ++ }; ++ const onScrollEnd = () => finish("scrollend"); ++ target.addEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.addEventListener("scrollend", onScrollEnd); ++ } else { ++ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); ++ } ++ scheduledWork.register("platformScrollCompletion", cancel); ++} ++ +// src/core/initialScrollSession.ts +var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +function hasInitialScrollSessionCompletion(completion) { @@ -5798,12 +6265,11 @@ index 95465f2..5669ad7 100644 + } else { + resetAdaptiveRender(ctx); + } -+ } -+} -+ + } +- scheduledWork.register("platformScrollCompletion", cancel); + } + // src/core/doMaintainScrollAtEnd.ts - function doMaintainScrollAtEnd(ctx) { - const state = ctx.state; @@ -1430,6 +1221,7 @@ function doMaintainScrollAtEnd(ctx) { const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; @@ -5840,29 +6306,97 @@ index 95465f2..5669ad7 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -1887,6 +1700,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { +@@ -1885,6 +1698,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { + state.scrollPrevTime = state.scrollTime; + state.scroll = newScroll; state.scrollTime = currentTime; ++ if ((options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust) { ++ releaseScrollTargetSettleIfMoved(ctx); ++ } const scrollDelta = Math.abs(newScroll - prevScroll); const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; -+ if (isUserScrollEvent) { -+ clearScrollTargetSettle(state); -+ } const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; - const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); - const scrollLength = state.scrollLength; -@@ -2048,7 +1864,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -2005,99 +1821,417 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { + if (start === void 0) { + return void 0; } - } - function scrollTo(ctx, params) { -- var _a3, _b; +- if (targetIndex !== void 0 && state.positions[start] === void 0) { +- return { end: start, start }; ++ if (targetIndex !== void 0 && state.positions[start] === void 0) { ++ return { end: start, start }; ++ } ++ if (targetIndex === void 0) { ++ const startBottom = getItemBottom(ctx, start); ++ if (startBottom === void 0 || startBottom <= viewportStart) { ++ return void 0; ++ } ++ } ++ while (start > 0) { ++ const top = state.positions[start]; ++ if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { ++ break; ++ } ++ start--; ++ } ++ while (start > 0) { ++ const previousBottom = getItemBottom(ctx, start - 1); ++ if (previousBottom === void 0 || previousBottom <= viewportStart) { ++ break; ++ } ++ start--; ++ } ++ let end = start; ++ while (end + 1 < dataLength) { ++ const nextTop = state.positions[end + 1]; ++ if (nextTop === void 0 || nextTop > viewportEnd) { ++ break; ++ } ++ end++; ++ } ++ return { end, start }; ++} ++function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { ++ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); ++ if (range) { ++ ctx.state.scrollTargetPinnedRange = range; ++ ctx.state.scrollForNextCalculateItemsInView = void 0; ++ } else { ++ ctx.state.scrollTargetPinnedRange = void 0; ++ } ++} ++function scrollTo(ctx, params) { + var _a3, _b, _c; - const state = ctx.state; - const { noScrollingTo, forceScroll, ...scrollTarget } = params; - const { -@@ -2080,6 +1896,15 @@ function scrollTo(ctx, params) { - }; - if (!isInitialScroll) { - pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); ++ const state = ctx.state; ++ const { noScrollingTo, forceScroll, ...scrollTarget } = params; ++ const { ++ animated, ++ isInitialScroll, ++ offset: scrollTargetOffset, ++ precomputedWithViewOffset, ++ waitForInitialScrollCompletionFrame ++ } = scrollTarget; ++ const { ++ props: { horizontal } ++ } = state; ++ cancelScrollCompletionChecks(state); ++ const requestedOffset = precomputedWithViewOffset ? scrollTargetOffset : calculateOffsetWithOffsetPosition(ctx, scrollTargetOffset, scrollTarget); ++ const shouldPreserveRawInitialOffsetRequest = !!isInitialScroll && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; ++ const targetOffset = clampScrollOffset(ctx, requestedOffset, scrollTarget); ++ const offset = shouldPreserveRawInitialOffsetRequest ? requestedOffset : targetOffset; ++ state.scrollHistory.length = 0; ++ if (!noScrollingTo) { ++ if (isInitialScroll) { ++ initialScrollCompletion.resetFlags(state); ++ } ++ const averageSizeSnapshot = getAverageSizeSnapshot(state); ++ state.scrollingTo = { ++ ...scrollTarget, ++ ...averageSizeSnapshot ? { averageSizeSnapshot } : {}, ++ targetOffset, ++ waitForInitialScrollCompletionFrame ++ }; ++ if (!isInitialScroll) { ++ pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); + if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { + beginScrollTargetSettle(ctx, { + index: scrollTarget.index, @@ -5872,22 +6406,26 @@ index 95465f2..5669ad7 100644 + } else { + clearScrollTargetSettle(state); + } - } - } - state.scrollPending = targetOffset; -@@ -2087,7 +1912,7 @@ function scrollTo(ctx, params) { - if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { - if (animated) { - if (state.scrollTargetPinnedRange) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++ } ++ } ++ state.scrollPending = targetOffset; ++ syncInitialScrollNativeWatchdog(state, { isInitialScroll, requestedOffset: offset, targetOffset }); ++ if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { ++ if (animated) { ++ if (state.scrollTargetPinnedRange) { + (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); - } - } else { - updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2100,6 +1925,299 @@ function scrollTo(ctx, params) { - } - } - ++ } ++ } else { ++ updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); ++ } ++ } ++ if (forceScroll || !isInitialScroll || Platform.OS === "android") { ++ doScrollTo(ctx, { animated, horizontal, offset }); ++ } else { ++ state.scroll = offset; ++ } ++} ++ +// src/core/scrollTargetSettle.ts +var SETTLE_POSITION_EPSILON = 0.5; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; @@ -5908,15 +6446,13 @@ index 95465f2..5669ad7 100644 + return; + } + const now = Date.now(); -+ const id = getId(state, index); -+ const existing = state.scrollTargetSettle; -+ const isSameTarget = (existing == null ? void 0 : existing.id) === id && existing.viewPosition === viewPosition && existing.viewOffset === viewOffset; + state.scrollTargetSettle = { -+ corrections: isSameTarget ? existing.corrections : 0, -+ deadline: isSameTarget ? existing.deadline : now + SETTLE_MAX_MS, ++ corrections: 0, ++ deadline: now + SETTLE_MAX_MS, + expiresAt: now + SETTLE_TTL_MS, -+ id, ++ id: getId(state, index), + quietPasses: 0, ++ requestedAt: now, + viewOffset, + viewPosition + }; @@ -5926,6 +6462,7 @@ index 95465f2..5669ad7 100644 + return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} +function applyScrollTargetCorrection(ctx, id) { ++ var _a3; + const state = ctx.state; + const settle = state.scrollTargetSettle; + if (!settle || settle.id !== id) { @@ -5942,18 +6479,20 @@ index 95465f2..5669ad7 100644 + return; + } + settle.corrections++; ++ settle.requestedAt = Date.now(); + scrollTo(ctx, { + animated: false, + index, + itemSize: getItemSizeAtIndex(ctx, index), -+ // Correcting where a scroll already landed is not a new imperative scroll: leaving -+ // state.scrollingTo alone keeps the list treating subsequent scroll events as the user's, -+ // which is what releases this settle, and keeps edge-reached callbacks firing. ++ // Correcting where a scroll already landed is not a new imperative scroll: claiming the ++ // session would suppress the list's own handling of subsequent scroll events and re-arm ++ // this settle from inside its own correction. + noScrollingTo: true, + offset: position, + viewOffset: settle.viewOffset, + viewPosition: settle.viewPosition + }); ++ (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +} +function settleScrollTarget(ctx, isCompensating) { + const state = ctx.state; @@ -5962,6 +6501,10 @@ index 95465f2..5669ad7 100644 + return false; + } + if (isCompensating) { ++ if (Date.now() > settle.deadline) { ++ clearScrollTargetSettle(state); ++ return false; ++ } + settle.quietPasses = 0; + settle.expiresAt = Date.now() + SETTLE_TTL_MS; + return false; @@ -5991,6 +6534,17 @@ index 95465f2..5669ad7 100644 + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} ++var SETTLE_ECHO_WINDOW_MS = 150; ++function releaseScrollTargetSettleIfMoved(ctx, _scrollOffset) { ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ return; ++ } ++ if (Date.now() - settle.requestedAt > SETTLE_ECHO_WINDOW_MS) { ++ clearScrollTargetSettle(state); ++ } ++} + +// src/core/cancelImperativeScroll.ts +function cancelScrollCompletionChecks({ scheduledWork }) { @@ -6054,7 +6608,12 @@ index 95465f2..5669ad7 100644 + } + if (resetInitialScroll) { + state.didFinishInitialScroll = false; -+ } + } +- if (targetIndex === void 0) { +- const startBottom = getItemBottom(ctx, start); +- if (startBottom === void 0 || startBottom <= viewportStart) { +- return void 0; +- } + set$(ctx, "readyToRender", false); + resetAdaptiveRender(ctx); +} @@ -6069,25 +6628,52 @@ index 95465f2..5669ad7 100644 + } = state; + if (didLayout) { + state.didContainersLayout = true; -+ } + } +- while (start > 0) { +- const top = state.positions[start]; +- if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { +- break; +- } +- start--; + if (didInitialScroll) { + state.didFinishInitialScroll = true; -+ } + } +- while (start > 0) { +- const previousBottom = getItemBottom(ctx, start - 1); +- if (previousBottom === void 0 || previousBottom <= viewportStart) { +- break; + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); + if (isReadyToRender && !peek$(ctx, "readyToRender")) { + set$(ctx, "readyToRender", true); + setAdaptiveRender(ctx, "normal", "ready"); + if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { + scheduleFullDrawDistancePrewarm(ctx); -+ } + } +- start--; +- } +- let end = start; +- while (end + 1 < dataLength) { +- const nextTop = state.positions[end + 1]; +- if (nextTop === void 0 || nextTop > viewportEnd) { +- break; + if (!state.didLoad) { + state.didLoad = true; + if (onLoad) { + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); + } -+ } -+ } -+} + } +- end++; + } +- return { end, start }; + } +-function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); +- if (range) { +- ctx.state.scrollTargetPinnedRange = range; +- ctx.state.scrollForNextCalculateItemsInView = void 0; +- } else { +- ctx.state.scrollTargetPinnedRange = void 0; +- } + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -6095,7 +6681,9 @@ index 95465f2..5669ad7 100644 + state.scroll = offset; + state.scrollPending = offset; + state.scrollPrev = offset; -+} + } +-function scrollTo(ctx, params) { +- var _a3, _b; +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -6107,12 +6695,41 @@ index 95465f2..5669ad7 100644 +} +function supersedeInitialScroll(ctx) { + var _a3, _b, _c; -+ const state = ctx.state; -+ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; -+ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { + const state = ctx.state; +- const { noScrollingTo, forceScroll, ...scrollTarget } = params; +- const { +- animated, +- isInitialScroll, +- offset: scrollTargetOffset, +- precomputedWithViewOffset, +- waitForInitialScrollCompletionFrame +- } = scrollTarget; +- const { +- props: { horizontal } +- } = state; +- cancelScrollCompletionChecks(state); +- const requestedOffset = precomputedWithViewOffset ? scrollTargetOffset : calculateOffsetWithOffsetPosition(ctx, scrollTargetOffset, scrollTarget); +- const shouldPreserveRawInitialOffsetRequest = !!isInitialScroll && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; +- const targetOffset = clampScrollOffset(ctx, requestedOffset, scrollTarget); +- const offset = shouldPreserveRawInitialOffsetRequest ? requestedOffset : targetOffset; +- state.scrollHistory.length = 0; +- if (!noScrollingTo) { +- if (isInitialScroll) { +- initialScrollCompletion.resetFlags(state); ++ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; ++ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { + if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { + cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -+ } + } +- const averageSizeSnapshot = getAverageSizeSnapshot(state); +- state.scrollingTo = { +- ...scrollTarget, +- ...averageSizeSnapshot ? { averageSizeSnapshot } : {}, +- targetOffset, +- waitForInitialScrollCompletionFrame +- }; +- if (!isInitialScroll) { +- pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); + if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { + cancelScrollCompletionChecks(state); + state.scrollingTo = void 0; @@ -6132,8 +6749,14 @@ index 95465f2..5669ad7 100644 + const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); + if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { + syncInitialScrollOffset(state, observedOffset); -+ } -+ } + } + } +- state.scrollPending = targetOffset; +- syncInitialScrollNativeWatchdog(state, { isInitialScroll, requestedOffset: offset, targetOffset }); +- if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { +- if (animated) { +- if (state.scrollTargetPinnedRange) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); + const complete = () => { + var _a4, _b2, _c2, _d, _e; + const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; @@ -6155,10 +6778,11 @@ index 95465f2..5669ad7 100644 + PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, + "preservedInitialScroll" + ); -+ } -+ } else { + } + } else { +- updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); + clearPreservedInitialScrollTarget(state); -+ } + } + if (options == null ? void 0 : options.recalculateItems) { + recalculateSettledScroll(ctx); + } @@ -6171,7 +6795,12 @@ index 95465f2..5669ad7 100644 + if (options == null ? void 0 : options.waitForCompletionFrame) { + requestAnimationFrame(complete); + return; -+ } + } +- if (forceScroll || !isInitialScroll || Platform.OS === "android") { +- doScrollTo(ctx, { animated, horizontal, offset }); +- } else { +- state.scroll = offset; +- } + complete(); +} + @@ -6179,12 +6808,10 @@ index 95465f2..5669ad7 100644 +function calculateOffsetForIndex(ctx, index) { + const state = ctx.state; + return index !== void 0 ? state.positions[index] || 0 : 0; -+} -+ + } + // src/core/scrollToIndex.ts - function clampScrollIndex(index, dataLength) { - if (dataLength <= 0) { -@@ -4346,8 +4464,13 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4346,8 +4480,17 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -6192,6 +6819,10 @@ index 95465f2..5669ad7 100644 - if (didMVCPAdjustScroll) { + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); ++ const mvcp = state.props.maintainVisibleContentPosition; ++ if (dataChanged && !mvcp.data && !mvcp.size) { ++ clearScrollTargetSettle(state); ++ } + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { + settleScrollTarget(ctx, isCompensating); @@ -6200,115 +6831,324 @@ index 95465f2..5669ad7 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5985,6 +6108,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -5894,6 +6037,119 @@ function useRafCoalescer(callback) { + return coalescer; + } + ++// src/components/temporaryEndPadding.ts ++var entriesByNode = /* @__PURE__ */ new WeakMap(); ++var nextRequestId = 1; ++function readResolvedPadding(node, prop) { ++ return Number.parseFloat(window.getComputedStyle(node)[prop]) || 0; ++} ++function totalRequested(entry) { ++ let total = 0; ++ for (const size of entry.requests.values()) { ++ total += size; ++ } ++ return total; ++} ++function isOwnedByUs(node, prop, entry) { ++ return entry.lastApplied === "" || node.style[prop] === entry.lastApplied; ++} ++function applyPadding(node, prop, entry) { ++ const total = totalRequested(entry); ++ node.style[prop] = total === 0 ? entry.baseline : `${entry.baselineSize + total}px`; ++ entry.lastApplied = node.style[prop]; ++} ++function releaseEntry(node, prop, requestId) { ++ const entries = entriesByNode.get(node); ++ const entry = entries == null ? void 0 : entries[prop]; ++ if (!entry || !entry.requests.delete(requestId)) { ++ return; ++ } ++ if (isOwnedByUs(node, prop, entry)) { ++ applyPadding(node, prop, entry); ++ } ++ if (entry.requests.size === 0) { ++ if (entry.resetHandle !== void 0) { ++ cancelAnimationFrame(entry.resetHandle); ++ } ++ entries == null ? true : delete entries[prop]; ++ } ++} ++function addTemporaryEndPadding(node, prop, extraSize) { ++ var _a3; ++ const entries = (_a3 = entriesByNode.get(node)) != null ? _a3 : {}; ++ let entry = entries[prop]; ++ if (entry && !isOwnedByUs(node, prop, entry)) { ++ entry.baseline = node.style[prop]; ++ entry.baselineSize = readResolvedPadding(node, prop); ++ } ++ if (!entry) { ++ entry = { ++ baseline: node.style[prop], ++ baselineSize: readResolvedPadding(node, prop), ++ lastApplied: "", ++ pendingReleases: /* @__PURE__ */ new Set(), ++ requests: /* @__PURE__ */ new Map(), ++ resetHandle: void 0 ++ }; ++ entries[prop] = entry; ++ entriesByNode.set(node, entries); ++ } ++ const requestId = nextRequestId++; ++ entry.requests.set(requestId, extraSize); ++ applyPadding(node, prop, entry); ++ void node.offsetHeight; ++ return function releaseTemporaryEndPadding() { ++ releaseEntry(node, prop, requestId); ++ }; ++} ++function scheduleTemporaryEndPaddingRelease(node, prop, release) { ++ var _a3; ++ const entry = (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop]; ++ if (!entry) { ++ release(); ++ return; ++ } ++ entry.pendingReleases.add(release); ++ if (entry.resetHandle !== void 0) { ++ return; ++ } ++ entry.resetHandle = requestAnimationFrame(() => { ++ entry.resetHandle = void 0; ++ const releases = [...entry.pendingReleases]; ++ entry.pendingReleases.clear(); ++ for (const pending of releases) { ++ pending(); ++ } ++ }); ++} ++function getTemporaryEndPadding(node, prop) { ++ var _a3; ++ const entry = node ? (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop] : void 0; ++ if (!entry || !isOwnedByUs(node, prop, entry)) { ++ return 0; ++ } ++ return totalRequested(entry); ++} ++function releaseAllTemporaryEndPadding(node) { ++ const entries = entriesByNode.get(node); ++ if (!entries) { ++ return; ++ } ++ for (const prop of Object.keys(entries)) { ++ const entry = entries[prop]; ++ if (!entry) { ++ continue; ++ } ++ if (entry.resetHandle !== void 0) { ++ cancelAnimationFrame(entry.resetHandle); ++ } ++ if (isOwnedByUs(node, prop, entry)) { ++ node.style[prop] = entry.baseline; ++ } ++ delete entries[prop]; ++ } ++} ++ + // src/components/webConstants.ts + var LEGEND_LIST_CONTENT_CONTAINER_CLASS = "legend-list-content-container"; + var LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS = "legend-list-scrollbar-x-hidden"; +@@ -5985,6 +6241,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; -+var SCROLL_CLAMP_EPSILON = 1; -+var SCROLL_CLAMP_RETRY_FRAMES = 10; ++var SCROLL_EXTENT_EPSILON = 1; ++var SMOOTH_SCROLL_MAX_MS = 2e3; var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6073,36 +6198,67 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6053,6 +6311,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), + [isWindowScroll] + ); ++ const paddingEndProp = horizontal ? isHorizontalRTL(ctx.state) ? "paddingLeft" : "paddingRight" : "paddingBottom"; ++ const getCommittedMaxScrollOffset = useCallback( ++ (maxOffset) => Math.max(0, maxOffset - getTemporaryEndPadding(contentRef.current, paddingEndProp)), ++ [paddingEndProp] ++ ); + const getMaxScrollOffset = useCallback(() => { + const scrollElement = scrollRef.current; + const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); +@@ -6073,6 +6336,59 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); -+ const scrollRetryToken = useRef(0); ++ const paddedNodeRef = useRef(null); ++ const animatedPaddingReleaseRef = useRef(void 0); ++ const withReachableExtent = useCallback( ++ (offset, maxOffset, animated, run) => { ++ var _a4; ++ const contentNode = contentRef.current; ++ const committedMaxOffset = getCommittedMaxScrollOffset(maxOffset); ++ if (!contentNode || offset <= committedMaxOffset + SCROLL_EXTENT_EPSILON) { ++ run(clampOffset(offset, maxOffset)); ++ return; ++ } ++ const release = addTemporaryEndPadding( ++ contentNode, ++ paddingEndProp, ++ offset - committedMaxOffset + SCROLL_EXTENT_EPSILON ++ ); ++ paddedNodeRef.current = contentNode; ++ run(offset); ++ if (!animated) { ++ scheduleTemporaryEndPaddingRelease(contentNode, paddingEndProp, release); ++ return; ++ } ++ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); ++ const scrollTarget = getScrollTarget(); ++ const supportsScrollEnd = !!scrollTarget && "onscrollend" in scrollTarget; ++ const finish = () => { ++ if (animatedPaddingReleaseRef.current !== finish) { ++ return; ++ } ++ animatedPaddingReleaseRef.current = void 0; ++ clearTimeout(settleTimeout); ++ scrollTarget == null ? void 0 : scrollTarget.removeEventListener("scrollend", finish); ++ release(); ++ }; ++ animatedPaddingReleaseRef.current = finish; ++ const settleTimeout = setTimeout(finish, SMOOTH_SCROLL_MAX_MS); ++ if (supportsScrollEnd) { ++ scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finish); ++ } ++ }, ++ [getCommittedMaxScrollOffset, getScrollTarget, paddingEndProp] ++ ); + useEffect( + () => () => { -+ scrollRetryToken.current++; ++ var _a4; ++ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); ++ const paddedNode = paddedNodeRef.current; ++ if (paddedNode) { ++ releaseAllTemporaryEndPadding(paddedNode); ++ } + }, + [] + ); const scrollToLocalOffset = useCallback( (offset, animated) => { -- const scrollElement = scrollRef.current; - const target = getScrollTarget(); - if (!target || typeof target.scrollTo !== "function") { - return; - } -- const maxOffset = getMaxScrollOffset(); -- const clampedOffset = clampOffset(offset, maxOffset); - const behavior = animated ? "smooth" : "auto"; -- const options = { behavior }; -- if (isWindowScroll) { -- const scroll = getWindowScrollPosition(); -- const listPos = getElementDocumentPosition(scrollElement, scroll); -- const { left, top } = resolveWindowScrollTarget({ -- clampedOffset, -- horizontal, -- listPos, -- scroll -+ const token = ++scrollRetryToken.current; -+ let previousMaxOffset = -1; -+ const apply = (attempt) => { -+ if (token !== scrollRetryToken.current || !scrollRef.current) { -+ return; -+ } -+ const scrollElement = scrollRef.current; -+ const maxOffset = getMaxScrollOffset(); -+ const didExtentGrow = maxOffset > previousMaxOffset; -+ previousMaxOffset = maxOffset; -+ const clampedOffset = clampOffset(offset, maxOffset); -+ const options = { behavior }; -+ if (isWindowScroll) { -+ const scroll = getWindowScrollPosition(); -+ const listPos = getElementDocumentPosition(scrollElement, scroll); -+ const { left, top } = resolveWindowScrollTarget({ -+ clampedOffset, -+ horizontal, -+ listPos, -+ scroll -+ }); -+ options.left = left; -+ options.top = top; -+ } else if (horizontal) { -+ options.left = clampedOffset; -+ } else { -+ options.top = clampedOffset; -+ } -+ target.scrollTo(options); -+ const landedOffset = getCurrentScrollOffset(); -+ const isRequestOutOfReach = clampedOffset < offset - SCROLL_CLAMP_EPSILON; -+ if (animated || !isRequestOutOfReach || !didExtentGrow || attempt >= SCROLL_CLAMP_RETRY_FRAMES) { -+ return; -+ } -+ requestAnimationFrame(() => { -+ if (token !== scrollRetryToken.current || !scrollRef.current) { -+ return; -+ } -+ if (Math.abs(getCurrentScrollOffset() - landedOffset) > SCROLL_CLAMP_EPSILON) { -+ return; -+ } -+ apply(attempt + 1); + const scrollElement = scrollRef.current; +@@ -6095,14 +6411,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); -- options.left = left; -- options.top = top; + options.left = left; + options.top = top; - } else if (horizontal) { - options.left = clampedOffset; -- } else { ++ } ++ if (isWindowScroll) { ++ target.scrollTo(options); + } else { - options.top = clampedOffset; -- } ++ withReachableExtent(offset, maxOffset, animated, (reachableOffset) => { ++ if (horizontal) { ++ options.left = reachableOffset; ++ } else { ++ options.top = reachableOffset; ++ } ++ target.scrollTo(options); ++ }); + } - target.scrollTo(options); -+ }; -+ apply(0); }, - [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] -+ [getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] ++ [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, withReachableExtent] ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6284,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6128,8 +6451,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; - const endOffset = getMaxScrollOffset(); - scrollToLocalOffset(endOffset, animated); -+ scrollToLocalOffset(Number.POSITIVE_INFINITY, animated); ++ scrollToLocalOffset(getCommittedMaxScrollOffset(getMaxScrollOffset()), animated); }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -8267,6 +8422,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6142,7 +6464,12 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + if (!onScroll2 || !scrollRef.current) { + return; + } +- const contentSize = getContentSize2(contentRef.current); ++ const rawContentSize = getContentSize2(contentRef.current); ++ const temporaryPadding = getTemporaryEndPadding(contentRef.current, paddingEndProp); ++ const contentSize = temporaryPadding ? { ++ height: horizontal ? rawContentSize.height : rawContentSize.height - temporaryPadding, ++ width: horizontal ? rawContentSize.width - temporaryPadding : rawContentSize.width ++ } : rawContentSize; + const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); + const offset = getCurrentScrollOffset(); + const scrollEvent = { +@@ -6358,10 +6685,12 @@ function useValueListener$(key, callback) { + } + + // src/components/ScrollAdjust.tsx +-function getScrollAdjustAxis(horizontal) { ++function getScrollAdjustAxis(horizontal, rtl = false) { + return horizontal ? { + contentSizeKey: "scrollWidth", +- paddingEndProp: "paddingRight", ++ // The end side under RTL is the left, matching how the list pads its own content and ++ // which property the scroll view borrows room on. ++ paddingEndProp: rtl ? "paddingLeft" : "paddingRight", + viewportSizeKey: "clientWidth", + x: 1, + y: 0 +@@ -6390,8 +6719,6 @@ function ScrollAdjust() { + const ctx = useStateContext(); + const lastScrollOffsetRef = React3.useRef(0); + const lastScrollAdjustUserOffsetRef = React3.useRef(0); +- const resetPaddingRafRef = React3.useRef(void 0); +- const temporaryPaddingRef = React3.useRef(void 0); + const contentNodeRef = React3.useRef(null); + const callback = React3.useCallback(() => { + const scrollAdjust = peek$(ctx, "scrollAdjust"); +@@ -6402,7 +6729,7 @@ function ScrollAdjust() { + const target = getScrollAdjustTarget(ctx, contentNodeRef.current); + if (target) { + const horizontal = !!ctx.state.props.horizontal; +- const axis = getScrollAdjustAxis(horizontal); ++ const axis = getScrollAdjustAxis(horizontal, isHorizontalRTL(ctx.state)); + const { contentNode, scrollElement: el } = target; + const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; + const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; +@@ -6417,29 +6744,10 @@ function ScrollAdjust() { + const nextScroll = currentScroll + scrollDelta; + const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; + if (needsTemporaryPadding) { +- const currentInlinePaddingEnd = contentNode.style[axis.paddingEndProp]; +- const previousTemporaryPadding = temporaryPaddingRef.current; +- const baselinePaddingEnd = (previousTemporaryPadding == null ? void 0 : previousTemporaryPadding.value) === currentInlinePaddingEnd ? previousTemporaryPadding.baseline : currentInlinePaddingEnd; + const pad = (nextScroll + viewportSize - totalSize) * 2; +- const currentPaddingEnd = Number.parseFloat( +- window.getComputedStyle(contentNode)[axis.paddingEndProp] +- ); +- const temporaryPaddingEnd = `${(currentPaddingEnd || 0) + pad}px`; +- temporaryPaddingRef.current = { baseline: baselinePaddingEnd, value: temporaryPaddingEnd }; +- contentNode.style[axis.paddingEndProp] = temporaryPaddingEnd; +- void contentNode.offsetHeight; ++ const release = addTemporaryEndPadding(contentNode, axis.paddingEndProp, pad); + scrollBy(); +- if (resetPaddingRafRef.current !== void 0) { +- cancelAnimationFrame(resetPaddingRafRef.current); +- } +- resetPaddingRafRef.current = requestAnimationFrame(() => { +- const temporaryPadding = temporaryPaddingRef.current; +- resetPaddingRafRef.current = void 0; +- temporaryPaddingRef.current = void 0; +- if (contentNode.style[axis.paddingEndProp] === (temporaryPadding == null ? void 0 : temporaryPadding.value)) { +- contentNode.style[axis.paddingEndProp] = temporaryPadding.baseline; +- } +- }); ++ scheduleTemporaryEndPaddingRelease(contentNode, axis.paddingEndProp, release); + } else { + scrollBy(); + } +@@ -8267,6 +8575,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -6317,7 +7157,7 @@ index 95465f2..5669ad7 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js -index 914d2da..66763c2 100644 +index 914d2da..ce6a0d7 100644 --- a/node_modules/@legendapp/list/react.js +++ b/node_modules/@legendapp/list/react.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -7132,14 +7972,27 @@ index 914d2da..66763c2 100644 + ); + } + } -+} -+ + } + +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +// src/utils/checkAtTop.ts +function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { + const state = ctx == null ? void 0 : ctx.state; + if (!state) { + return; -+ } + } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } + const { + isStartReached, + props: { data, onStartReachedThreshold }, @@ -7154,7 +8007,22 @@ index 914d2da..66763c2 100644 + if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { + state.isStartReached = false; + state.startReachedSnapshot = void 0; -+ } + } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } + set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); + set$(ctx, "isNearStart", scroll <= threshold); + const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; @@ -7181,50 +8049,19 @@ index 914d2da..66763c2 100644 + state.startReachedSnapshot = snapshot; + } + ); -+ } + } +- return offset; } --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; -- } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } -- } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } -- } -- return offset; +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); - } - --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { ++} ++ +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { + var _a3, _b; @@ -7246,10 +8083,30 @@ index 914d2da..66763c2 100644 } // src/core/finishScrollTo.ts -@@ -1425,6 +1041,181 @@ function listenForScrollEnd(ctx, params) { - scheduledWork.register("platformScrollCompletion", cancel); - } - +@@ -1413,16 +1029,191 @@ function listenForScrollEnd(ctx, params) { + if (idleTimeout !== void 0) { + clearTimeout(idleTimeout); + } +- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); +- }; +- const onScrollEnd = () => finish("scrollend"); +- target.addEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.addEventListener("scrollend", onScrollEnd); +- } else { +- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); ++ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); ++ }; ++ const onScrollEnd = () => finish("scrollend"); ++ target.addEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.addEventListener("scrollend", onScrollEnd); ++ } else { ++ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); ++ } ++ scheduledWork.register("platformScrollCompletion", cancel); ++} ++ +// src/core/initialScrollSession.ts +var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +function hasInitialScrollSessionCompletion(completion) { @@ -7422,12 +8279,11 @@ index 914d2da..66763c2 100644 + } else { + resetAdaptiveRender(ctx); + } -+ } -+} -+ + } +- scheduledWork.register("platformScrollCompletion", cancel); + } + // src/core/doMaintainScrollAtEnd.ts - function doMaintainScrollAtEnd(ctx) { - const state = ctx.state; @@ -1451,6 +1242,7 @@ function doMaintainScrollAtEnd(ctx) { const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; @@ -7464,29 +8320,97 @@ index 914d2da..66763c2 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -1908,6 +1721,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { +@@ -1906,6 +1719,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { + state.scrollPrevTime = state.scrollTime; + state.scroll = newScroll; state.scrollTime = currentTime; ++ if ((options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust) { ++ releaseScrollTargetSettleIfMoved(ctx); ++ } const scrollDelta = Math.abs(newScroll - prevScroll); const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; -+ if (isUserScrollEvent) { -+ clearScrollTargetSettle(state); -+ } const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; - const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); - const scrollLength = state.scrollLength; -@@ -2069,7 +1885,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -2026,99 +1842,417 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { + if (start === void 0) { + return void 0; } - } - function scrollTo(ctx, params) { -- var _a3, _b; +- if (targetIndex !== void 0 && state.positions[start] === void 0) { +- return { end: start, start }; ++ if (targetIndex !== void 0 && state.positions[start] === void 0) { ++ return { end: start, start }; ++ } ++ if (targetIndex === void 0) { ++ const startBottom = getItemBottom(ctx, start); ++ if (startBottom === void 0 || startBottom <= viewportStart) { ++ return void 0; ++ } ++ } ++ while (start > 0) { ++ const top = state.positions[start]; ++ if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { ++ break; ++ } ++ start--; ++ } ++ while (start > 0) { ++ const previousBottom = getItemBottom(ctx, start - 1); ++ if (previousBottom === void 0 || previousBottom <= viewportStart) { ++ break; ++ } ++ start--; ++ } ++ let end = start; ++ while (end + 1 < dataLength) { ++ const nextTop = state.positions[end + 1]; ++ if (nextTop === void 0 || nextTop > viewportEnd) { ++ break; ++ } ++ end++; ++ } ++ return { end, start }; ++} ++function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { ++ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); ++ if (range) { ++ ctx.state.scrollTargetPinnedRange = range; ++ ctx.state.scrollForNextCalculateItemsInView = void 0; ++ } else { ++ ctx.state.scrollTargetPinnedRange = void 0; ++ } ++} ++function scrollTo(ctx, params) { + var _a3, _b, _c; - const state = ctx.state; - const { noScrollingTo, forceScroll, ...scrollTarget } = params; - const { -@@ -2101,6 +1917,15 @@ function scrollTo(ctx, params) { - }; - if (!isInitialScroll) { - pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); ++ const state = ctx.state; ++ const { noScrollingTo, forceScroll, ...scrollTarget } = params; ++ const { ++ animated, ++ isInitialScroll, ++ offset: scrollTargetOffset, ++ precomputedWithViewOffset, ++ waitForInitialScrollCompletionFrame ++ } = scrollTarget; ++ const { ++ props: { horizontal } ++ } = state; ++ cancelScrollCompletionChecks(state); ++ const requestedOffset = precomputedWithViewOffset ? scrollTargetOffset : calculateOffsetWithOffsetPosition(ctx, scrollTargetOffset, scrollTarget); ++ const shouldPreserveRawInitialOffsetRequest = !!isInitialScroll && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; ++ const targetOffset = clampScrollOffset(ctx, requestedOffset, scrollTarget); ++ const offset = shouldPreserveRawInitialOffsetRequest ? requestedOffset : targetOffset; ++ state.scrollHistory.length = 0; ++ if (!noScrollingTo) { ++ if (isInitialScroll) { ++ initialScrollCompletion.resetFlags(state); ++ } ++ const averageSizeSnapshot = getAverageSizeSnapshot(state); ++ state.scrollingTo = { ++ ...scrollTarget, ++ ...averageSizeSnapshot ? { averageSizeSnapshot } : {}, ++ targetOffset, ++ waitForInitialScrollCompletionFrame ++ }; ++ if (!isInitialScroll) { ++ pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); + if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { + beginScrollTargetSettle(ctx, { + index: scrollTarget.index, @@ -7496,22 +8420,26 @@ index 914d2da..66763c2 100644 + } else { + clearScrollTargetSettle(state); + } - } - } - state.scrollPending = targetOffset; -@@ -2108,7 +1933,7 @@ function scrollTo(ctx, params) { - if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { - if (animated) { - if (state.scrollTargetPinnedRange) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++ } ++ } ++ state.scrollPending = targetOffset; ++ syncInitialScrollNativeWatchdog(state, { isInitialScroll, requestedOffset: offset, targetOffset }); ++ if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { ++ if (animated) { ++ if (state.scrollTargetPinnedRange) { + (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); - } - } else { - updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2121,6 +1946,299 @@ function scrollTo(ctx, params) { - } - } - ++ } ++ } else { ++ updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); ++ } ++ } ++ if (forceScroll || !isInitialScroll || Platform.OS === "android") { ++ doScrollTo(ctx, { animated, horizontal, offset }); ++ } else { ++ state.scroll = offset; ++ } ++} ++ +// src/core/scrollTargetSettle.ts +var SETTLE_POSITION_EPSILON = 0.5; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; @@ -7532,15 +8460,13 @@ index 914d2da..66763c2 100644 + return; + } + const now = Date.now(); -+ const id = getId(state, index); -+ const existing = state.scrollTargetSettle; -+ const isSameTarget = (existing == null ? void 0 : existing.id) === id && existing.viewPosition === viewPosition && existing.viewOffset === viewOffset; + state.scrollTargetSettle = { -+ corrections: isSameTarget ? existing.corrections : 0, -+ deadline: isSameTarget ? existing.deadline : now + SETTLE_MAX_MS, ++ corrections: 0, ++ deadline: now + SETTLE_MAX_MS, + expiresAt: now + SETTLE_TTL_MS, -+ id, ++ id: getId(state, index), + quietPasses: 0, ++ requestedAt: now, + viewOffset, + viewPosition + }; @@ -7550,6 +8476,7 @@ index 914d2da..66763c2 100644 + return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} +function applyScrollTargetCorrection(ctx, id) { ++ var _a3; + const state = ctx.state; + const settle = state.scrollTargetSettle; + if (!settle || settle.id !== id) { @@ -7566,18 +8493,20 @@ index 914d2da..66763c2 100644 + return; + } + settle.corrections++; ++ settle.requestedAt = Date.now(); + scrollTo(ctx, { + animated: false, + index, + itemSize: getItemSizeAtIndex(ctx, index), -+ // Correcting where a scroll already landed is not a new imperative scroll: leaving -+ // state.scrollingTo alone keeps the list treating subsequent scroll events as the user's, -+ // which is what releases this settle, and keeps edge-reached callbacks firing. ++ // Correcting where a scroll already landed is not a new imperative scroll: claiming the ++ // session would suppress the list's own handling of subsequent scroll events and re-arm ++ // this settle from inside its own correction. + noScrollingTo: true, + offset: position, + viewOffset: settle.viewOffset, + viewPosition: settle.viewPosition + }); ++ (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +} +function settleScrollTarget(ctx, isCompensating) { + const state = ctx.state; @@ -7586,6 +8515,10 @@ index 914d2da..66763c2 100644 + return false; + } + if (isCompensating) { ++ if (Date.now() > settle.deadline) { ++ clearScrollTargetSettle(state); ++ return false; ++ } + settle.quietPasses = 0; + settle.expiresAt = Date.now() + SETTLE_TTL_MS; + return false; @@ -7615,13 +8548,24 @@ index 914d2da..66763c2 100644 + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} -+ -+// src/core/cancelImperativeScroll.ts -+function cancelScrollCompletionChecks({ scheduledWork }) { -+ scheduledWork.cancel("checkFinishedScrollFrame"); -+ scheduledWork.cancel("checkFinishedScrollRetryFrame"); -+ scheduledWork.cancel("checkFinishedScrollFallback"); -+ scheduledWork.cancel("platformScrollCompletion"); ++var SETTLE_ECHO_WINDOW_MS = 150; ++function releaseScrollTargetSettleIfMoved(ctx, _scrollOffset) { ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ return; ++ } ++ if (Date.now() - settle.requestedAt > SETTLE_ECHO_WINDOW_MS) { ++ clearScrollTargetSettle(state); ++ } ++} ++ ++// src/core/cancelImperativeScroll.ts ++function cancelScrollCompletionChecks({ scheduledWork }) { ++ scheduledWork.cancel("checkFinishedScrollFrame"); ++ scheduledWork.cancel("checkFinishedScrollRetryFrame"); ++ scheduledWork.cancel("checkFinishedScrollFallback"); ++ scheduledWork.cancel("platformScrollCompletion"); +} +function settlePendingImperativeScroll(state) { + var _a3, _b; @@ -7678,7 +8622,12 @@ index 914d2da..66763c2 100644 + } + if (resetInitialScroll) { + state.didFinishInitialScroll = false; -+ } + } +- if (targetIndex === void 0) { +- const startBottom = getItemBottom(ctx, start); +- if (startBottom === void 0 || startBottom <= viewportStart) { +- return void 0; +- } + set$(ctx, "readyToRender", false); + resetAdaptiveRender(ctx); +} @@ -7693,25 +8642,52 @@ index 914d2da..66763c2 100644 + } = state; + if (didLayout) { + state.didContainersLayout = true; -+ } + } +- while (start > 0) { +- const top = state.positions[start]; +- if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { +- break; +- } +- start--; + if (didInitialScroll) { + state.didFinishInitialScroll = true; -+ } + } +- while (start > 0) { +- const previousBottom = getItemBottom(ctx, start - 1); +- if (previousBottom === void 0 || previousBottom <= viewportStart) { +- break; + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); + if (isReadyToRender && !peek$(ctx, "readyToRender")) { + set$(ctx, "readyToRender", true); + setAdaptiveRender(ctx, "normal", "ready"); + if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { + scheduleFullDrawDistancePrewarm(ctx); -+ } + } +- start--; +- } +- let end = start; +- while (end + 1 < dataLength) { +- const nextTop = state.positions[end + 1]; +- if (nextTop === void 0 || nextTop > viewportEnd) { +- break; + if (!state.didLoad) { + state.didLoad = true; + if (onLoad) { + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); + } -+ } -+ } -+} + } +- end++; + } +- return { end, start }; + } +-function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); +- if (range) { +- ctx.state.scrollTargetPinnedRange = range; +- ctx.state.scrollForNextCalculateItemsInView = void 0; +- } else { +- ctx.state.scrollTargetPinnedRange = void 0; +- } + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -7719,7 +8695,9 @@ index 914d2da..66763c2 100644 + state.scroll = offset; + state.scrollPending = offset; + state.scrollPrev = offset; -+} + } +-function scrollTo(ctx, params) { +- var _a3, _b; +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -7731,12 +8709,41 @@ index 914d2da..66763c2 100644 +} +function supersedeInitialScroll(ctx) { + var _a3, _b, _c; -+ const state = ctx.state; + const state = ctx.state; +- const { noScrollingTo, forceScroll, ...scrollTarget } = params; +- const { +- animated, +- isInitialScroll, +- offset: scrollTargetOffset, +- precomputedWithViewOffset, +- waitForInitialScrollCompletionFrame +- } = scrollTarget; +- const { +- props: { horizontal } +- } = state; +- cancelScrollCompletionChecks(state); +- const requestedOffset = precomputedWithViewOffset ? scrollTargetOffset : calculateOffsetWithOffsetPosition(ctx, scrollTargetOffset, scrollTarget); +- const shouldPreserveRawInitialOffsetRequest = !!isInitialScroll && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; +- const targetOffset = clampScrollOffset(ctx, requestedOffset, scrollTarget); +- const offset = shouldPreserveRawInitialOffsetRequest ? requestedOffset : targetOffset; +- state.scrollHistory.length = 0; +- if (!noScrollingTo) { +- if (isInitialScroll) { +- initialScrollCompletion.resetFlags(state); + const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; + if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { + if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { + cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -+ } + } +- const averageSizeSnapshot = getAverageSizeSnapshot(state); +- state.scrollingTo = { +- ...scrollTarget, +- ...averageSizeSnapshot ? { averageSizeSnapshot } : {}, +- targetOffset, +- waitForInitialScrollCompletionFrame +- }; +- if (!isInitialScroll) { +- pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); + if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { + cancelScrollCompletionChecks(state); + state.scrollingTo = void 0; @@ -7756,8 +8763,14 @@ index 914d2da..66763c2 100644 + const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); + if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { + syncInitialScrollOffset(state, observedOffset); -+ } -+ } + } + } +- state.scrollPending = targetOffset; +- syncInitialScrollNativeWatchdog(state, { isInitialScroll, requestedOffset: offset, targetOffset }); +- if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { +- if (animated) { +- if (state.scrollTargetPinnedRange) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); + const complete = () => { + var _a4, _b2, _c2, _d, _e; + const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; @@ -7779,10 +8792,11 @@ index 914d2da..66763c2 100644 + PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, + "preservedInitialScroll" + ); -+ } -+ } else { + } + } else { +- updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); + clearPreservedInitialScrollTarget(state); -+ } + } + if (options == null ? void 0 : options.recalculateItems) { + recalculateSettledScroll(ctx); + } @@ -7795,7 +8809,12 @@ index 914d2da..66763c2 100644 + if (options == null ? void 0 : options.waitForCompletionFrame) { + requestAnimationFrame(complete); + return; -+ } + } +- if (forceScroll || !isInitialScroll || Platform.OS === "android") { +- doScrollTo(ctx, { animated, horizontal, offset }); +- } else { +- state.scroll = offset; +- } + complete(); +} + @@ -7803,12 +8822,10 @@ index 914d2da..66763c2 100644 +function calculateOffsetForIndex(ctx, index) { + const state = ctx.state; + return index !== void 0 ? state.positions[index] || 0 : 0; -+} -+ + } + // src/core/scrollToIndex.ts - function clampScrollIndex(index, dataLength) { - if (dataLength <= 0) { -@@ -4367,8 +4485,13 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4367,8 +4501,17 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -7816,6 +8833,10 @@ index 914d2da..66763c2 100644 - if (didMVCPAdjustScroll) { + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); ++ const mvcp = state.props.maintainVisibleContentPosition; ++ if (dataChanged && !mvcp.data && !mvcp.size) { ++ clearScrollTargetSettle(state); ++ } + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { + settleScrollTarget(ctx, isCompensating); @@ -7824,115 +8845,324 @@ index 914d2da..66763c2 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -6006,6 +6129,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -5915,6 +6058,119 @@ function useRafCoalescer(callback) { + return coalescer; + } + ++// src/components/temporaryEndPadding.ts ++var entriesByNode = /* @__PURE__ */ new WeakMap(); ++var nextRequestId = 1; ++function readResolvedPadding(node, prop) { ++ return Number.parseFloat(window.getComputedStyle(node)[prop]) || 0; ++} ++function totalRequested(entry) { ++ let total = 0; ++ for (const size of entry.requests.values()) { ++ total += size; ++ } ++ return total; ++} ++function isOwnedByUs(node, prop, entry) { ++ return entry.lastApplied === "" || node.style[prop] === entry.lastApplied; ++} ++function applyPadding(node, prop, entry) { ++ const total = totalRequested(entry); ++ node.style[prop] = total === 0 ? entry.baseline : `${entry.baselineSize + total}px`; ++ entry.lastApplied = node.style[prop]; ++} ++function releaseEntry(node, prop, requestId) { ++ const entries = entriesByNode.get(node); ++ const entry = entries == null ? void 0 : entries[prop]; ++ if (!entry || !entry.requests.delete(requestId)) { ++ return; ++ } ++ if (isOwnedByUs(node, prop, entry)) { ++ applyPadding(node, prop, entry); ++ } ++ if (entry.requests.size === 0) { ++ if (entry.resetHandle !== void 0) { ++ cancelAnimationFrame(entry.resetHandle); ++ } ++ entries == null ? true : delete entries[prop]; ++ } ++} ++function addTemporaryEndPadding(node, prop, extraSize) { ++ var _a3; ++ const entries = (_a3 = entriesByNode.get(node)) != null ? _a3 : {}; ++ let entry = entries[prop]; ++ if (entry && !isOwnedByUs(node, prop, entry)) { ++ entry.baseline = node.style[prop]; ++ entry.baselineSize = readResolvedPadding(node, prop); ++ } ++ if (!entry) { ++ entry = { ++ baseline: node.style[prop], ++ baselineSize: readResolvedPadding(node, prop), ++ lastApplied: "", ++ pendingReleases: /* @__PURE__ */ new Set(), ++ requests: /* @__PURE__ */ new Map(), ++ resetHandle: void 0 ++ }; ++ entries[prop] = entry; ++ entriesByNode.set(node, entries); ++ } ++ const requestId = nextRequestId++; ++ entry.requests.set(requestId, extraSize); ++ applyPadding(node, prop, entry); ++ void node.offsetHeight; ++ return function releaseTemporaryEndPadding() { ++ releaseEntry(node, prop, requestId); ++ }; ++} ++function scheduleTemporaryEndPaddingRelease(node, prop, release) { ++ var _a3; ++ const entry = (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop]; ++ if (!entry) { ++ release(); ++ return; ++ } ++ entry.pendingReleases.add(release); ++ if (entry.resetHandle !== void 0) { ++ return; ++ } ++ entry.resetHandle = requestAnimationFrame(() => { ++ entry.resetHandle = void 0; ++ const releases = [...entry.pendingReleases]; ++ entry.pendingReleases.clear(); ++ for (const pending of releases) { ++ pending(); ++ } ++ }); ++} ++function getTemporaryEndPadding(node, prop) { ++ var _a3; ++ const entry = node ? (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop] : void 0; ++ if (!entry || !isOwnedByUs(node, prop, entry)) { ++ return 0; ++ } ++ return totalRequested(entry); ++} ++function releaseAllTemporaryEndPadding(node) { ++ const entries = entriesByNode.get(node); ++ if (!entries) { ++ return; ++ } ++ for (const prop of Object.keys(entries)) { ++ const entry = entries[prop]; ++ if (!entry) { ++ continue; ++ } ++ if (entry.resetHandle !== void 0) { ++ cancelAnimationFrame(entry.resetHandle); ++ } ++ if (isOwnedByUs(node, prop, entry)) { ++ node.style[prop] = entry.baseline; ++ } ++ delete entries[prop]; ++ } ++} ++ + // src/components/webConstants.ts + var LEGEND_LIST_CONTENT_CONTAINER_CLASS = "legend-list-content-container"; + var LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS = "legend-list-scrollbar-x-hidden"; +@@ -6006,6 +6262,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; -+var SCROLL_CLAMP_EPSILON = 1; -+var SCROLL_CLAMP_RETRY_FRAMES = 10; ++var SCROLL_EXTENT_EPSILON = 1; ++var SMOOTH_SCROLL_MAX_MS = 2e3; var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6094,36 +6219,67 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6074,6 +6332,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), + [isWindowScroll] + ); ++ const paddingEndProp = horizontal ? isHorizontalRTL(ctx.state) ? "paddingLeft" : "paddingRight" : "paddingBottom"; ++ const getCommittedMaxScrollOffset = React3.useCallback( ++ (maxOffset) => Math.max(0, maxOffset - getTemporaryEndPadding(contentRef.current, paddingEndProp)), ++ [paddingEndProp] ++ ); + const getMaxScrollOffset = React3.useCallback(() => { + const scrollElement = scrollRef.current; + const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); +@@ -6094,6 +6357,59 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); -+ const scrollRetryToken = React3.useRef(0); ++ const paddedNodeRef = React3.useRef(null); ++ const animatedPaddingReleaseRef = React3.useRef(void 0); ++ const withReachableExtent = React3.useCallback( ++ (offset, maxOffset, animated, run) => { ++ var _a4; ++ const contentNode = contentRef.current; ++ const committedMaxOffset = getCommittedMaxScrollOffset(maxOffset); ++ if (!contentNode || offset <= committedMaxOffset + SCROLL_EXTENT_EPSILON) { ++ run(clampOffset(offset, maxOffset)); ++ return; ++ } ++ const release = addTemporaryEndPadding( ++ contentNode, ++ paddingEndProp, ++ offset - committedMaxOffset + SCROLL_EXTENT_EPSILON ++ ); ++ paddedNodeRef.current = contentNode; ++ run(offset); ++ if (!animated) { ++ scheduleTemporaryEndPaddingRelease(contentNode, paddingEndProp, release); ++ return; ++ } ++ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); ++ const scrollTarget = getScrollTarget(); ++ const supportsScrollEnd = !!scrollTarget && "onscrollend" in scrollTarget; ++ const finish = () => { ++ if (animatedPaddingReleaseRef.current !== finish) { ++ return; ++ } ++ animatedPaddingReleaseRef.current = void 0; ++ clearTimeout(settleTimeout); ++ scrollTarget == null ? void 0 : scrollTarget.removeEventListener("scrollend", finish); ++ release(); ++ }; ++ animatedPaddingReleaseRef.current = finish; ++ const settleTimeout = setTimeout(finish, SMOOTH_SCROLL_MAX_MS); ++ if (supportsScrollEnd) { ++ scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finish); ++ } ++ }, ++ [getCommittedMaxScrollOffset, getScrollTarget, paddingEndProp] ++ ); + React3.useEffect( + () => () => { -+ scrollRetryToken.current++; ++ var _a4; ++ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); ++ const paddedNode = paddedNodeRef.current; ++ if (paddedNode) { ++ releaseAllTemporaryEndPadding(paddedNode); ++ } + }, + [] + ); const scrollToLocalOffset = React3.useCallback( (offset, animated) => { -- const scrollElement = scrollRef.current; - const target = getScrollTarget(); - if (!target || typeof target.scrollTo !== "function") { - return; - } -- const maxOffset = getMaxScrollOffset(); -- const clampedOffset = clampOffset(offset, maxOffset); - const behavior = animated ? "smooth" : "auto"; -- const options = { behavior }; -- if (isWindowScroll) { -- const scroll = getWindowScrollPosition(); -- const listPos = getElementDocumentPosition(scrollElement, scroll); -- const { left, top } = resolveWindowScrollTarget({ -- clampedOffset, -- horizontal, -- listPos, -- scroll -+ const token = ++scrollRetryToken.current; -+ let previousMaxOffset = -1; -+ const apply = (attempt) => { -+ if (token !== scrollRetryToken.current || !scrollRef.current) { -+ return; -+ } -+ const scrollElement = scrollRef.current; -+ const maxOffset = getMaxScrollOffset(); -+ const didExtentGrow = maxOffset > previousMaxOffset; -+ previousMaxOffset = maxOffset; -+ const clampedOffset = clampOffset(offset, maxOffset); -+ const options = { behavior }; -+ if (isWindowScroll) { -+ const scroll = getWindowScrollPosition(); -+ const listPos = getElementDocumentPosition(scrollElement, scroll); -+ const { left, top } = resolveWindowScrollTarget({ -+ clampedOffset, -+ horizontal, -+ listPos, -+ scroll -+ }); -+ options.left = left; -+ options.top = top; -+ } else if (horizontal) { -+ options.left = clampedOffset; -+ } else { -+ options.top = clampedOffset; -+ } -+ target.scrollTo(options); -+ const landedOffset = getCurrentScrollOffset(); -+ const isRequestOutOfReach = clampedOffset < offset - SCROLL_CLAMP_EPSILON; -+ if (animated || !isRequestOutOfReach || !didExtentGrow || attempt >= SCROLL_CLAMP_RETRY_FRAMES) { -+ return; -+ } -+ requestAnimationFrame(() => { -+ if (token !== scrollRetryToken.current || !scrollRef.current) { -+ return; -+ } -+ if (Math.abs(getCurrentScrollOffset() - landedOffset) > SCROLL_CLAMP_EPSILON) { -+ return; -+ } -+ apply(attempt + 1); + const scrollElement = scrollRef.current; +@@ -6116,14 +6432,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); -- options.left = left; -- options.top = top; + options.left = left; + options.top = top; - } else if (horizontal) { - options.left = clampedOffset; -- } else { ++ } ++ if (isWindowScroll) { ++ target.scrollTo(options); + } else { - options.top = clampedOffset; -- } ++ withReachableExtent(offset, maxOffset, animated, (reachableOffset) => { ++ if (horizontal) { ++ options.left = reachableOffset; ++ } else { ++ options.top = reachableOffset; ++ } ++ target.scrollTo(options); ++ }); + } - target.scrollTo(options); -+ }; -+ apply(0); }, - [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] -+ [getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] ++ [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, withReachableExtent] ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6305,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6149,8 +6472,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; - const endOffset = getMaxScrollOffset(); - scrollToLocalOffset(endOffset, animated); -+ scrollToLocalOffset(Number.POSITIVE_INFINITY, animated); ++ scrollToLocalOffset(getCommittedMaxScrollOffset(getMaxScrollOffset()), animated); }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -8288,6 +8443,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6163,7 +6485,12 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + if (!onScroll2 || !scrollRef.current) { + return; + } +- const contentSize = getContentSize2(contentRef.current); ++ const rawContentSize = getContentSize2(contentRef.current); ++ const temporaryPadding = getTemporaryEndPadding(contentRef.current, paddingEndProp); ++ const contentSize = temporaryPadding ? { ++ height: horizontal ? rawContentSize.height : rawContentSize.height - temporaryPadding, ++ width: horizontal ? rawContentSize.width - temporaryPadding : rawContentSize.width ++ } : rawContentSize; + const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); + const offset = getCurrentScrollOffset(); + const scrollEvent = { +@@ -6379,10 +6706,12 @@ function useValueListener$(key, callback) { + } + + // src/components/ScrollAdjust.tsx +-function getScrollAdjustAxis(horizontal) { ++function getScrollAdjustAxis(horizontal, rtl = false) { + return horizontal ? { + contentSizeKey: "scrollWidth", +- paddingEndProp: "paddingRight", ++ // The end side under RTL is the left, matching how the list pads its own content and ++ // which property the scroll view borrows room on. ++ paddingEndProp: rtl ? "paddingLeft" : "paddingRight", + viewportSizeKey: "clientWidth", + x: 1, + y: 0 +@@ -6411,8 +6740,6 @@ function ScrollAdjust() { + const ctx = useStateContext(); + const lastScrollOffsetRef = React3__namespace.useRef(0); + const lastScrollAdjustUserOffsetRef = React3__namespace.useRef(0); +- const resetPaddingRafRef = React3__namespace.useRef(void 0); +- const temporaryPaddingRef = React3__namespace.useRef(void 0); + const contentNodeRef = React3__namespace.useRef(null); + const callback = React3__namespace.useCallback(() => { + const scrollAdjust = peek$(ctx, "scrollAdjust"); +@@ -6423,7 +6750,7 @@ function ScrollAdjust() { + const target = getScrollAdjustTarget(ctx, contentNodeRef.current); + if (target) { + const horizontal = !!ctx.state.props.horizontal; +- const axis = getScrollAdjustAxis(horizontal); ++ const axis = getScrollAdjustAxis(horizontal, isHorizontalRTL(ctx.state)); + const { contentNode, scrollElement: el } = target; + const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; + const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; +@@ -6438,29 +6765,10 @@ function ScrollAdjust() { + const nextScroll = currentScroll + scrollDelta; + const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; + if (needsTemporaryPadding) { +- const currentInlinePaddingEnd = contentNode.style[axis.paddingEndProp]; +- const previousTemporaryPadding = temporaryPaddingRef.current; +- const baselinePaddingEnd = (previousTemporaryPadding == null ? void 0 : previousTemporaryPadding.value) === currentInlinePaddingEnd ? previousTemporaryPadding.baseline : currentInlinePaddingEnd; + const pad = (nextScroll + viewportSize - totalSize) * 2; +- const currentPaddingEnd = Number.parseFloat( +- window.getComputedStyle(contentNode)[axis.paddingEndProp] +- ); +- const temporaryPaddingEnd = `${(currentPaddingEnd || 0) + pad}px`; +- temporaryPaddingRef.current = { baseline: baselinePaddingEnd, value: temporaryPaddingEnd }; +- contentNode.style[axis.paddingEndProp] = temporaryPaddingEnd; +- void contentNode.offsetHeight; ++ const release = addTemporaryEndPadding(contentNode, axis.paddingEndProp, pad); + scrollBy(); +- if (resetPaddingRafRef.current !== void 0) { +- cancelAnimationFrame(resetPaddingRafRef.current); +- } +- resetPaddingRafRef.current = requestAnimationFrame(() => { +- const temporaryPadding = temporaryPaddingRef.current; +- resetPaddingRafRef.current = void 0; +- temporaryPaddingRef.current = void 0; +- if (contentNode.style[axis.paddingEndProp] === (temporaryPadding == null ? void 0 : temporaryPadding.value)) { +- contentNode.style[axis.paddingEndProp] = temporaryPadding.baseline; +- } +- }); ++ scheduleTemporaryEndPaddingRelease(contentNode, axis.paddingEndProp, release); + } else { + scrollBy(); + } +@@ -8288,6 +8596,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -7941,7 +9171,7 @@ index 914d2da..66763c2 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs -index 95465f2..5669ad7 100644 +index 95465f2..b028ddc 100644 --- a/node_modules/@legendapp/list/react.mjs +++ b/node_modules/@legendapp/list/react.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -8756,14 +9986,27 @@ index 95465f2..5669ad7 100644 + ); + } + } -+} -+ + } + +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +// src/utils/checkAtTop.ts +function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { + const state = ctx == null ? void 0 : ctx.state; + if (!state) { + return; -+ } + } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } + const { + isStartReached, + props: { data, onStartReachedThreshold }, @@ -8778,7 +10021,22 @@ index 95465f2..5669ad7 100644 + if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { + state.isStartReached = false; + state.startReachedSnapshot = void 0; -+ } + } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } + set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); + set$(ctx, "isNearStart", scroll <= threshold); + const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; @@ -8805,50 +10063,19 @@ index 95465f2..5669ad7 100644 + state.startReachedSnapshot = snapshot; + } + ); -+ } + } +- return offset; } --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; -- } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } -- } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } -- } -- return offset; +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); - } - --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { ++} ++ +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { + var _a3, _b; @@ -8870,10 +10097,30 @@ index 95465f2..5669ad7 100644 } // src/core/finishScrollTo.ts -@@ -1404,6 +1020,181 @@ function listenForScrollEnd(ctx, params) { - scheduledWork.register("platformScrollCompletion", cancel); - } - +@@ -1392,16 +1008,191 @@ function listenForScrollEnd(ctx, params) { + if (idleTimeout !== void 0) { + clearTimeout(idleTimeout); + } +- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); +- }; +- const onScrollEnd = () => finish("scrollend"); +- target.addEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.addEventListener("scrollend", onScrollEnd); +- } else { +- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); ++ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); ++ }; ++ const onScrollEnd = () => finish("scrollend"); ++ target.addEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.addEventListener("scrollend", onScrollEnd); ++ } else { ++ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); ++ } ++ scheduledWork.register("platformScrollCompletion", cancel); ++} ++ +// src/core/initialScrollSession.ts +var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +function hasInitialScrollSessionCompletion(completion) { @@ -9046,12 +10293,11 @@ index 95465f2..5669ad7 100644 + } else { + resetAdaptiveRender(ctx); + } -+ } -+} -+ + } +- scheduledWork.register("platformScrollCompletion", cancel); + } + // src/core/doMaintainScrollAtEnd.ts - function doMaintainScrollAtEnd(ctx) { - const state = ctx.state; @@ -1430,6 +1221,7 @@ function doMaintainScrollAtEnd(ctx) { const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; @@ -9088,29 +10334,97 @@ index 95465f2..5669ad7 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -1887,6 +1700,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { +@@ -1885,6 +1698,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { + state.scrollPrevTime = state.scrollTime; + state.scroll = newScroll; state.scrollTime = currentTime; ++ if ((options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust) { ++ releaseScrollTargetSettleIfMoved(ctx); ++ } const scrollDelta = Math.abs(newScroll - prevScroll); const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; -+ if (isUserScrollEvent) { -+ clearScrollTargetSettle(state); -+ } const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; - const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); - const scrollLength = state.scrollLength; -@@ -2048,7 +1864,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -2005,99 +1821,417 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { + if (start === void 0) { + return void 0; } - } - function scrollTo(ctx, params) { -- var _a3, _b; +- if (targetIndex !== void 0 && state.positions[start] === void 0) { +- return { end: start, start }; ++ if (targetIndex !== void 0 && state.positions[start] === void 0) { ++ return { end: start, start }; ++ } ++ if (targetIndex === void 0) { ++ const startBottom = getItemBottom(ctx, start); ++ if (startBottom === void 0 || startBottom <= viewportStart) { ++ return void 0; ++ } ++ } ++ while (start > 0) { ++ const top = state.positions[start]; ++ if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { ++ break; ++ } ++ start--; ++ } ++ while (start > 0) { ++ const previousBottom = getItemBottom(ctx, start - 1); ++ if (previousBottom === void 0 || previousBottom <= viewportStart) { ++ break; ++ } ++ start--; ++ } ++ let end = start; ++ while (end + 1 < dataLength) { ++ const nextTop = state.positions[end + 1]; ++ if (nextTop === void 0 || nextTop > viewportEnd) { ++ break; ++ } ++ end++; ++ } ++ return { end, start }; ++} ++function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { ++ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); ++ if (range) { ++ ctx.state.scrollTargetPinnedRange = range; ++ ctx.state.scrollForNextCalculateItemsInView = void 0; ++ } else { ++ ctx.state.scrollTargetPinnedRange = void 0; ++ } ++} ++function scrollTo(ctx, params) { + var _a3, _b, _c; - const state = ctx.state; - const { noScrollingTo, forceScroll, ...scrollTarget } = params; - const { -@@ -2080,6 +1896,15 @@ function scrollTo(ctx, params) { - }; - if (!isInitialScroll) { - pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); ++ const state = ctx.state; ++ const { noScrollingTo, forceScroll, ...scrollTarget } = params; ++ const { ++ animated, ++ isInitialScroll, ++ offset: scrollTargetOffset, ++ precomputedWithViewOffset, ++ waitForInitialScrollCompletionFrame ++ } = scrollTarget; ++ const { ++ props: { horizontal } ++ } = state; ++ cancelScrollCompletionChecks(state); ++ const requestedOffset = precomputedWithViewOffset ? scrollTargetOffset : calculateOffsetWithOffsetPosition(ctx, scrollTargetOffset, scrollTarget); ++ const shouldPreserveRawInitialOffsetRequest = !!isInitialScroll && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; ++ const targetOffset = clampScrollOffset(ctx, requestedOffset, scrollTarget); ++ const offset = shouldPreserveRawInitialOffsetRequest ? requestedOffset : targetOffset; ++ state.scrollHistory.length = 0; ++ if (!noScrollingTo) { ++ if (isInitialScroll) { ++ initialScrollCompletion.resetFlags(state); ++ } ++ const averageSizeSnapshot = getAverageSizeSnapshot(state); ++ state.scrollingTo = { ++ ...scrollTarget, ++ ...averageSizeSnapshot ? { averageSizeSnapshot } : {}, ++ targetOffset, ++ waitForInitialScrollCompletionFrame ++ }; ++ if (!isInitialScroll) { ++ pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); + if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { + beginScrollTargetSettle(ctx, { + index: scrollTarget.index, @@ -9120,22 +10434,26 @@ index 95465f2..5669ad7 100644 + } else { + clearScrollTargetSettle(state); + } - } - } - state.scrollPending = targetOffset; -@@ -2087,7 +1912,7 @@ function scrollTo(ctx, params) { - if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { - if (animated) { - if (state.scrollTargetPinnedRange) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++ } ++ } ++ state.scrollPending = targetOffset; ++ syncInitialScrollNativeWatchdog(state, { isInitialScroll, requestedOffset: offset, targetOffset }); ++ if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { ++ if (animated) { ++ if (state.scrollTargetPinnedRange) { + (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); - } - } else { - updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2100,6 +1925,299 @@ function scrollTo(ctx, params) { - } - } - ++ } ++ } else { ++ updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); ++ } ++ } ++ if (forceScroll || !isInitialScroll || Platform.OS === "android") { ++ doScrollTo(ctx, { animated, horizontal, offset }); ++ } else { ++ state.scroll = offset; ++ } ++} ++ +// src/core/scrollTargetSettle.ts +var SETTLE_POSITION_EPSILON = 0.5; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; @@ -9156,15 +10474,13 @@ index 95465f2..5669ad7 100644 + return; + } + const now = Date.now(); -+ const id = getId(state, index); -+ const existing = state.scrollTargetSettle; -+ const isSameTarget = (existing == null ? void 0 : existing.id) === id && existing.viewPosition === viewPosition && existing.viewOffset === viewOffset; + state.scrollTargetSettle = { -+ corrections: isSameTarget ? existing.corrections : 0, -+ deadline: isSameTarget ? existing.deadline : now + SETTLE_MAX_MS, ++ corrections: 0, ++ deadline: now + SETTLE_MAX_MS, + expiresAt: now + SETTLE_TTL_MS, -+ id, ++ id: getId(state, index), + quietPasses: 0, ++ requestedAt: now, + viewOffset, + viewPosition + }; @@ -9174,6 +10490,7 @@ index 95465f2..5669ad7 100644 + return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} +function applyScrollTargetCorrection(ctx, id) { ++ var _a3; + const state = ctx.state; + const settle = state.scrollTargetSettle; + if (!settle || settle.id !== id) { @@ -9190,18 +10507,20 @@ index 95465f2..5669ad7 100644 + return; + } + settle.corrections++; ++ settle.requestedAt = Date.now(); + scrollTo(ctx, { + animated: false, + index, + itemSize: getItemSizeAtIndex(ctx, index), -+ // Correcting where a scroll already landed is not a new imperative scroll: leaving -+ // state.scrollingTo alone keeps the list treating subsequent scroll events as the user's, -+ // which is what releases this settle, and keeps edge-reached callbacks firing. ++ // Correcting where a scroll already landed is not a new imperative scroll: claiming the ++ // session would suppress the list's own handling of subsequent scroll events and re-arm ++ // this settle from inside its own correction. + noScrollingTo: true, + offset: position, + viewOffset: settle.viewOffset, + viewPosition: settle.viewPosition + }); ++ (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +} +function settleScrollTarget(ctx, isCompensating) { + const state = ctx.state; @@ -9210,6 +10529,10 @@ index 95465f2..5669ad7 100644 + return false; + } + if (isCompensating) { ++ if (Date.now() > settle.deadline) { ++ clearScrollTargetSettle(state); ++ return false; ++ } + settle.quietPasses = 0; + settle.expiresAt = Date.now() + SETTLE_TTL_MS; + return false; @@ -9239,6 +10562,17 @@ index 95465f2..5669ad7 100644 + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} ++var SETTLE_ECHO_WINDOW_MS = 150; ++function releaseScrollTargetSettleIfMoved(ctx, _scrollOffset) { ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ return; ++ } ++ if (Date.now() - settle.requestedAt > SETTLE_ECHO_WINDOW_MS) { ++ clearScrollTargetSettle(state); ++ } ++} + +// src/core/cancelImperativeScroll.ts +function cancelScrollCompletionChecks({ scheduledWork }) { @@ -9302,7 +10636,12 @@ index 95465f2..5669ad7 100644 + } + if (resetInitialScroll) { + state.didFinishInitialScroll = false; -+ } + } +- if (targetIndex === void 0) { +- const startBottom = getItemBottom(ctx, start); +- if (startBottom === void 0 || startBottom <= viewportStart) { +- return void 0; +- } + set$(ctx, "readyToRender", false); + resetAdaptiveRender(ctx); +} @@ -9317,25 +10656,52 @@ index 95465f2..5669ad7 100644 + } = state; + if (didLayout) { + state.didContainersLayout = true; -+ } + } +- while (start > 0) { +- const top = state.positions[start]; +- if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { +- break; +- } +- start--; + if (didInitialScroll) { + state.didFinishInitialScroll = true; -+ } + } +- while (start > 0) { +- const previousBottom = getItemBottom(ctx, start - 1); +- if (previousBottom === void 0 || previousBottom <= viewportStart) { +- break; + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); + if (isReadyToRender && !peek$(ctx, "readyToRender")) { + set$(ctx, "readyToRender", true); + setAdaptiveRender(ctx, "normal", "ready"); + if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { + scheduleFullDrawDistancePrewarm(ctx); -+ } + } +- start--; +- } +- let end = start; +- while (end + 1 < dataLength) { +- const nextTop = state.positions[end + 1]; +- if (nextTop === void 0 || nextTop > viewportEnd) { +- break; + if (!state.didLoad) { + state.didLoad = true; + if (onLoad) { + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); + } -+ } -+ } -+} + } +- end++; + } +- return { end, start }; + } +-function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); +- if (range) { +- ctx.state.scrollTargetPinnedRange = range; +- ctx.state.scrollForNextCalculateItemsInView = void 0; +- } else { +- ctx.state.scrollTargetPinnedRange = void 0; +- } + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -9343,7 +10709,9 @@ index 95465f2..5669ad7 100644 + state.scroll = offset; + state.scrollPending = offset; + state.scrollPrev = offset; -+} + } +-function scrollTo(ctx, params) { +- var _a3, _b; +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -9355,12 +10723,41 @@ index 95465f2..5669ad7 100644 +} +function supersedeInitialScroll(ctx) { + var _a3, _b, _c; -+ const state = ctx.state; + const state = ctx.state; +- const { noScrollingTo, forceScroll, ...scrollTarget } = params; +- const { +- animated, +- isInitialScroll, +- offset: scrollTargetOffset, +- precomputedWithViewOffset, +- waitForInitialScrollCompletionFrame +- } = scrollTarget; +- const { +- props: { horizontal } +- } = state; +- cancelScrollCompletionChecks(state); +- const requestedOffset = precomputedWithViewOffset ? scrollTargetOffset : calculateOffsetWithOffsetPosition(ctx, scrollTargetOffset, scrollTarget); +- const shouldPreserveRawInitialOffsetRequest = !!isInitialScroll && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; +- const targetOffset = clampScrollOffset(ctx, requestedOffset, scrollTarget); +- const offset = shouldPreserveRawInitialOffsetRequest ? requestedOffset : targetOffset; +- state.scrollHistory.length = 0; +- if (!noScrollingTo) { +- if (isInitialScroll) { +- initialScrollCompletion.resetFlags(state); + const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; + if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { + if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { + cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -+ } + } +- const averageSizeSnapshot = getAverageSizeSnapshot(state); +- state.scrollingTo = { +- ...scrollTarget, +- ...averageSizeSnapshot ? { averageSizeSnapshot } : {}, +- targetOffset, +- waitForInitialScrollCompletionFrame +- }; +- if (!isInitialScroll) { +- pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); + if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { + cancelScrollCompletionChecks(state); + state.scrollingTo = void 0; @@ -9380,8 +10777,14 @@ index 95465f2..5669ad7 100644 + const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); + if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { + syncInitialScrollOffset(state, observedOffset); -+ } -+ } + } + } +- state.scrollPending = targetOffset; +- syncInitialScrollNativeWatchdog(state, { isInitialScroll, requestedOffset: offset, targetOffset }); +- if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { +- if (animated) { +- if (state.scrollTargetPinnedRange) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); + const complete = () => { + var _a4, _b2, _c2, _d, _e; + const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; @@ -9403,10 +10806,11 @@ index 95465f2..5669ad7 100644 + PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, + "preservedInitialScroll" + ); -+ } -+ } else { + } + } else { +- updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); + clearPreservedInitialScrollTarget(state); -+ } + } + if (options == null ? void 0 : options.recalculateItems) { + recalculateSettledScroll(ctx); + } @@ -9419,7 +10823,12 @@ index 95465f2..5669ad7 100644 + if (options == null ? void 0 : options.waitForCompletionFrame) { + requestAnimationFrame(complete); + return; -+ } + } +- if (forceScroll || !isInitialScroll || Platform.OS === "android") { +- doScrollTo(ctx, { animated, horizontal, offset }); +- } else { +- state.scroll = offset; +- } + complete(); +} + @@ -9427,12 +10836,10 @@ index 95465f2..5669ad7 100644 +function calculateOffsetForIndex(ctx, index) { + const state = ctx.state; + return index !== void 0 ? state.positions[index] || 0 : 0; -+} -+ + } + // src/core/scrollToIndex.ts - function clampScrollIndex(index, dataLength) { - if (dataLength <= 0) { -@@ -4346,8 +4464,13 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4346,8 +4480,17 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -9440,6 +10847,10 @@ index 95465f2..5669ad7 100644 - if (didMVCPAdjustScroll) { + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); ++ const mvcp = state.props.maintainVisibleContentPosition; ++ if (dataChanged && !mvcp.data && !mvcp.size) { ++ clearScrollTargetSettle(state); ++ } + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { + settleScrollTarget(ctx, isCompensating); @@ -9448,115 +10859,324 @@ index 95465f2..5669ad7 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5985,6 +6108,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -5894,6 +6037,119 @@ function useRafCoalescer(callback) { + return coalescer; + } + ++// src/components/temporaryEndPadding.ts ++var entriesByNode = /* @__PURE__ */ new WeakMap(); ++var nextRequestId = 1; ++function readResolvedPadding(node, prop) { ++ return Number.parseFloat(window.getComputedStyle(node)[prop]) || 0; ++} ++function totalRequested(entry) { ++ let total = 0; ++ for (const size of entry.requests.values()) { ++ total += size; ++ } ++ return total; ++} ++function isOwnedByUs(node, prop, entry) { ++ return entry.lastApplied === "" || node.style[prop] === entry.lastApplied; ++} ++function applyPadding(node, prop, entry) { ++ const total = totalRequested(entry); ++ node.style[prop] = total === 0 ? entry.baseline : `${entry.baselineSize + total}px`; ++ entry.lastApplied = node.style[prop]; ++} ++function releaseEntry(node, prop, requestId) { ++ const entries = entriesByNode.get(node); ++ const entry = entries == null ? void 0 : entries[prop]; ++ if (!entry || !entry.requests.delete(requestId)) { ++ return; ++ } ++ if (isOwnedByUs(node, prop, entry)) { ++ applyPadding(node, prop, entry); ++ } ++ if (entry.requests.size === 0) { ++ if (entry.resetHandle !== void 0) { ++ cancelAnimationFrame(entry.resetHandle); ++ } ++ entries == null ? true : delete entries[prop]; ++ } ++} ++function addTemporaryEndPadding(node, prop, extraSize) { ++ var _a3; ++ const entries = (_a3 = entriesByNode.get(node)) != null ? _a3 : {}; ++ let entry = entries[prop]; ++ if (entry && !isOwnedByUs(node, prop, entry)) { ++ entry.baseline = node.style[prop]; ++ entry.baselineSize = readResolvedPadding(node, prop); ++ } ++ if (!entry) { ++ entry = { ++ baseline: node.style[prop], ++ baselineSize: readResolvedPadding(node, prop), ++ lastApplied: "", ++ pendingReleases: /* @__PURE__ */ new Set(), ++ requests: /* @__PURE__ */ new Map(), ++ resetHandle: void 0 ++ }; ++ entries[prop] = entry; ++ entriesByNode.set(node, entries); ++ } ++ const requestId = nextRequestId++; ++ entry.requests.set(requestId, extraSize); ++ applyPadding(node, prop, entry); ++ void node.offsetHeight; ++ return function releaseTemporaryEndPadding() { ++ releaseEntry(node, prop, requestId); ++ }; ++} ++function scheduleTemporaryEndPaddingRelease(node, prop, release) { ++ var _a3; ++ const entry = (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop]; ++ if (!entry) { ++ release(); ++ return; ++ } ++ entry.pendingReleases.add(release); ++ if (entry.resetHandle !== void 0) { ++ return; ++ } ++ entry.resetHandle = requestAnimationFrame(() => { ++ entry.resetHandle = void 0; ++ const releases = [...entry.pendingReleases]; ++ entry.pendingReleases.clear(); ++ for (const pending of releases) { ++ pending(); ++ } ++ }); ++} ++function getTemporaryEndPadding(node, prop) { ++ var _a3; ++ const entry = node ? (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop] : void 0; ++ if (!entry || !isOwnedByUs(node, prop, entry)) { ++ return 0; ++ } ++ return totalRequested(entry); ++} ++function releaseAllTemporaryEndPadding(node) { ++ const entries = entriesByNode.get(node); ++ if (!entries) { ++ return; ++ } ++ for (const prop of Object.keys(entries)) { ++ const entry = entries[prop]; ++ if (!entry) { ++ continue; ++ } ++ if (entry.resetHandle !== void 0) { ++ cancelAnimationFrame(entry.resetHandle); ++ } ++ if (isOwnedByUs(node, prop, entry)) { ++ node.style[prop] = entry.baseline; ++ } ++ delete entries[prop]; ++ } ++} ++ + // src/components/webConstants.ts + var LEGEND_LIST_CONTENT_CONTAINER_CLASS = "legend-list-content-container"; + var LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS = "legend-list-scrollbar-x-hidden"; +@@ -5985,6 +6241,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; -+var SCROLL_CLAMP_EPSILON = 1; -+var SCROLL_CLAMP_RETRY_FRAMES = 10; ++var SCROLL_EXTENT_EPSILON = 1; ++var SMOOTH_SCROLL_MAX_MS = 2e3; var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6073,36 +6198,67 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6053,6 +6311,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), + [isWindowScroll] + ); ++ const paddingEndProp = horizontal ? isHorizontalRTL(ctx.state) ? "paddingLeft" : "paddingRight" : "paddingBottom"; ++ const getCommittedMaxScrollOffset = useCallback( ++ (maxOffset) => Math.max(0, maxOffset - getTemporaryEndPadding(contentRef.current, paddingEndProp)), ++ [paddingEndProp] ++ ); + const getMaxScrollOffset = useCallback(() => { + const scrollElement = scrollRef.current; + const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); +@@ -6073,6 +6336,59 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); -+ const scrollRetryToken = useRef(0); ++ const paddedNodeRef = useRef(null); ++ const animatedPaddingReleaseRef = useRef(void 0); ++ const withReachableExtent = useCallback( ++ (offset, maxOffset, animated, run) => { ++ var _a4; ++ const contentNode = contentRef.current; ++ const committedMaxOffset = getCommittedMaxScrollOffset(maxOffset); ++ if (!contentNode || offset <= committedMaxOffset + SCROLL_EXTENT_EPSILON) { ++ run(clampOffset(offset, maxOffset)); ++ return; ++ } ++ const release = addTemporaryEndPadding( ++ contentNode, ++ paddingEndProp, ++ offset - committedMaxOffset + SCROLL_EXTENT_EPSILON ++ ); ++ paddedNodeRef.current = contentNode; ++ run(offset); ++ if (!animated) { ++ scheduleTemporaryEndPaddingRelease(contentNode, paddingEndProp, release); ++ return; ++ } ++ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); ++ const scrollTarget = getScrollTarget(); ++ const supportsScrollEnd = !!scrollTarget && "onscrollend" in scrollTarget; ++ const finish = () => { ++ if (animatedPaddingReleaseRef.current !== finish) { ++ return; ++ } ++ animatedPaddingReleaseRef.current = void 0; ++ clearTimeout(settleTimeout); ++ scrollTarget == null ? void 0 : scrollTarget.removeEventListener("scrollend", finish); ++ release(); ++ }; ++ animatedPaddingReleaseRef.current = finish; ++ const settleTimeout = setTimeout(finish, SMOOTH_SCROLL_MAX_MS); ++ if (supportsScrollEnd) { ++ scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finish); ++ } ++ }, ++ [getCommittedMaxScrollOffset, getScrollTarget, paddingEndProp] ++ ); + useEffect( + () => () => { -+ scrollRetryToken.current++; ++ var _a4; ++ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); ++ const paddedNode = paddedNodeRef.current; ++ if (paddedNode) { ++ releaseAllTemporaryEndPadding(paddedNode); ++ } + }, + [] + ); const scrollToLocalOffset = useCallback( (offset, animated) => { -- const scrollElement = scrollRef.current; - const target = getScrollTarget(); - if (!target || typeof target.scrollTo !== "function") { - return; - } -- const maxOffset = getMaxScrollOffset(); -- const clampedOffset = clampOffset(offset, maxOffset); - const behavior = animated ? "smooth" : "auto"; -- const options = { behavior }; -- if (isWindowScroll) { -- const scroll = getWindowScrollPosition(); -- const listPos = getElementDocumentPosition(scrollElement, scroll); -- const { left, top } = resolveWindowScrollTarget({ -- clampedOffset, -- horizontal, -- listPos, -- scroll -+ const token = ++scrollRetryToken.current; -+ let previousMaxOffset = -1; -+ const apply = (attempt) => { -+ if (token !== scrollRetryToken.current || !scrollRef.current) { -+ return; -+ } -+ const scrollElement = scrollRef.current; -+ const maxOffset = getMaxScrollOffset(); -+ const didExtentGrow = maxOffset > previousMaxOffset; -+ previousMaxOffset = maxOffset; -+ const clampedOffset = clampOffset(offset, maxOffset); -+ const options = { behavior }; -+ if (isWindowScroll) { -+ const scroll = getWindowScrollPosition(); -+ const listPos = getElementDocumentPosition(scrollElement, scroll); -+ const { left, top } = resolveWindowScrollTarget({ -+ clampedOffset, -+ horizontal, -+ listPos, -+ scroll -+ }); -+ options.left = left; -+ options.top = top; -+ } else if (horizontal) { -+ options.left = clampedOffset; -+ } else { -+ options.top = clampedOffset; -+ } -+ target.scrollTo(options); -+ const landedOffset = getCurrentScrollOffset(); -+ const isRequestOutOfReach = clampedOffset < offset - SCROLL_CLAMP_EPSILON; -+ if (animated || !isRequestOutOfReach || !didExtentGrow || attempt >= SCROLL_CLAMP_RETRY_FRAMES) { -+ return; -+ } -+ requestAnimationFrame(() => { -+ if (token !== scrollRetryToken.current || !scrollRef.current) { -+ return; -+ } -+ if (Math.abs(getCurrentScrollOffset() - landedOffset) > SCROLL_CLAMP_EPSILON) { -+ return; -+ } -+ apply(attempt + 1); + const scrollElement = scrollRef.current; +@@ -6095,14 +6411,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); -- options.left = left; -- options.top = top; + options.left = left; + options.top = top; - } else if (horizontal) { - options.left = clampedOffset; -- } else { ++ } ++ if (isWindowScroll) { ++ target.scrollTo(options); + } else { - options.top = clampedOffset; -- } ++ withReachableExtent(offset, maxOffset, animated, (reachableOffset) => { ++ if (horizontal) { ++ options.left = reachableOffset; ++ } else { ++ options.top = reachableOffset; ++ } ++ target.scrollTo(options); ++ }); + } - target.scrollTo(options); -+ }; -+ apply(0); }, - [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] -+ [getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] ++ [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, withReachableExtent] ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6284,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6128,8 +6451,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; - const endOffset = getMaxScrollOffset(); - scrollToLocalOffset(endOffset, animated); -+ scrollToLocalOffset(Number.POSITIVE_INFINITY, animated); ++ scrollToLocalOffset(getCommittedMaxScrollOffset(getMaxScrollOffset()), animated); }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -8267,6 +8422,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6142,7 +6464,12 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + if (!onScroll2 || !scrollRef.current) { + return; + } +- const contentSize = getContentSize2(contentRef.current); ++ const rawContentSize = getContentSize2(contentRef.current); ++ const temporaryPadding = getTemporaryEndPadding(contentRef.current, paddingEndProp); ++ const contentSize = temporaryPadding ? { ++ height: horizontal ? rawContentSize.height : rawContentSize.height - temporaryPadding, ++ width: horizontal ? rawContentSize.width - temporaryPadding : rawContentSize.width ++ } : rawContentSize; + const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); + const offset = getCurrentScrollOffset(); + const scrollEvent = { +@@ -6358,10 +6685,12 @@ function useValueListener$(key, callback) { + } + + // src/components/ScrollAdjust.tsx +-function getScrollAdjustAxis(horizontal) { ++function getScrollAdjustAxis(horizontal, rtl = false) { + return horizontal ? { + contentSizeKey: "scrollWidth", +- paddingEndProp: "paddingRight", ++ // The end side under RTL is the left, matching how the list pads its own content and ++ // which property the scroll view borrows room on. ++ paddingEndProp: rtl ? "paddingLeft" : "paddingRight", + viewportSizeKey: "clientWidth", + x: 1, + y: 0 +@@ -6390,8 +6719,6 @@ function ScrollAdjust() { + const ctx = useStateContext(); + const lastScrollOffsetRef = React3.useRef(0); + const lastScrollAdjustUserOffsetRef = React3.useRef(0); +- const resetPaddingRafRef = React3.useRef(void 0); +- const temporaryPaddingRef = React3.useRef(void 0); + const contentNodeRef = React3.useRef(null); + const callback = React3.useCallback(() => { + const scrollAdjust = peek$(ctx, "scrollAdjust"); +@@ -6402,7 +6729,7 @@ function ScrollAdjust() { + const target = getScrollAdjustTarget(ctx, contentNodeRef.current); + if (target) { + const horizontal = !!ctx.state.props.horizontal; +- const axis = getScrollAdjustAxis(horizontal); ++ const axis = getScrollAdjustAxis(horizontal, isHorizontalRTL(ctx.state)); + const { contentNode, scrollElement: el } = target; + const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; + const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; +@@ -6417,29 +6744,10 @@ function ScrollAdjust() { + const nextScroll = currentScroll + scrollDelta; + const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; + if (needsTemporaryPadding) { +- const currentInlinePaddingEnd = contentNode.style[axis.paddingEndProp]; +- const previousTemporaryPadding = temporaryPaddingRef.current; +- const baselinePaddingEnd = (previousTemporaryPadding == null ? void 0 : previousTemporaryPadding.value) === currentInlinePaddingEnd ? previousTemporaryPadding.baseline : currentInlinePaddingEnd; + const pad = (nextScroll + viewportSize - totalSize) * 2; +- const currentPaddingEnd = Number.parseFloat( +- window.getComputedStyle(contentNode)[axis.paddingEndProp] +- ); +- const temporaryPaddingEnd = `${(currentPaddingEnd || 0) + pad}px`; +- temporaryPaddingRef.current = { baseline: baselinePaddingEnd, value: temporaryPaddingEnd }; +- contentNode.style[axis.paddingEndProp] = temporaryPaddingEnd; +- void contentNode.offsetHeight; ++ const release = addTemporaryEndPadding(contentNode, axis.paddingEndProp, pad); + scrollBy(); +- if (resetPaddingRafRef.current !== void 0) { +- cancelAnimationFrame(resetPaddingRafRef.current); +- } +- resetPaddingRafRef.current = requestAnimationFrame(() => { +- const temporaryPadding = temporaryPaddingRef.current; +- resetPaddingRafRef.current = void 0; +- temporaryPaddingRef.current = void 0; +- if (contentNode.style[axis.paddingEndProp] === (temporaryPadding == null ? void 0 : temporaryPadding.value)) { +- contentNode.style[axis.paddingEndProp] = temporaryPadding.baseline; +- } +- }); ++ scheduleTemporaryEndPaddingRelease(contentNode, axis.paddingEndProp, release); + } else { + scrollBy(); + } +@@ -8267,6 +8575,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); From e31d207351d961b67909c04b15e92e08993857b7 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 11 Aug 2026 17:25:36 -0400 Subject: [PATCH 07/38] test(e2e): drive thread search hits on device Verifying search-hit landing has meant rebuilding, reloading and stepping through hits by hand, which is slow and easy to get wrong. This drives it: step past the end of the hit list so the search wraps, jump to a hit already on screen, and drag the thread after landing. Asserting the hit is on screen has to compare where the row is against where the list is. The row keeps its marker while it is the selected hit, but a virtualised list also renders rows outside the viewport, so asserting the marker exists passes against a build where the hit lands off screen - verified by disabling the scroll-target settle and watching it stay green. The marker sits on the row wrapper that already exists, so no extra view is introduced. iOS 26 keeps the conversation's actions in a native overflow menu with no React view to tag, so the flow addresses those by the accessibility labels the platform exposes. What each case is worth today, having mutated the code under each: - wrapping around: real. Disabling the settle fails it with the row off screen. - hit already on screen: passes, but no native mutation I tried made it fail - the fix it covers is on web. Kept as a guard, worth little on iOS. - drag after a hit: no teeth yet. It survives disabling the settle, removing the measurement gate, removing the drag release, and restoring the index-shift re-issue that caused the reported snap-back. The reported failure needed the drag to page older messages in; three drags from where the hit lands does not get there, and by then the settle's deadline has passed. Needs to drive until a page-in actually happens. Also picks up the reworked legend-list patch: the settle now keys on measurement rather than on guessing who scrolled, and remembers a measurement seen while another controller was compensating instead of dropping it - which is why hit 10 never re-aimed. --- .../chat/conversation/header-area/index.tsx | 2 +- .../conversation/messages/wrapper/wrapper.tsx | 8 +- shared/chat/conversation/search.tsx | 10 +- shared/patches/@legendapp+list+3.3.5.patch | 10048 ++++++++++------ shared/tests/e2e/ios-appium/all.test.ts | 1 + .../ios-appium/flows/chat-search-hit.test.ts | 150 + shared/tests/e2e/shared/test-ids.ts | 7 + 7 files changed, 6447 insertions(+), 3779 deletions(-) create mode 100644 shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts diff --git a/shared/chat/conversation/header-area/index.tsx b/shared/chat/conversation/header-area/index.tsx index bcb42c00574c..bf2c775acacd 100644 --- a/shared/chat/conversation/header-area/index.tsx +++ b/shared/chat/conversation/header-area/index.tsx @@ -51,7 +51,7 @@ const HeaderAreaRight = (props: HeaderConversationProps) => { noShrink={true} style={Kb.Styles.collapseStyles([styles.headerRight, {opacity: pendingWaiting ? 0 : 1}])} > - + ) diff --git a/shared/chat/conversation/messages/wrapper/wrapper.tsx b/shared/chat/conversation/messages/wrapper/wrapper.tsx index 42cb9dd9ef89..f37b910283ce 100644 --- a/shared/chat/conversation/messages/wrapper/wrapper.tsx +++ b/shared/chat/conversation/messages/wrapper/wrapper.tsx @@ -34,6 +34,7 @@ import {emptyParticipantInfo} from '../../data-hooks' import {useInboxMetadataState} from '@/chat/inbox/metadata' import type {ConversationInputState} from '../../input-area/input-state' import {useChatTeamMemberRole} from '../../team-hooks' +import * as TestIDs from '@/tests/e2e/shared/test-ids' type AccountsInfoMap = ReadonlyMap type PaymentStatusMap = ReadonlyMap @@ -1018,7 +1019,12 @@ export function WrapperMessage(p: WrapperMessageProps) { const messageContext = {isHighlighted: showCenteredHighlight, ordinal} const row = ( - + - + Cancel @@ -541,11 +547,13 @@ const ThreadSearchMobileInner = function ThreadSearchMobileInner(p: CommonProps) color={numHits > 0 ? theme.blue : theme.black_50} onClick={onUp} type="iconfont-arrow-up" + testID={TestIDs.CHAT_THREAD_SEARCH_PREV} /> 0 ? theme.blue : theme.black_50} onClick={onDown} type="iconfont-arrow-down" + testID={TestIDs.CHAT_THREAD_SEARCH_NEXT} /> diff --git a/shared/patches/@legendapp+list+3.3.5.patch b/shared/patches/@legendapp+list+3.3.5.patch index b9a06de8cceb..b000e0b9e9bc 100644 --- a/shared/patches/@legendapp+list+3.3.5.patch +++ b/shared/patches/@legendapp+list+3.3.5.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js -index b3c5a30..ae7108d 100644 +index b3c5a30..923b748 100644 --- a/node_modules/@legendapp/list/react-native.js +++ b/node_modules/@legendapp/list/react-native.js @@ -457,611 +457,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -1143,20 +1143,10 @@ index b3c5a30..ae7108d 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1972,11 +1770,32 @@ function prepareMVCP(ctx, dataChanged) { - } - } +@@ -1977,6 +1775,27 @@ var flushSync = (fn) => { + fn(); + }; --// src/platform/flushSync.native.ts --var flushSync = (fn) => { -- fn(); --}; -- -+// src/platform/flushSync.native.ts -+var flushSync = (fn) => { -+ fn(); -+}; -+ +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { @@ -1181,17 +1171,7 @@ index b3c5a30..ae7108d 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2061,6 +1880,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { - state.scrollPrevTime = state.scrollTime; - state.scroll = newScroll; - state.scrollTime = currentTime; -+ if ((options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust) { -+ releaseScrollTargetSettleIfMoved(ctx); -+ } - const scrollDelta = Math.abs(newScroll - prevScroll); - const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; - const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; -@@ -2224,7 +2046,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -2224,7 +2043,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } function scrollTo(ctx, params) { @@ -1200,7 +1180,7 @@ index b3c5a30..ae7108d 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2256,6 +2078,15 @@ function scrollTo(ctx, params) { +@@ -2256,6 +2075,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); @@ -1216,7 +1196,7 @@ index b3c5a30..ae7108d 100644 } } state.scrollPending = targetOffset; -@@ -2263,7 +2094,7 @@ function scrollTo(ctx, params) { +@@ -2263,7 +2091,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -1225,7 +1205,7 @@ index b3c5a30..ae7108d 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2276,6 +2107,309 @@ function scrollTo(ctx, params) { +@@ -2276,6 +2104,309 @@ function scrollTo(ctx, params) { } } @@ -1238,6 +1218,7 @@ index b3c5a30..ae7108d 100644 +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; + state.scheduledWork.cancel("scrollTargetSettle"); ++ state.scheduledWork.cancel("scrollTargetSettleDeadline"); +} +function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; @@ -1254,11 +1235,12 @@ index b3c5a30..ae7108d 100644 + deadline: now + SETTLE_MAX_MS, + expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), ++ measuredIndex: void 0, + quietPasses: 0, -+ requestedAt: now, + viewOffset, + viewPosition + }; ++ state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline"); +} +function getSettleTargetOffset(ctx, settle, index, position) { + const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; @@ -1282,7 +1264,7 @@ index b3c5a30..ae7108d 100644 + return; + } + settle.corrections++; -+ settle.requestedAt = Date.now(); ++ settle.measuredIndex = void 0; + scrollTo(ctx, { + animated: false, + index, @@ -1297,13 +1279,18 @@ index b3c5a30..ae7108d 100644 + }); + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +} -+function settleScrollTarget(ctx, isCompensating) { ++function settleScrollTarget(ctx, options) { ++ var _a3; + const state = ctx.state; + const settle = state.scrollTargetSettle; + if (!settle) { + return false; + } -+ if (isCompensating) { ++ const measured = options == null ? void 0 : options.minIndexSizeChanged; ++ if (measured !== void 0) { ++ settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); ++ } ++ if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return false; @@ -1323,9 +1310,13 @@ index b3c5a30..ae7108d 100644 + clearScrollTargetSettle(state); + return false; + } ++ if (settle.measuredIndex === void 0 || settle.measuredIndex > index) { ++ return false; ++ } + const targetOffset = getSettleTargetOffset(ctx, settle, index, position); + const diff = targetOffset - state.scroll; + if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ settle.measuredIndex = void 0; + settle.quietPasses++; + if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { + clearScrollTargetSettle(state); @@ -1337,17 +1328,6 @@ index b3c5a30..ae7108d 100644 + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} -+var SETTLE_ECHO_WINDOW_MS = 150; -+function releaseScrollTargetSettleIfMoved(ctx, _scrollOffset) { -+ const state = ctx.state; -+ const settle = state.scrollTargetSettle; -+ if (!settle) { -+ return; -+ } -+ if (Date.now() - settle.requestedAt > SETTLE_ECHO_WINDOW_MS) { -+ clearScrollTargetSettle(state); -+ } -+} + +// src/core/cancelImperativeScroll.ts +function cancelScrollCompletionChecks({ scheduledWork }) { @@ -1535,7 +1515,15 @@ index b3c5a30..ae7108d 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4337,8 +4471,17 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4320,6 +4451,7 @@ function calculateItemsInView(ctx, params = {}) { + startIndex + }); + totalSize = getContentSize(ctx); ++ const minIndexSizeChangedThisPass = dataChanged ? 0 : minIndexSizeChanged; + if (minIndexSizeChanged !== void 0) { + state.minIndexSizeChanged = void 0; + } +@@ -4337,8 +4469,17 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -1549,13 +1537,13 @@ index b3c5a30..ae7108d 100644 + } + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { -+ settleScrollTarget(ctx, isCompensating); ++ settleScrollTarget(ctx, { isCompensating, minIndexSizeChanged: minIndexSizeChangedThisPass }); + } + if (didMVCPAdjust) { updateScroll2(state.scroll); updateScrollRange(); } -@@ -7652,6 +7795,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7652,6 +7793,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -1564,7 +1552,7 @@ index b3c5a30..ae7108d 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs -index 40e87cd..7ed3902 100644 +index 40e87cd..b47d103 100644 --- a/node_modules/@legendapp/list/react-native.mjs +++ b/node_modules/@legendapp/list/react-native.mjs @@ -436,611 +436,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -2708,20 +2696,10 @@ index 40e87cd..7ed3902 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1951,11 +1749,32 @@ function prepareMVCP(ctx, dataChanged) { - } - } +@@ -1956,6 +1754,27 @@ var flushSync = (fn) => { + fn(); + }; --// src/platform/flushSync.native.ts --var flushSync = (fn) => { -- fn(); --}; -- -+// src/platform/flushSync.native.ts -+var flushSync = (fn) => { -+ fn(); -+}; -+ +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { @@ -2746,17 +2724,7 @@ index 40e87cd..7ed3902 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2040,6 +1859,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { - state.scrollPrevTime = state.scrollTime; - state.scroll = newScroll; - state.scrollTime = currentTime; -+ if ((options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust) { -+ releaseScrollTargetSettleIfMoved(ctx); -+ } - const scrollDelta = Math.abs(newScroll - prevScroll); - const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; - const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; -@@ -2203,7 +2025,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -2203,7 +2022,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } function scrollTo(ctx, params) { @@ -2765,7 +2733,7 @@ index 40e87cd..7ed3902 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2235,6 +2057,15 @@ function scrollTo(ctx, params) { +@@ -2235,6 +2054,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); @@ -2781,7 +2749,7 @@ index 40e87cd..7ed3902 100644 } } state.scrollPending = targetOffset; -@@ -2242,7 +2073,7 @@ function scrollTo(ctx, params) { +@@ -2242,7 +2070,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -2790,7 +2758,7 @@ index 40e87cd..7ed3902 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2255,6 +2086,309 @@ function scrollTo(ctx, params) { +@@ -2255,6 +2083,309 @@ function scrollTo(ctx, params) { } } @@ -2803,6 +2771,7 @@ index 40e87cd..7ed3902 100644 +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; + state.scheduledWork.cancel("scrollTargetSettle"); ++ state.scheduledWork.cancel("scrollTargetSettleDeadline"); +} +function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; @@ -2819,11 +2788,12 @@ index 40e87cd..7ed3902 100644 + deadline: now + SETTLE_MAX_MS, + expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), ++ measuredIndex: void 0, + quietPasses: 0, -+ requestedAt: now, + viewOffset, + viewPosition + }; ++ state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline"); +} +function getSettleTargetOffset(ctx, settle, index, position) { + const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; @@ -2847,7 +2817,7 @@ index 40e87cd..7ed3902 100644 + return; + } + settle.corrections++; -+ settle.requestedAt = Date.now(); ++ settle.measuredIndex = void 0; + scrollTo(ctx, { + animated: false, + index, @@ -2862,13 +2832,18 @@ index 40e87cd..7ed3902 100644 + }); + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +} -+function settleScrollTarget(ctx, isCompensating) { ++function settleScrollTarget(ctx, options) { ++ var _a3; + const state = ctx.state; + const settle = state.scrollTargetSettle; + if (!settle) { + return false; + } -+ if (isCompensating) { ++ const measured = options == null ? void 0 : options.minIndexSizeChanged; ++ if (measured !== void 0) { ++ settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); ++ } ++ if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return false; @@ -2888,9 +2863,13 @@ index 40e87cd..7ed3902 100644 + clearScrollTargetSettle(state); + return false; + } ++ if (settle.measuredIndex === void 0 || settle.measuredIndex > index) { ++ return false; ++ } + const targetOffset = getSettleTargetOffset(ctx, settle, index, position); + const diff = targetOffset - state.scroll; + if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ settle.measuredIndex = void 0; + settle.quietPasses++; + if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { + clearScrollTargetSettle(state); @@ -2902,17 +2881,6 @@ index 40e87cd..7ed3902 100644 + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} -+var SETTLE_ECHO_WINDOW_MS = 150; -+function releaseScrollTargetSettleIfMoved(ctx, _scrollOffset) { -+ const state = ctx.state; -+ const settle = state.scrollTargetSettle; -+ if (!settle) { -+ return; -+ } -+ if (Date.now() - settle.requestedAt > SETTLE_ECHO_WINDOW_MS) { -+ clearScrollTargetSettle(state); -+ } -+} + +// src/core/cancelImperativeScroll.ts +function cancelScrollCompletionChecks({ scheduledWork }) { @@ -3100,7 +3068,15 @@ index 40e87cd..7ed3902 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4316,8 +4450,17 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4299,6 +4430,7 @@ function calculateItemsInView(ctx, params = {}) { + startIndex + }); + totalSize = getContentSize(ctx); ++ const minIndexSizeChangedThisPass = dataChanged ? 0 : minIndexSizeChanged; + if (minIndexSizeChanged !== void 0) { + state.minIndexSizeChanged = void 0; + } +@@ -4316,8 +4448,17 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -3114,13 +3090,13 @@ index 40e87cd..7ed3902 100644 + } + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { -+ settleScrollTarget(ctx, isCompensating); ++ settleScrollTarget(ctx, { isCompensating, minIndexSizeChanged: minIndexSizeChangedThisPass }); + } + if (didMVCPAdjust) { updateScroll2(state.scroll); updateScrollRange(); } -@@ -7631,6 +7774,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7631,6 +7772,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -3129,10 +3105,10 @@ index 40e87cd..7ed3902 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js -index 914d2da..ce6a0d7 100644 +index 914d2da..c3ed202 100644 --- a/node_modules/@legendapp/list/react-native.web.js +++ b/node_modules/@legendapp/list/react-native.web.js -@@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; +@@ -434,178 +434,266 @@ var EDGE_POSITION_EPSILON = 1; var ENABLE_DEVMODE = IS_DEV && false; var ENABLE_DEBUG_VIEW = IS_DEV && false; @@ -3142,22 +3118,56 @@ index 914d2da..ce6a0d7 100644 - scheduledWork.cancel("checkFinishedScrollRetryFrame"); - scheduledWork.cancel("checkFinishedScrollFallback"); - scheduledWork.cancel("platformScrollCompletion"); --} ++// src/core/getStartOffsetAdjustment.ts ++function getStartOffsetAdjustment(ctx) { ++ const { state } = ctx; ++ const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; ++ return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); + } -function settlePendingImperativeScroll(state) { - var _a3, _b; - const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; - state.pendingScrollResolve = void 0; - state.pendingScrollToEnd = void 0; - resolvePendingScroll == null ? void 0 : resolvePendingScroll(); --} ++ ++// src/utils/getId.ts ++function getId(state, index) { ++ const { data, keyExtractor } = state.props; ++ if (!data) { ++ return ""; ++ } ++ const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; ++ const id = ret; ++ state.idCache[index] = id; ++ return id; + } -function cancelImperativeScroll(state) { - cancelScrollCompletionChecks(state); - state.scheduledWork.cancel("imperativeScrollReady"); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; - settlePendingImperativeScroll(state); --} -- ++ ++// src/core/updateContentMetricsState.ts ++function getRawContentLength(ctx) { ++ var _a3, _b, _c; ++ const { state, values } = ctx; ++ return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); ++} ++function getAlignItemsAtEndPadding(ctx) { ++ const { state } = ctx; ++ const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; ++ return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; ++} ++function updateContentMetricsState(ctx) { ++ const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; ++ const nextPadding = getAlignItemsAtEndPadding(ctx); ++ if (previousPadding !== nextPadding) { ++ set$(ctx, "alignItemsAtEndPadding", nextPadding); ++ } + } + -// src/core/deferredPublicOnScroll.ts -function withResolvedContentOffset(state, event, resolvedOffset) { - return { @@ -3165,12 +3175,40 @@ index 914d2da..ce6a0d7 100644 - nativeEvent: { - ...event.nativeEvent, - contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -- } ++// src/core/addTotalSize.ts ++function addTotalSize(ctx, key, add, notifyTotalSize = true) { ++ const state = ctx.state; ++ const prevTotalSize = state.totalSize; ++ let totalSize = state.totalSize; ++ if (key === null) { ++ totalSize = add; ++ if (state.timeoutSetPaddingTop) { ++ clearTimeout(state.timeoutSetPaddingTop); ++ state.timeoutSetPaddingTop = void 0; + } - }; --} ++ } else { ++ totalSize += add; ++ } ++ if (prevTotalSize !== totalSize) { ++ { ++ state.pendingTotalSize = void 0; ++ state.totalSize = totalSize; ++ if (notifyTotalSize) { ++ set$(ctx, "totalSize", totalSize); ++ } ++ updateContentMetricsState(ctx); ++ } ++ } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { ++ set$(ctx, "totalSize", totalSize); ++ } + } -function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { - var _a3, _b, _c, _d; -- const state = ctx.state; ++ ++// src/core/setSize.ts ++function setSize(ctx, itemKey, size, notifyTotalSize = true) { + const state = ctx.state; - const deferredEvent = state.deferredPublicOnScrollEvent; - state.deferredPublicOnScrollEvent = void 0; - if (deferredEvent) { @@ -3182,9 +3220,15 @@ index 914d2da..ce6a0d7 100644 - (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 - ) - ); -- } --} -- ++ const { sizes } = state; ++ const previousSize = sizes.get(itemKey); ++ const diff = previousSize !== void 0 ? size - previousSize : size; ++ if (diff !== 0) { ++ addTotalSize(ctx, itemKey, diff, notifyTotalSize); + } ++ sizes.set(itemKey, size); + } + -// src/core/initialScrollSession.ts -var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -function hasInitialScrollSessionCompletion(completion) { @@ -3193,7 +3237,10 @@ index 914d2da..ce6a0d7 100644 -function clearInitialScrollSession(state) { - state.initialScrollSession = void 0; - return void 0; --} ++// src/utils/helpers.ts ++function isFunction(obj) { ++ return typeof obj === "function"; + } -function createInitialScrollSession(options) { - const { bootstrap, completion, kind, previousDataLength } = options; - return kind === "offset" ? { @@ -3206,7 +3253,9 @@ index 914d2da..ce6a0d7 100644 - kind, - previousDataLength - }; --} ++function isArray(obj) { ++ return Array.isArray(obj); + } -function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { - var _a4, _b2; - if (!state.initialScrollSession) { @@ -3222,10 +3271,15 @@ index 914d2da..ce6a0d7 100644 - kind, - previousDataLength: state.initialScrollSession.previousDataLength - }); -- } ++var warned = /* @__PURE__ */ new Set(); ++function warnDevOnce(id, text) { ++ if (IS_DEV && !warned.has(id)) { ++ warned.add(id); ++ console.warn(`[legend-list] ${text}`); + } - (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; - return state.initialScrollSession.completion; --} + } -var initialScrollCompletion = { - didDispatchNativeScroll(state) { - var _a3, _b; @@ -3248,7 +3302,26 @@ index 914d2da..ce6a0d7 100644 - const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); - completion.didDispatchNativeScroll = void 0; - completion.didRetrySilentInitialScroll = void 0; -- } ++function roundSize(size) { ++ return Math.floor(size * 8) / 8; ++} ++function isNullOrUndefined(value) { ++ return value === null || value === void 0; ++} ++function getPadding(s, type) { ++ var _a3, _b, _c; ++ const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; ++ return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; ++} ++function extractPadding(style, contentContainerStyle, type) { ++ return getPadding(style, type) + getPadding(contentContainerStyle, type); ++} ++function findContainerId(ctx, key) { ++ var _a3, _b; ++ const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); ++ if (directMatch !== void 0) { ++ return directMatch; + } -}; -var initialScrollWatchdog = { - clear(state) { @@ -3272,7 +3345,12 @@ index 914d2da..ce6a0d7 100644 - var _a3, _b; - if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { - return; -- } ++ const numContainers = peek$(ctx, "numContainers"); ++ for (let i = 0; i < numContainers; i++) { ++ const itemKey = peek$(ctx, `containerItemKey${i}`); ++ if (itemKey === key) { ++ return i; + } - const completion = ensureInitialScrollSessionCompletion(state); - completion.watchdog = watchdog ? { - startScroll: watchdog.startScroll, @@ -3292,7 +3370,7 @@ index 914d2da..ce6a0d7 100644 - } - if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { - return clearInitialScrollSession(state); -- } + } - const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; - state.initialScrollSession = createInitialScrollSession({ - bootstrap, @@ -3301,247 +3379,187 @@ index 914d2da..ce6a0d7 100644 - previousDataLength - }); - return state.initialScrollSession; --} -- ++ return -1; + } + -// src/utils/checkThreshold.ts -var HYSTERESIS_MULTIPLIER = 1.3; -function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { - const absDistance = Math.abs(distance); - return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; ++// src/utils/getItemSize.ts ++function getKnownOrFixedSize(ctx, key, index, data, resolved) { ++ var _a3, _b; ++ const state = ctx.state; ++ const { getFixedItemSize, getItemType } = state.props; ++ let size = key ? state.sizesKnown.get(key) : void 0; ++ if (size === void 0 && key && getFixedItemSize) { ++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; ++ const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); ++ if (fixedSize !== void 0) { ++ size = fixedSize + ctx.scrollAxisGap; ++ state.sizesKnown.set(key, size); ++ } ++ } ++ return size; ++} ++function getKnownOrFixedItemSize(ctx, index) { ++ const key = getId(ctx.state, index); ++ return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); ++} ++function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { ++ for (let index = startIndex; index <= endIndex; index++) { ++ if (getKnownOrFixedItemSize(ctx, index) === void 0) { ++ return false; ++ } ++ } ++ return true; ++} ++function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { ++ var _a3, _b, _c, _d; ++ const state = ctx.state; ++ const { ++ sizes, ++ averageSizes, ++ props: { estimatedItemSize, getItemType }, ++ scrollingTo ++ } = state; ++ const sizeKnown = state.sizesKnown.get(key); ++ if (sizeKnown !== void 0) { ++ return sizeKnown; ++ } ++ let size; ++ const renderedSize = sizes.get(key); ++ if (preferCachedSize) { ++ if (renderedSize !== void 0) { ++ return renderedSize; ++ } ++ } ++ size = getKnownOrFixedSize(ctx, key, index, data, resolved); ++ if (size !== void 0) { ++ setSize(ctx, key, size, notifyTotalSize); ++ return size; ++ } ++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; ++ if (useAverageSize && !scrollingTo) { ++ const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; ++ if (averageSizeForType !== void 0) { ++ size = roundSize(averageSizeForType); ++ } ++ } ++ if (size === void 0 && renderedSize !== void 0) { ++ return renderedSize; ++ } ++ if (size === void 0 && useAverageSize && scrollingTo) { ++ const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; ++ if (averageSizeForType !== void 0) { ++ size = roundSize(averageSizeForType); ++ } ++ } ++ if (size === void 0) { ++ size = estimatedItemSize + ctx.scrollAxisGap; ++ } ++ setSize(ctx, key, size, notifyTotalSize); ++ return size; ++} ++function getItemSizeAtIndex(ctx, index) { ++ if (index === void 0 || index < 0) { ++ return void 0; ++ } ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; ++ } ++ if (index !== void 0) { ++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); ++ if (startOffsetAdjustment) { ++ offset += startOffsetAdjustment; ++ } ++ } ++ if (viewPosition !== void 0 && index !== void 0) { ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return offset; ++ } ++ const isOutOfBounds = index < 0 || index >= dataLength; ++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; ++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); ++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); ++ const trailingInset = getContentInsetEnd(ctx); ++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); ++ if (!isOutOfBounds && index === state.props.data.length - 1) { ++ const footerSize = peek$(ctx, "footerSize") || 0; ++ offset += footerSize; ++ } ++ } ++ return offset; ++} ++ ++// src/core/clampScrollOffset.ts ++function clampScrollOffset(ctx, offset, scrollTarget) { ++ const state = ctx.state; ++ const contentSize = getContentSize(ctx); ++ let clampedOffset = offset; ++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { ++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); ++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; ++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; ++ const maxOffset = baseMaxOffset + extraEndOffset; ++ clampedOffset = Math.min(offset, maxOffset); ++ } ++ clampedOffset = Math.max(0, clampedOffset); ++ return clampedOffset; ++} ++ ++// src/utils/checkThreshold.ts ++var HYSTERESIS_MULTIPLIER = 1.3; ++function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { ++ const absDistance = Math.abs(distance); ++ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; + } + var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { + const absDistance = Math.abs(distance); +@@ -815,642 +903,346 @@ function recalculateSettledScroll(ctx) { + } + checkThresholds(ctx); + } +-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; +-function clearAdaptiveRenderExitTimeout(ctx) { +- ctx.state.scheduledWork.cancel("adaptiveRender"); -} --var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { -- const absDistance = Math.abs(distance); -- const within = atThreshold || threshold > 0 && absDistance <= threshold; -- const updateSnapshot = () => { -- setSnapshot({ -- atThreshold, -- contentSize: context.contentSize, -- dataLength: context.dataLength, -- scrollPosition: context.scrollPosition -- }); -- }; -- if (!wasReached) { -- if (!within) { -- return false; -- } -- onReached(distance); -- updateSnapshot(); -- return true; -- } -- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); -- if (reset) { -- setSnapshot(void 0); -- return false; -- } -- if (within) { -- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; -- if (changed) { -- updateSnapshot(); -- } -- } -- return true; --}; -- --// src/utils/edgeReachedGate.ts --function resetEdgeLatch(ctx, edge) { +-function scheduleAdaptiveRenderExit(ctx, exitDelay) { - const state = ctx.state; -- if (edge === "start") { -- state.isStartReached = false; -- state.startReachedSnapshot = void 0; +- clearAdaptiveRenderExitTimeout(ctx); +- if (exitDelay <= 0) { +- setAdaptiveRender(ctx, "normal", "scroll"); - } else { -- state.isEndReached = false; -- state.endReachedSnapshot = void 0; +- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); - } -} --function resetSharedEdgeGateIfOutsideHysteresis(ctx) { -- const state = ctx.state; -- if (!state.edgeReachedGate) { -- return; -- } -- const contentSize = getContentSize(ctx); -- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); -- const isContentLess = contentSize < state.scrollLength; -- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; -- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; -- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); -- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); -- if (isOutsideStart && isOutsideEnd) { -- state.edgeReachedGate = void 0; -- } --} --function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { -- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; --} --function markReachedEdge(ctx) { -- ctx.state.edgeReachedGate = "closed"; --} --function prepareReachedEdgeForNextUserScroll(ctx) { -- if (ctx.state.edgeReachedGate) { -- ctx.state.edgeReachedGate = "prepared"; -- } --} --function beginReachedEdgeUserScroll(ctx, scrollDelta) { -- const state = ctx.state; -- if (state.edgeReachedGate !== "prepared") { -- return void 0; -- } -- const allowedEdge = scrollDelta < 0 ? "start" : "end"; -- state.edgeReachedGate = "closed"; -- resetEdgeLatch(ctx, allowedEdge); -- return allowedEdge; --} -- --// src/utils/hasActiveInitialScroll.ts --function hasActiveInitialScroll(state) { -- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; --} -- --// src/utils/checkAtBottom.ts --function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -- var _a3; -- const state = ctx.state; -- if (!state) { -- return; -- } -- const { -- queuedInitialLayout, -- scrollLength, -- scroll, -- maintainingScrollAtEnd, -- props: { maintainScrollAtEndThreshold, onEndReachedThreshold } -- } = state; -- const contentSize = getContentSize(ctx); -- resetSharedEdgeGateIfOutsideHysteresis(ctx); -- if (contentSize > 0 && queuedInitialLayout) { -- const insetEnd = getContentInsetEnd(ctx); -- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; -- const isContentLess = contentSize < scrollLength; -- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); -- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); -- set$( -- ctx, -- "isWithinMaintainScrollAtEndThreshold", -- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength -- ); -- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; -- if (!shouldSkipThresholdChecks) { -- state.isEndReached = checkThreshold( -- distanceFromEnd, -- isContentLess, -- onEndReachedThreshold * scrollLength, -- state.isEndReached, -- state.endReachedSnapshot, -- { -- contentSize, -- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, -- scrollPosition: scroll -- }, -- (distance) => { -- var _a4, _b; -- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { -- markReachedEdge(ctx); -- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); -- } -- }, -- (snapshot) => { -- state.endReachedSnapshot = snapshot; -- } -- ); -- } -- } --} -- --// src/utils/checkAtTop.ts --function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -- const state = ctx == null ? void 0 : ctx.state; -- if (!state) { -- return; -- } -- const { -- isStartReached, -- props: { data, onStartReachedThreshold }, -- scroll, -- scrollLength, -- startReachedSnapshot, -- totalSize -- } = state; -- const dataLength = data.length; -- const threshold = onStartReachedThreshold * scrollLength; -- resetSharedEdgeGateIfOutsideHysteresis(ctx); -- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { -- state.isStartReached = false; -- state.startReachedSnapshot = void 0; -- } -- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); -- set$(ctx, "isNearStart", scroll <= threshold); -- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; -- if (!shouldSkipThresholdChecks) { -- state.isStartReached = checkThreshold( -- scroll, -- false, -- threshold, -- state.isStartReached, -- startReachedSnapshot, -- { -- contentSize: totalSize, -- dataLength, -- scrollPosition: scroll -- }, -- (distance) => { -- var _a3, _b; -- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { -- markReachedEdge(ctx); -- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); -- } -- }, -- (snapshot) => { -- state.startReachedSnapshot = snapshot; -- } -- ); -- } --} -- --// src/utils/checkThresholds.ts --function checkThresholds(ctx, allowedEdge) { -- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; -- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); --} -- --// src/core/recalculateSettledScroll.ts --function recalculateSettledScroll(ctx) { -- var _a3, _b; -- const state = ctx.state; -- if ((_a3 = state.props) == null ? void 0 : _a3.data) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); -- } -- checkThresholds(ctx); --} --var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; --var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; --var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; --function clearAdaptiveRenderExitTimeout(ctx) { -- ctx.state.scheduledWork.cancel("adaptiveRender"); --} --function scheduleAdaptiveRenderExit(ctx, exitDelay) { -- const state = ctx.state; -- clearAdaptiveRenderExitTimeout(ctx); -- if (exitDelay <= 0) { -- setAdaptiveRender(ctx, "normal", "scroll"); -- } else { -- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); -- } --} --function setAdaptiveRender(ctx, mode, reason) { -- var _a3, _b; -- const previousMode = peek$(ctx, "adaptiveRender"); -- if (previousMode !== mode) { -- set$(ctx, "adaptiveRender", mode); -- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); +-function setAdaptiveRender(ctx, mode, reason) { +- var _a3, _b; +- const previousMode = peek$(ctx, "adaptiveRender"); +- if (previousMode !== mode) { +- set$(ctx, "adaptiveRender", mode); +- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); - } -} -function resetAdaptiveRender(ctx) { -- var _a3, _b; ++ ++// src/core/finishScrollTo.ts ++function finishScrollTo(ctx) { + var _a3, _b; - clearAdaptiveRenderExitTimeout(ctx); - const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; - if (peek$(ctx, "adaptiveRender") !== mode) { @@ -3550,7 +3568,7 @@ index 914d2da..ce6a0d7 100644 -} -function updateAdaptiveRender(ctx, scrollVelocity, options) { - var _a3, _b, _c; -- const state = ctx.state; + const state = ctx.state; - const adaptiveRender = state.props.adaptiveRender; - const currentMode = peek$(ctx, "adaptiveRender"); - if (peek$(ctx, "readyToRender")) { @@ -3571,10 +3589,40 @@ index 914d2da..ce6a0d7 100644 - } - } else { - resetAdaptiveRender(ctx); -- } -- } --} -- ++ if (state == null ? void 0 : state.scrollingTo) { ++ cancelScrollCompletionChecks(state); ++ const resolvePendingScroll = state.pendingScrollResolve; ++ state.pendingScrollResolve = void 0; ++ const scrollingTo = state.scrollingTo; ++ state.scrollHistory.length = 0; ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ if (state.pendingTotalSize !== void 0) { ++ addTotalSize(ctx, null, state.pendingTotalSize); ++ } ++ { ++ state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); ++ } ++ if (scrollingTo.isInitialScroll || state.initialScroll) { ++ const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; ++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; ++ finishInitialScroll(ctx, { ++ onFinished: () => { ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); ++ }, ++ preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, ++ recalculateItems: true, ++ schedulePreservedTargetClear: shouldPreserveResizeTarget, ++ syncObservedOffset: isOffsetSession, ++ waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame ++ }); ++ return; + } ++ recalculateSettledScroll(ctx); ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); + } + } + -// src/utils/getEffectiveDrawDistance.ts -var INITIAL_DRAW_DISTANCE = 50; -function getEffectiveDrawDistance(ctx, mode) { @@ -3588,8 +3636,21 @@ index 914d2da..ce6a0d7 100644 -function scheduleFullDrawDistancePrewarm(ctx) { - const { state } = ctx; - if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { -- return; -- } ++// src/core/doScrollTo.ts ++var SCROLL_END_IDLE_MS = 80; ++var SCROLL_END_MAX_MS = 1500; ++var SMOOTH_SCROLL_DURATION_MS = 320; ++var SCROLL_END_TARGET_EPSILON = 1; ++function doScrollTo(ctx, params) { ++ var _a3, _b; ++ const state = ctx.state; ++ const { animated, horizontal, offset } = params; ++ state.scheduledWork.cancel("platformScrollCompletion"); ++ const scroller = state.refScroller.current; ++ const node = scroller == null ? void 0 : scroller.getScrollableNode(); ++ if (!scroller || !node) { + return; + } - state.scheduledWork.frame(() => { - var _a3; - return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); @@ -3608,10 +3669,35 @@ index 914d2da..ce6a0d7 100644 - } - if (resetInitialScroll) { - state.didFinishInitialScroll = false; -- } ++ const isAnimated = !!animated; ++ const isHorizontal = !!horizontal; ++ const contentSize = isHorizontal ? getContentSize(ctx) : void 0; ++ const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; ++ const top = isHorizontal ? 0 : offset; ++ scroller.scrollTo({ animated: isAnimated, x: left, y: top }); ++ if (isAnimated) { ++ const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; ++ listenForScrollEnd(ctx, { ++ readOffset: () => scroller.getCurrentScrollOffset(), ++ target, ++ targetOffset: offset ++ }); ++ } else { ++ state.scroll = offset; ++ const targetToken = state.scrollingTo; ++ state.scheduledWork.timeout( ++ () => { ++ if (targetToken === state.scrollingTo) { ++ finishScrollTo(ctx); ++ } ++ }, ++ 100, ++ "platformScrollCompletion" ++ ); + } - set$(ctx, "readyToRender", false); - resetAdaptiveRender(ctx); --} + } -function setInitialRenderState(ctx, { - didLayout, - didInitialScroll @@ -3640,7 +3726,12 @@ index 914d2da..ce6a0d7 100644 - onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); - } - } -- } ++function listenForScrollEnd(ctx, params) { ++ const { readOffset, target, targetOffset } = params; ++ if (!target) { ++ finishScrollTo(ctx); ++ return; + } -} - -// src/core/finishInitialScroll.ts @@ -3666,12 +3757,23 @@ index 914d2da..ce6a0d7 100644 - if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { - if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { - cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -- } ++ const supportsScrollEnd = "onscrollend" in target; ++ let idleTimeout; ++ let settled = false; ++ const { scheduledWork, scrollingTo: targetToken } = ctx.state; ++ const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); ++ const cleanup = () => { ++ target.removeEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.removeEventListener("scrollend", onScrollEnd); + } - if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { - cancelScrollCompletionChecks(state); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; -- } ++ if (idleTimeout !== void 0) { ++ clearTimeout(idleTimeout); + } - initialScrollCompletion.resetFlags(state); - setInitialScrollSession(state, { bootstrap: null }); - finishInitialScroll(ctx); @@ -3686,7 +3788,13 @@ index 914d2da..ce6a0d7 100644 - const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); - if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { - syncInitialScrollOffset(state, observedOffset); -- } ++ clearTimeout(maxTimeout); ++ }; ++ const cancel = () => { ++ if (!settled) { ++ settled = true; ++ cleanup(); + } - } - const complete = () => { - var _a4, _b2, _c2, _d, _e; @@ -3712,240 +3820,422 @@ index 914d2da..ce6a0d7 100644 - } - } else { - clearPreservedInitialScrollTarget(state); -- } ++ }; ++ const finish = (reason) => { ++ if (settled) return; ++ if (targetToken !== ctx.state.scrollingTo) { ++ scheduledWork.cancel("platformScrollCompletion"); ++ return; + } - if (options == null ? void 0 : options.recalculateItems) { - recalculateSettledScroll(ctx); -- } ++ const currentOffset = readOffset(); ++ const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; ++ if (reason === "scrollend" && !isNearTarget) { ++ return; + } - setInitialRenderState(ctx, { didInitialScroll: true }); - if (shouldReleaseDeferredPublicOnScroll) { - releaseDeferredPublicOnScroll(ctx, finalScrollOffset); -- } ++ scheduledWork.cancel("platformScrollCompletion"); ++ finishScrollTo(ctx); ++ }; ++ const onScroll2 = () => { ++ if (idleTimeout !== void 0) { ++ clearTimeout(idleTimeout); + } - (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); -- }; ++ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); + }; - if (options == null ? void 0 : options.waitForCompletionFrame) { - requestAnimationFrame(complete); - return; -- } ++ const onScrollEnd = () => finish("scrollend"); ++ target.addEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.addEventListener("scrollend", onScrollEnd); ++ } else { ++ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); + } - complete(); --} -- ++ scheduledWork.register("platformScrollCompletion", cancel); + } + -// src/core/calculateOffsetForIndex.ts -function calculateOffsetForIndex(ctx, index) { - const state = ctx.state; - return index !== void 0 ? state.positions[index] || 0 : 0; --} ++// src/core/initialScrollSession.ts ++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; ++function hasInitialScrollSessionCompletion(completion) { ++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); + } - - // src/core/getStartOffsetAdjustment.ts - function getStartOffsetAdjustment(ctx) { - const { state } = ctx; -@@ -1234,58 +635,273 @@ function getItemSizeAtIndex(ctx, index) { - if (index === void 0 || index < 0) { - return void 0; - } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+ const targetId = getId(ctx.state, index); -+ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+} -+ -+// src/core/calculateOffsetWithOffsetPosition.ts -+function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -+ var _a3; -+ const state = ctx.state; -+ const { index, viewOffset, viewPosition } = params; -+ let offset = offsetParam; -+ if (viewOffset) { -+ offset -= viewOffset; -+ } -+ if (index !== void 0) { -+ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -+ if (startOffsetAdjustment) { -+ offset += startOffsetAdjustment; -+ } -+ } -+ if (viewPosition !== void 0 && index !== void 0) { -+ const dataLength = state.props.data.length; -+ if (dataLength === 0) { -+ return offset; -+ } -+ const isOutOfBounds = index < 0 || index >= dataLength; -+ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -+ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -+ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -+ const trailingInset = getContentInsetEnd(ctx); -+ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -+ if (!isOutOfBounds && index === state.props.data.length - 1) { -+ const footerSize = peek$(ctx, "footerSize") || 0; -+ offset += footerSize; -+ } -+ } -+ return offset; -+} -+ -+// src/core/clampScrollOffset.ts -+function clampScrollOffset(ctx, offset, scrollTarget) { -+ const state = ctx.state; -+ const contentSize = getContentSize(ctx); -+ let clampedOffset = offset; -+ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { -+ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); -+ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; -+ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; -+ const maxOffset = baseMaxOffset + extraEndOffset; -+ clampedOffset = Math.min(offset, maxOffset); -+ } -+ clampedOffset = Math.max(0, clampedOffset); -+ return clampedOffset; -+} -+ -+// src/utils/checkThreshold.ts -+var HYSTERESIS_MULTIPLIER = 1.3; -+function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { -+ const absDistance = Math.abs(distance); -+ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; -+} -+var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { -+ const absDistance = Math.abs(distance); -+ const within = atThreshold || threshold > 0 && absDistance <= threshold; -+ const updateSnapshot = () => { -+ setSnapshot({ -+ atThreshold, -+ contentSize: context.contentSize, -+ dataLength: context.dataLength, -+ scrollPosition: context.scrollPosition -+ }); +-// src/core/getStartOffsetAdjustment.ts +-function getStartOffsetAdjustment(ctx) { +- const { state } = ctx; +- const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; +- return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); ++function clearInitialScrollSession(state) { ++ state.initialScrollSession = void 0; ++ return void 0; + } +- +-// src/utils/getId.ts +-function getId(state, index) { +- const { data, keyExtractor } = state.props; +- if (!data) { +- return ""; +- } +- const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; +- const id = ret; +- state.idCache[index] = id; +- return id; ++function createInitialScrollSession(options) { ++ const { bootstrap, completion, kind, previousDataLength } = options; ++ return kind === "offset" ? { ++ completion, ++ kind, ++ previousDataLength ++ } : { ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength + }; -+ if (!wasReached) { -+ if (!within) { -+ return false; -+ } -+ onReached(distance); -+ updateSnapshot(); -+ return true; -+ } -+ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); -+ if (reset) { -+ setSnapshot(void 0); -+ return false; -+ } -+ if (within) { -+ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; -+ if (changed) { -+ updateSnapshot(); -+ } + } +- +-// src/core/updateContentMetricsState.ts +-function getRawContentLength(ctx) { +- var _a3, _b, _c; +- const { state, values } = ctx; +- return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); ++function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { ++ var _a4, _b2; ++ if (!state.initialScrollSession) { ++ state.initialScrollSession = createInitialScrollSession({ ++ completion: {}, ++ kind, ++ previousDataLength: 0 ++ }); ++ } else if (state.initialScrollSession.kind !== kind) { ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, ++ completion: state.initialScrollSession.completion, ++ kind, ++ previousDataLength: state.initialScrollSession.previousDataLength ++ }); + } -+ return true; ++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; ++ return state.initialScrollSession.completion; + } +-function getAlignItemsAtEndPadding(ctx) { +- const { state } = ctx; +- const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; +- return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; +-} +-function updateContentMetricsState(ctx) { +- const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; +- const nextPadding = getAlignItemsAtEndPadding(ctx); +- if (previousPadding !== nextPadding) { +- set$(ctx, "alignItemsAtEndPadding", nextPadding); +- } +-} +- +-// src/core/addTotalSize.ts +-function addTotalSize(ctx, key, add, notifyTotalSize = true) { +- const state = ctx.state; +- const prevTotalSize = state.totalSize; +- let totalSize = state.totalSize; +- if (key === null) { +- totalSize = add; +- if (state.timeoutSetPaddingTop) { +- clearTimeout(state.timeoutSetPaddingTop); +- state.timeoutSetPaddingTop = void 0; ++var initialScrollCompletion = { ++ didDispatchNativeScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); ++ }, ++ didRetrySilentInitialScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); ++ }, ++ markInitialScrollNativeDispatch(state) { ++ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; ++ }, ++ markSilentInitialScrollRetry(state) { ++ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; ++ }, ++ resetFlags(state) { ++ if (!state.initialScrollSession) { ++ return; + } +- } else { +- totalSize += add; ++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); ++ completion.didDispatchNativeScroll = void 0; ++ completion.didRetrySilentInitialScroll = void 0; + } +- if (prevTotalSize !== totalSize) { +- { +- state.pendingTotalSize = void 0; +- state.totalSize = totalSize; +- if (notifyTotalSize) { +- set$(ctx, "totalSize", totalSize); +- } +- updateContentMetricsState(ctx); +}; -+ -+// src/utils/edgeReachedGate.ts -+function resetEdgeLatch(ctx, edge) { ++var initialScrollWatchdog = { ++ clear(state) { ++ initialScrollWatchdog.set(state, void 0); ++ }, ++ didReachTarget(newScroll, watchdog) { ++ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); ++ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ get(state) { ++ var _a3, _b; ++ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; ++ }, ++ hasNonZeroTargetOffset(targetOffset) { ++ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ isAtZeroTargetOffset(targetOffset) { ++ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ set(state, watchdog) { ++ var _a3, _b; ++ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { ++ return; + } +- } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { +- set$(ctx, "totalSize", totalSize); ++ const completion = ensureInitialScrollSessionCompletion(state); ++ completion.watchdog = watchdog ? { ++ startScroll: watchdog.startScroll, ++ targetOffset: watchdog.targetOffset ++ } : void 0; + } +-} +- +-// src/core/setSize.ts +-function setSize(ctx, itemKey, size, notifyTotalSize = true) { +- const state = ctx.state; +- const { sizes } = state; +- const previousSize = sizes.get(itemKey); +- const diff = previousSize !== void 0 ? size - previousSize : size; +- if (diff !== 0) { +- addTotalSize(ctx, itemKey, diff, notifyTotalSize); ++}; ++function setInitialScrollSession(state, options = {}) { ++ var _a3, _b, _c, _d; ++ const existingSession = state.initialScrollSession; ++ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; ++ const completion = existingSession == null ? void 0 : existingSession.completion; ++ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; ++ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; ++ if (!kind) { ++ return clearInitialScrollSession(state); + } +- sizes.set(itemKey, size); +-} +- +-// src/utils/helpers.ts +-function isFunction(obj) { +- return typeof obj === "function"; +-} +-function isArray(obj) { +- return Array.isArray(obj); +-} +-var warned = /* @__PURE__ */ new Set(); +-function warnDevOnce(id, text) { +- if (IS_DEV && !warned.has(id)) { +- warned.add(id); +- console.warn(`[legend-list] ${text}`); ++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { ++ return clearInitialScrollSession(state); + } ++ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }); ++ return state.initialScrollSession; + } +-function roundSize(size) { +- return Math.floor(size * 8) / 8; +-} +-function isNullOrUndefined(value) { +- return value === null || value === void 0; +-} +-function getPadding(s, type) { +- var _a3, _b, _c; +- const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; +- return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; ++var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; ++function clearAdaptiveRenderExitTimeout(ctx) { ++ ctx.state.scheduledWork.cancel("adaptiveRender"); + } +-function extractPadding(style, contentContainerStyle, type) { +- return getPadding(style, type) + getPadding(contentContainerStyle, type); ++function scheduleAdaptiveRenderExit(ctx, exitDelay) { + const state = ctx.state; -+ if (edge === "start") { -+ state.isStartReached = false; -+ state.startReachedSnapshot = void 0; ++ clearAdaptiveRenderExitTimeout(ctx); ++ if (exitDelay <= 0) { ++ setAdaptiveRender(ctx, "normal", "scroll"); + } else { -+ state.isEndReached = false; -+ state.endReachedSnapshot = void 0; -+ } -+} -+function resetSharedEdgeGateIfOutsideHysteresis(ctx) { -+ const state = ctx.state; -+ if (!state.edgeReachedGate) { -+ return; -+ } -+ const contentSize = getContentSize(ctx); -+ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); -+ const isContentLess = contentSize < state.scrollLength; -+ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; -+ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; -+ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); -+ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); -+ if (isOutsideStart && isOutsideEnd) { -+ state.edgeReachedGate = void 0; ++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); + } -+} -+function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { -+ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; -+} -+function markReachedEdge(ctx) { -+ ctx.state.edgeReachedGate = "closed"; -+} -+function prepareReachedEdgeForNextUserScroll(ctx) { -+ if (ctx.state.edgeReachedGate) { -+ ctx.state.edgeReachedGate = "prepared"; -+ } -+} -+function beginReachedEdgeUserScroll(ctx, scrollDelta) { -+ const state = ctx.state; -+ if (state.edgeReachedGate !== "prepared") { -+ return void 0; -+ } -+ const allowedEdge = scrollDelta < 0 ? "start" : "end"; -+ state.edgeReachedGate = "closed"; -+ resetEdgeLatch(ctx, allowedEdge); -+ return allowedEdge; -+} -+ -+// src/utils/hasActiveInitialScroll.ts -+function hasActiveInitialScroll(state) { -+ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; -+} -+ -+// src/utils/checkAtBottom.ts -+function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -+ var _a3; + } +-function findContainerId(ctx, key) { ++function setAdaptiveRender(ctx, mode, reason) { + var _a3, _b; +- const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); +- if (directMatch !== void 0) { +- return directMatch; +- } +- const numContainers = peek$(ctx, "numContainers"); +- for (let i = 0; i < numContainers; i++) { +- const itemKey = peek$(ctx, `containerItemKey${i}`); +- if (itemKey === key) { +- return i; +- } ++ const previousMode = peek$(ctx, "adaptiveRender"); ++ if (previousMode !== mode) { ++ set$(ctx, "adaptiveRender", mode); ++ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); + } +- return -1; + } +- +-// src/utils/getItemSize.ts +-function getKnownOrFixedSize(ctx, key, index, data, resolved) { ++function resetAdaptiveRender(ctx) { + var _a3, _b; +- const state = ctx.state; +- const { getFixedItemSize, getItemType } = state.props; +- let size = key ? state.sizesKnown.get(key) : void 0; +- if (size === void 0 && key && getFixedItemSize) { +- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; +- const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); +- if (fixedSize !== void 0) { +- size = fixedSize + ctx.scrollAxisGap; +- state.sizesKnown.set(key, size); +- } ++ clearAdaptiveRenderExitTimeout(ctx); ++ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; ++ if (peek$(ctx, "adaptiveRender") !== mode) { ++ setAdaptiveRender(ctx, mode, "initial"); + } +- return size; +-} +-function getKnownOrFixedItemSize(ctx, index) { +- const key = getId(ctx.state, index); +- return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); + } +-function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { +- for (let index = startIndex; index <= endIndex; index++) { +- if (getKnownOrFixedItemSize(ctx, index) === void 0) { +- return false; ++function updateAdaptiveRender(ctx, scrollVelocity, options) { ++ var _a3, _b, _c; + const state = ctx.state; -+ if (!state) { -+ return; -+ } -+ const { -+ queuedInitialLayout, -+ scrollLength, -+ scroll, -+ maintainingScrollAtEnd, -+ props: { maintainScrollAtEndThreshold, onEndReachedThreshold } -+ } = state; -+ const contentSize = getContentSize(ctx); -+ resetSharedEdgeGateIfOutsideHysteresis(ctx); -+ if (contentSize > 0 && queuedInitialLayout) { -+ const insetEnd = getContentInsetEnd(ctx); -+ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; -+ const isContentLess = contentSize < scrollLength; -+ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); -+ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); -+ set$( -+ ctx, -+ "isWithinMaintainScrollAtEndThreshold", -+ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength -+ ); -+ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; -+ if (!shouldSkipThresholdChecks) { -+ state.isEndReached = checkThreshold( -+ distanceFromEnd, -+ isContentLess, -+ onEndReachedThreshold * scrollLength, -+ state.isEndReached, -+ state.endReachedSnapshot, -+ { -+ contentSize, -+ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, -+ scrollPosition: scroll -+ }, -+ (distance) => { -+ var _a4, _b; -+ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { -+ markReachedEdge(ctx); -+ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); -+ } -+ }, -+ (snapshot) => { -+ state.endReachedSnapshot = snapshot; ++ const adaptiveRender = state.props.adaptiveRender; ++ const currentMode = peek$(ctx, "adaptiveRender"); ++ if (peek$(ctx, "readyToRender")) { ++ if (adaptiveRender) { ++ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; ++ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; ++ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; ++ const threshold = currentMode === "light" ? exitVelocity : enterVelocity; ++ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; ++ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; ++ if (nextMode !== previousMode) { ++ if (nextMode === "light") { ++ setAdaptiveRender(ctx, "light", "scroll"); ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } else if (currentMode === "light") { ++ scheduleAdaptiveRenderExit(ctx, exitDelay); + } -+ ); -+ } -+ } ++ } ++ } else { ++ resetAdaptiveRender(ctx); + } + } +- return true; } - +-function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { +- var _a3, _b, _c, _d; ++ ++// src/core/doMaintainScrollAtEnd.ts ++function doMaintainScrollAtEnd(ctx) { + const state = ctx.state; + const { +- sizes, +- averageSizes, +- props: { estimatedItemSize, getItemType }, +- scrollingTo ++ didContainersLayout, ++ pendingNativeMVCPAdjust, ++ refScroller, ++ props: { maintainScrollAtEnd } + } = state; +- const sizeKnown = state.sizesKnown.get(key); +- if (sizeKnown !== void 0) { +- return sizeKnown; ++ const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); ++ const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout); ++ if (pendingNativeMVCPAdjust) { ++ state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd; ++ return false; + } +- let size; +- const renderedSize = sizes.get(key); +- if (preferCachedSize) { +- if (renderedSize !== void 0) { +- return renderedSize; +- } +- } +- size = getKnownOrFixedSize(ctx, key, index, data, resolved); +- if (size !== void 0) { +- setSize(ctx, key, size, notifyTotalSize); +- return size; +- } +- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; +- if (useAverageSize && !scrollingTo) { +- const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; +- if (averageSizeForType !== void 0) { +- size = roundSize(averageSizeForType); +- } +- } +- if (size === void 0 && renderedSize !== void 0) { +- return renderedSize; +- } +- if (size === void 0 && useAverageSize && scrollingTo) { +- const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; +- if (averageSizeForType !== void 0) { +- size = roundSize(averageSizeForType); +- } +- } +- if (size === void 0) { +- size = estimatedItemSize + ctx.scrollAxisGap; +- } +- setSize(ctx, key, size, notifyTotalSize); +- return size; +-} +-function getItemSizeAtIndex(ctx, index) { +- if (index === void 0 || index < 0) { +- return void 0; +- } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); +-} +- -// src/core/calculateOffsetWithOffsetPosition.ts -function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { - var _a3; @@ -3954,32 +4244,13 @@ index 914d2da..ce6a0d7 100644 - let offset = offsetParam; - if (viewOffset) { - offset -= viewOffset; -+// src/utils/checkAtTop.ts -+function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -+ const state = ctx == null ? void 0 : ctx.state; -+ if (!state) { -+ return; - } +- } - if (index !== void 0) { - const startOffsetAdjustment = getStartOffsetAdjustment(ctx); - if (startOffsetAdjustment) { - offset += startOffsetAdjustment; - } -+ const { -+ isStartReached, -+ props: { data, onStartReachedThreshold }, -+ scroll, -+ scrollLength, -+ startReachedSnapshot, -+ totalSize -+ } = state; -+ const dataLength = data.length; -+ const threshold = onStartReachedThreshold * scrollLength; -+ resetSharedEdgeGateIfOutsideHysteresis(ctx); -+ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { -+ state.isStartReached = false; -+ state.startReachedSnapshot = void 0; - } +- } - if (viewPosition !== void 0 && index !== void 0) { - const dataLength = state.props.data.length; - if (dataLength === 0) { @@ -3995,49 +4266,13 @@ index 914d2da..ce6a0d7 100644 - const footerSize = peek$(ctx, "footerSize") || 0; - offset += footerSize; - } -+ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); -+ set$(ctx, "isNearStart", scroll <= threshold); -+ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; -+ if (!shouldSkipThresholdChecks) { -+ state.isStartReached = checkThreshold( -+ scroll, -+ false, -+ threshold, -+ state.isStartReached, -+ startReachedSnapshot, -+ { -+ contentSize: totalSize, -+ dataLength, -+ scrollPosition: scroll -+ }, -+ (distance) => { -+ var _a3, _b; -+ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { -+ markReachedEdge(ctx); -+ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); -+ } -+ }, -+ (snapshot) => { -+ state.startReachedSnapshot = snapshot; -+ } -+ ); - } +- } - return offset; - } - +-} +- -// src/core/clampScrollOffset.ts -function clampScrollOffset(ctx, offset, scrollTarget) { -+// src/utils/checkThresholds.ts -+function checkThresholds(ctx, allowedEdge) { -+ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; -+ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+} -+ -+// src/core/recalculateSettledScroll.ts -+function recalculateSettledScroll(ctx) { -+ var _a3, _b; - const state = ctx.state; +- const state = ctx.state; - const contentSize = getContentSize(ctx); - let clampedOffset = offset; - if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { @@ -4046,19 +4281,136 @@ index 914d2da..ce6a0d7 100644 - const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; - const maxOffset = baseMaxOffset + extraEndOffset; - clampedOffset = Math.min(offset, maxOffset); -+ if ((_a3 = state.props) == null ? void 0 : _a3.data) { -+ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); - } +- } - clampedOffset = Math.max(0, clampedOffset); - return clampedOffset; -+ checkThresholds(ctx); - } - - // src/core/finishScrollTo.ts -@@ -1413,16 +1029,191 @@ function listenForScrollEnd(ctx, params) { - if (idleTimeout !== void 0) { - clearTimeout(idleTimeout); - } +-} +- +-// src/core/finishScrollTo.ts +-function finishScrollTo(ctx) { +- var _a3, _b; +- const state = ctx.state; +- if (state == null ? void 0 : state.scrollingTo) { +- cancelScrollCompletionChecks(state); +- const resolvePendingScroll = state.pendingScrollResolve; +- state.pendingScrollResolve = void 0; +- const scrollingTo = state.scrollingTo; +- state.scrollHistory.length = 0; +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- if (state.pendingTotalSize !== void 0) { +- addTotalSize(ctx, null, state.pendingTotalSize); +- } +- { +- state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); +- } +- if (scrollingTo.isInitialScroll || state.initialScroll) { +- const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; +- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; +- finishInitialScroll(ctx, { +- onFinished: () => { +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +- }, +- preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, +- recalculateItems: true, +- schedulePreservedTargetClear: shouldPreserveResizeTarget, +- syncObservedOffset: isOffsetSession, +- waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame +- }); +- return; +- } +- recalculateSettledScroll(ctx); +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +- } +-} +- +-// src/core/doScrollTo.ts +-var SCROLL_END_IDLE_MS = 80; +-var SCROLL_END_MAX_MS = 1500; +-var SMOOTH_SCROLL_DURATION_MS = 320; +-var SCROLL_END_TARGET_EPSILON = 1; +-function doScrollTo(ctx, params) { +- var _a3, _b; +- const state = ctx.state; +- const { animated, horizontal, offset } = params; +- state.scheduledWork.cancel("platformScrollCompletion"); +- const scroller = state.refScroller.current; +- const node = scroller == null ? void 0 : scroller.getScrollableNode(); +- if (!scroller || !node) { +- return; +- } +- const isAnimated = !!animated; +- const isHorizontal = !!horizontal; +- const contentSize = isHorizontal ? getContentSize(ctx) : void 0; +- const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; +- const top = isHorizontal ? 0 : offset; +- scroller.scrollTo({ animated: isAnimated, x: left, y: top }); +- if (isAnimated) { +- const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; +- listenForScrollEnd(ctx, { +- readOffset: () => scroller.getCurrentScrollOffset(), +- target, +- targetOffset: offset +- }); +- } else { +- state.scroll = offset; +- const targetToken = state.scrollingTo; +- state.scheduledWork.timeout( +- () => { +- if (targetToken === state.scrollingTo) { +- finishScrollTo(ctx); +- } +- }, +- 100, +- "platformScrollCompletion" +- ); +- } +-} +-function listenForScrollEnd(ctx, params) { +- const { readOffset, target, targetOffset } = params; +- if (!target) { +- finishScrollTo(ctx); +- return; +- } +- const supportsScrollEnd = "onscrollend" in target; +- let idleTimeout; +- let settled = false; +- const { scheduledWork, scrollingTo: targetToken } = ctx.state; +- const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); +- const cleanup = () => { +- target.removeEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.removeEventListener("scrollend", onScrollEnd); +- } +- if (idleTimeout !== void 0) { +- clearTimeout(idleTimeout); +- } +- clearTimeout(maxTimeout); +- }; +- const cancel = () => { +- if (!settled) { +- settled = true; +- cleanup(); +- } +- }; +- const finish = (reason) => { +- if (settled) return; +- if (targetToken !== ctx.state.scrollingTo) { +- scheduledWork.cancel("platformScrollCompletion"); +- return; +- } +- const currentOffset = readOffset(); +- const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; +- if (reason === "scrollend" && !isNearTarget) { +- return; +- } +- scheduledWork.cancel("platformScrollCompletion"); +- finishScrollTo(ctx); +- }; +- const onScroll2 = () => { +- if (idleTimeout !== void 0) { +- clearTimeout(idleTimeout); +- } - idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); - }; - const onScrollEnd = () => finish("scrollend"); @@ -4067,196 +4419,38 @@ index 914d2da..ce6a0d7 100644 - target.addEventListener("scrollend", onScrollEnd); - } else { - idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); -+ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); -+ }; -+ const onScrollEnd = () => finish("scrollend"); -+ target.addEventListener("scroll", onScroll2); -+ if (supportsScrollEnd) { -+ target.addEventListener("scrollend", onScrollEnd); -+ } else { -+ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); -+ } -+ scheduledWork.register("platformScrollCompletion", cancel); -+} -+ -+// src/core/initialScrollSession.ts -+var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -+function hasInitialScrollSessionCompletion(completion) { -+ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); -+} -+function clearInitialScrollSession(state) { -+ state.initialScrollSession = void 0; -+ return void 0; -+} -+function createInitialScrollSession(options) { -+ const { bootstrap, completion, kind, previousDataLength } = options; -+ return kind === "offset" ? { -+ completion, -+ kind, -+ previousDataLength -+ } : { -+ bootstrap, -+ completion, -+ kind, -+ previousDataLength -+ }; -+} -+function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { -+ var _a4, _b2; -+ if (!state.initialScrollSession) { -+ state.initialScrollSession = createInitialScrollSession({ -+ completion: {}, -+ kind, -+ previousDataLength: 0 -+ }); -+ } else if (state.initialScrollSession.kind !== kind) { -+ state.initialScrollSession = createInitialScrollSession({ -+ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, -+ completion: state.initialScrollSession.completion, -+ kind, -+ previousDataLength: state.initialScrollSession.previousDataLength -+ }); -+ } -+ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; -+ return state.initialScrollSession.completion; -+} -+var initialScrollCompletion = { -+ didDispatchNativeScroll(state) { -+ var _a3, _b; -+ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); -+ }, -+ didRetrySilentInitialScroll(state) { -+ var _a3, _b; -+ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); -+ }, -+ markInitialScrollNativeDispatch(state) { -+ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; -+ }, -+ markSilentInitialScrollRetry(state) { -+ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; -+ }, -+ resetFlags(state) { -+ if (!state.initialScrollSession) { -+ return; -+ } -+ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); -+ completion.didDispatchNativeScroll = void 0; -+ completion.didRetrySilentInitialScroll = void 0; -+ } -+}; -+var initialScrollWatchdog = { -+ clear(state) { -+ initialScrollWatchdog.set(state, void 0); -+ }, -+ didReachTarget(newScroll, watchdog) { -+ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); -+ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ get(state) { -+ var _a3, _b; -+ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; -+ }, -+ hasNonZeroTargetOffset(targetOffset) { -+ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ isAtZeroTargetOffset(targetOffset) { -+ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ set(state, watchdog) { -+ var _a3, _b; -+ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { -+ return; -+ } -+ const completion = ensureInitialScrollSessionCompletion(state); -+ completion.watchdog = watchdog ? { -+ startScroll: watchdog.startScroll, -+ targetOffset: watchdog.targetOffset -+ } : void 0; -+ } -+}; -+function setInitialScrollSession(state, options = {}) { -+ var _a3, _b, _c, _d; -+ const existingSession = state.initialScrollSession; -+ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; -+ const completion = existingSession == null ? void 0 : existingSession.completion; -+ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; -+ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; -+ if (!kind) { -+ return clearInitialScrollSession(state); -+ } -+ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { -+ return clearInitialScrollSession(state); -+ } -+ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; -+ state.initialScrollSession = createInitialScrollSession({ -+ bootstrap, -+ completion, -+ kind, -+ previousDataLength -+ }); -+ return state.initialScrollSession; -+} -+var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; -+var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; -+var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; -+function clearAdaptiveRenderExitTimeout(ctx) { -+ ctx.state.scheduledWork.cancel("adaptiveRender"); -+} -+function scheduleAdaptiveRenderExit(ctx, exitDelay) { -+ const state = ctx.state; -+ clearAdaptiveRenderExitTimeout(ctx); -+ if (exitDelay <= 0) { -+ setAdaptiveRender(ctx, "normal", "scroll"); -+ } else { -+ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); -+ } -+} -+function setAdaptiveRender(ctx, mode, reason) { -+ var _a3, _b; -+ const previousMode = peek$(ctx, "adaptiveRender"); -+ if (previousMode !== mode) { -+ set$(ctx, "adaptiveRender", mode); -+ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); -+ } -+} -+function resetAdaptiveRender(ctx) { -+ var _a3, _b; -+ clearAdaptiveRenderExitTimeout(ctx); -+ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; -+ if (peek$(ctx, "adaptiveRender") !== mode) { -+ setAdaptiveRender(ctx, mode, "initial"); -+ } -+} -+function updateAdaptiveRender(ctx, scrollVelocity, options) { -+ var _a3, _b, _c; -+ const state = ctx.state; -+ const adaptiveRender = state.props.adaptiveRender; -+ const currentMode = peek$(ctx, "adaptiveRender"); -+ if (peek$(ctx, "readyToRender")) { -+ if (adaptiveRender) { -+ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; -+ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; -+ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; -+ const threshold = currentMode === "light" ? exitVelocity : enterVelocity; -+ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; -+ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; -+ if (nextMode !== previousMode) { -+ if (nextMode === "light") { -+ setAdaptiveRender(ctx, "light", "scroll"); -+ scheduleAdaptiveRenderExit(ctx, exitDelay); -+ } else if (currentMode === "light") { -+ scheduleAdaptiveRenderExit(ctx, exitDelay); -+ } -+ } -+ } else { -+ resetAdaptiveRender(ctx); -+ } - } +- } - scheduledWork.register("platformScrollCompletion", cancel); - } - - // src/core/doMaintainScrollAtEnd.ts -@@ -1451,6 +1242,7 @@ function doMaintainScrollAtEnd(ctx) { +-} +- +-// src/core/doMaintainScrollAtEnd.ts +-function doMaintainScrollAtEnd(ctx) { +- const state = ctx.state; +- const { +- didContainersLayout, +- pendingNativeMVCPAdjust, +- refScroller, +- props: { maintainScrollAtEnd } +- } = state; +- const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); +- const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout); +- if (pendingNativeMVCPAdjust) { +- state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd; +- return false; +- } +- if (shouldMaintainScrollAtEnd) { +- state.pendingMaintainScrollAtEnd = false; +- const contentSize = getContentSize(ctx); +- if (contentSize < state.scrollLength) { +- state.scroll = 0; ++ if (shouldMaintainScrollAtEnd) { ++ state.pendingMaintainScrollAtEnd = false; ++ const contentSize = getContentSize(ctx); ++ if (contentSize < state.scrollLength) { ++ state.scroll = 0; + } + if (!state.maintainingScrollAtEnd) { + const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; state.maintainingScrollAtEnd = pendingState; @@ -4264,10 +4458,27 @@ index 914d2da..ce6a0d7 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1809,6 +1601,27 @@ function prepareMVCP(ctx, dataChanged) { +@@ -1809,23 +1601,44 @@ function prepareMVCP(ctx, dataChanged) { } } +-// src/utils/getScrollVelocity.ts +-var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; +-var SCROLL_VELOCITY_HALF_LIFE_MS = 200; +-var getScrollVelocity = (state) => { +- const { scrollHistory } = state; +- const newestIndex = scrollHistory.length - 1; +- if (newestIndex < 1) { +- return 0; +- } +- const newest = scrollHistory[newestIndex]; +- if (Date.now() - newest.time > MAX_SCROLL_VELOCITY_WINDOW_MS) { +- return 0; +- } +- let direction = 0; +- let weightedVelocity = 0; +- let totalWeight = 0; +- for (let i = newestIndex; i > 0; i--) { +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { @@ -4289,74 +4500,274 @@ index 914d2da..ce6a0d7 100644 + }, "fullDrawDistancePrewarm"); +} + - // src/utils/getScrollVelocity.ts - var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; - var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -1906,6 +1719,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { - state.scrollPrevTime = state.scrollTime; - state.scroll = newScroll; - state.scrollTime = currentTime; -+ if ((options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust) { -+ releaseScrollTargetSettleIfMoved(ctx); -+ } - const scrollDelta = Math.abs(newScroll - prevScroll); - const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; - const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; -@@ -2026,99 +1842,417 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { - if (start === void 0) { - return void 0; - } -- if (targetIndex !== void 0 && state.positions[start] === void 0) { -- return { end: start, start }; -+ if (targetIndex !== void 0 && state.positions[start] === void 0) { -+ return { end: start, start }; ++// src/utils/getScrollVelocity.ts ++var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; ++var SCROLL_VELOCITY_HALF_LIFE_MS = 200; ++var getScrollVelocity = (state) => { ++ const { scrollHistory } = state; ++ const newestIndex = scrollHistory.length - 1; ++ if (newestIndex < 1) { ++ return 0; + } -+ if (targetIndex === void 0) { -+ const startBottom = getItemBottom(ctx, start); -+ if (startBottom === void 0 || startBottom <= viewportStart) { -+ return void 0; -+ } ++ const newest = scrollHistory[newestIndex]; ++ if (Date.now() - newest.time > MAX_SCROLL_VELOCITY_WINDOW_MS) { ++ return 0; + } -+ while (start > 0) { -+ const top = state.positions[start]; -+ if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { -+ break; -+ } -+ start--; ++ let direction = 0; ++ let weightedVelocity = 0; ++ let totalWeight = 0; ++ for (let i = newestIndex; i > 0; i--) { + const current = scrollHistory[i]; + const previous = scrollHistory[i - 1]; + const scrollDiff = current.scroll - previous.scroll; +@@ -1844,281 +1657,604 @@ var getScrollVelocity = (state) => { + if (scrollDiff === 0 || timeDiff <= 0) { + continue; + } +- const age = newest.time - current.time; +- const weight = Math.exp(-age / SCROLL_VELOCITY_HALF_LIFE_MS); +- weightedVelocity += scrollDiff / timeDiff * weight; +- totalWeight += weight; +- } +- return totalWeight > 0 ? weightedVelocity / totalWeight : 0; +-}; +- +-// src/utils/hasActiveMVCPAnchorLock.ts +-function hasActiveMVCPAnchorLock(state) { +- const lock = state.mvcpAnchorLock; +- if (!lock) { +- return false; ++ const age = newest.time - current.time; ++ const weight = Math.exp(-age / SCROLL_VELOCITY_HALF_LIFE_MS); ++ weightedVelocity += scrollDiff / timeDiff * weight; ++ totalWeight += weight; + } -+ while (start > 0) { -+ const previousBottom = getItemBottom(ctx, start - 1); -+ if (previousBottom === void 0 || previousBottom <= viewportStart) { -+ break; -+ } -+ start--; ++ return totalWeight > 0 ? weightedVelocity / totalWeight : 0; ++}; ++ ++// src/utils/hasActiveMVCPAnchorLock.ts ++function hasActiveMVCPAnchorLock(state) { ++ const lock = state.mvcpAnchorLock; ++ if (!lock) { ++ return false; + } -+ let end = start; -+ while (end + 1 < dataLength) { -+ const nextTop = state.positions[end + 1]; -+ if (nextTop === void 0 || nextTop > viewportEnd) { -+ break; -+ } -+ end++; ++ if (Date.now() > lock.expiresAt) { ++ state.mvcpAnchorLock = void 0; ++ return false; + } -+ return { end, start }; ++ return true; +} -+function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -+ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -+ if (range) { -+ ctx.state.scrollTargetPinnedRange = range; -+ ctx.state.scrollForNextCalculateItemsInView = void 0; -+ } else { -+ ctx.state.scrollTargetPinnedRange = void 0; -+ } ++ ++// src/utils/isInMVCPActiveMode.ts ++function isInMVCPActiveMode(state) { ++ return state.dataChangeNeedsScrollUpdate || hasActiveMVCPAnchorLock(state); +} -+function scrollTo(ctx, params) { -+ var _a3, _b, _c; ++ ++// src/core/updateScroll.ts ++function updateScroll(ctx, newScroll, forceUpdate, options) { ++ var _a3; + const state = ctx.state; -+ const { noScrollingTo, forceScroll, ...scrollTarget } = params; -+ const { -+ animated, -+ isInitialScroll, ++ const { ignoreScrollFromMVCP, lastScrollAdjustForHistory, scrollAdjustHandler, scrollHistory, scrollingTo } = state; ++ const prevScroll = state.scroll; ++ if ((options == null ? void 0 : options.markHasScrolled) !== false) { ++ state.hasScrolled = true; ++ } ++ const currentTime = Date.now(); ++ state.lastBatchingAction = currentTime; ++ const adjust = scrollAdjustHandler.getAdjust(); ++ const adjustChanged = lastScrollAdjustForHistory !== void 0 && Math.abs(adjust - lastScrollAdjustForHistory) > 0.1; ++ if (adjustChanged) { ++ scrollHistory.length = 0; ++ } ++ state.lastScrollAdjustForHistory = adjust; ++ if (scrollingTo === void 0 && !(scrollHistory.length === 0 && newScroll === state.scroll)) { ++ if (!adjustChanged) { ++ scrollHistory.push({ scroll: newScroll, time: currentTime }); ++ } ++ } ++ if (scrollHistory.length > 5) { ++ scrollHistory.shift(); ++ } ++ if (ignoreScrollFromMVCP && !scrollingTo) { ++ const { lt, gt } = ignoreScrollFromMVCP; ++ if (lt && newScroll < lt || gt && newScroll > gt) { ++ state.ignoreScrollFromMVCPIgnored = true; ++ return; ++ } ++ } ++ state.scrollPrev = prevScroll; ++ state.scrollPrevTime = state.scrollTime; ++ state.scroll = newScroll; ++ state.scrollTime = currentTime; ++ const scrollDelta = Math.abs(newScroll - prevScroll); ++ const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; ++ const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; ++ const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); ++ const scrollLength = state.scrollLength; ++ const isLargeUserScrollJump = scrollLength > 0 && scrollingTo === void 0 && scrollDelta > scrollLength && !state.pendingNativeMVCPAdjust; ++ const scrollVelocity = getScrollVelocity(state); ++ updateAdaptiveRender(ctx, scrollVelocity, { forceLight: isLargeUserScrollJump }); ++ const lastCalculated = state.scrollLastCalculate; ++ const useAggressiveItemRecalculation = isInMVCPActiveMode(state); ++ const shouldUpdate = useAggressiveItemRecalculation || didResolvePendingNativeMVCPAdjust || allowedEdge !== void 0 || forceUpdate || lastCalculated === void 0 || Math.abs(state.scroll - lastCalculated) > 2; ++ if (shouldUpdate) { ++ state.scrollLastCalculate = state.scroll; ++ state.ignoreScrollFromMVCPIgnored = false; ++ state.lastScrollDelta = scrollDelta; ++ const runCalculateItems = () => { ++ var _a4; ++ const calculateItemsParams = { ++ doMVCP: scrollingTo !== void 0, ++ scrollVelocity ++ }; ++ if (isLargeUserScrollJump) { ++ calculateItemsParams.drawDistanceMode = "visible-first"; ++ } ++ (_a4 = state.triggerCalculateItemsInView) == null ? void 0 : _a4.call(state, calculateItemsParams); ++ checkThresholds(ctx, allowedEdge); ++ }; ++ if (isLargeUserScrollJump) { ++ state.mvcpAnchorLock = void 0; ++ state.pendingNativeMVCPAdjust = void 0; ++ state.userScrollAnchorReset = { keys: /* @__PURE__ */ new Set() }; ++ state.scheduledWork.cancel("mvcpRecalculate"); ++ ReactDOM.flushSync(runCalculateItems); ++ scheduleFullDrawDistancePrewarm(ctx); ++ } else { ++ runCalculateItems(); ++ } ++ const shouldMaintainScrollAtEndAfterPendingSettle = !!state.pendingMaintainScrollAtEnd || !!((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onDataChange); ++ if (didResolvePendingNativeMVCPAdjust && shouldMaintainScrollAtEndAfterPendingSettle) { ++ state.pendingMaintainScrollAtEnd = false; ++ doMaintainScrollAtEnd(ctx); ++ } ++ state.dataChangeNeedsScrollUpdate = false; ++ state.lastScrollDelta = 0; ++ } ++} ++ ++// src/core/scrollTo.ts ++function getAverageSizeSnapshot(state) { ++ if (Object.keys(state.averageSizes).length === 0) { ++ return void 0; ++ } ++ const snapshot = {}; ++ for (const itemType in state.averageSizes) { ++ const averages = state.averageSizes[itemType]; ++ snapshot[itemType] = averages.avg; ++ } ++ return snapshot; ++} ++function syncInitialScrollNativeWatchdog(state, options) { ++ var _a3; ++ const { isInitialScroll, requestedOffset, targetOffset } = options; ++ const existingWatchdog = initialScrollWatchdog.get(state); ++ const shouldWatchInitialNativeScroll = !state.didFinishInitialScroll && (isInitialScroll || !!existingWatchdog) && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); ++ const shouldClearInitialNativeScrollWatchdog = !state.didFinishInitialScroll && !!existingWatchdog && initialScrollWatchdog.isAtZeroTargetOffset(requestedOffset); ++ if (shouldWatchInitialNativeScroll) { ++ state.hasScrolled = false; ++ initialScrollWatchdog.set(state, { ++ startScroll: (_a3 = existingWatchdog == null ? void 0 : existingWatchdog.startScroll) != null ? _a3 : state.scroll, ++ targetOffset ++ }); ++ return; ++ } ++ if (shouldClearInitialNativeScrollWatchdog) { ++ initialScrollWatchdog.clear(state); ++ } ++} ++function findPositionIndexAtOrBeforeOffset(ctx, offset) { ++ const state = ctx.state; ++ const dataLength = state.props.data.length; ++ let low = 0; ++ let high = dataLength - 1; ++ let match; ++ while (low <= high) { ++ const mid = Math.floor((low + high) / 2); ++ const top = state.positions[mid]; ++ if (top === void 0) { ++ high = mid - 1; ++ } else { ++ if (top <= offset) { ++ match = mid; ++ low = mid + 1; ++ } else { ++ high = mid - 1; ++ } ++ } ++ } ++ return match; ++} ++function getItemBottom(ctx, index) { ++ var _a3; ++ const top = ctx.state.positions[index]; ++ if (top === void 0) { ++ return void 0; ++ } ++ const itemSize = (_a3 = getItemSizeAtIndex(ctx, index)) != null ? _a3 : 0; ++ return top + (Number.isFinite(itemSize) ? itemSize : 0); ++} ++function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { ++ const state = ctx.state; ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return void 0; ++ } ++ const viewportStart = Math.max(0, targetOffset); ++ const viewportEnd = Math.max(viewportStart, targetOffset + state.scrollLength); ++ let start = targetIndex !== void 0 ? Math.max(0, Math.min(dataLength - 1, targetIndex)) : findPositionIndexAtOrBeforeOffset(ctx, viewportStart); ++ if (start === void 0) { ++ return void 0; ++ } ++ if (targetIndex !== void 0 && state.positions[start] === void 0) { ++ return { end: start, start }; ++ } ++ if (targetIndex === void 0) { ++ const startBottom = getItemBottom(ctx, start); ++ if (startBottom === void 0 || startBottom <= viewportStart) { ++ return void 0; ++ } ++ } ++ while (start > 0) { ++ const top = state.positions[start]; ++ if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { ++ break; ++ } ++ start--; ++ } ++ while (start > 0) { ++ const previousBottom = getItemBottom(ctx, start - 1); ++ if (previousBottom === void 0 || previousBottom <= viewportStart) { ++ break; ++ } ++ start--; ++ } ++ let end = start; ++ while (end + 1 < dataLength) { ++ const nextTop = state.positions[end + 1]; ++ if (nextTop === void 0 || nextTop > viewportEnd) { ++ break; ++ } ++ end++; ++ } ++ return { end, start }; ++} ++function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { ++ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); ++ if (range) { ++ ctx.state.scrollTargetPinnedRange = range; ++ ctx.state.scrollForNextCalculateItemsInView = void 0; ++ } else { ++ ctx.state.scrollTargetPinnedRange = void 0; ++ } ++} ++function scrollTo(ctx, params) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ const { noScrollingTo, forceScroll, ...scrollTarget } = params; ++ const { ++ animated, ++ isInitialScroll, + offset: scrollTargetOffset, + precomputedWithViewOffset, + waitForInitialScrollCompletionFrame @@ -4404,23 +4815,36 @@ index 914d2da..ce6a0d7 100644 + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); + } -+ } + } +- if (Date.now() > lock.expiresAt) { +- state.mvcpAnchorLock = void 0; +- return false; + if (forceScroll || !isInitialScroll || Platform.OS === "android") { + doScrollTo(ctx, { animated, horizontal, offset }); + } else { + state.scroll = offset; -+ } -+} -+ + } +- return true; + } + +-// src/utils/isInMVCPActiveMode.ts +-function isInMVCPActiveMode(state) { +- return state.dataChangeNeedsScrollUpdate || hasActiveMVCPAnchorLock(state); +// src/core/scrollTargetSettle.ts +var SETTLE_POSITION_EPSILON = 0.5; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; +var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; ++function releaseScrollTargetForUserInteraction(ctx) { ++ if (ctx.state.scrollTargetSettle) { ++ clearScrollTargetSettle(ctx.state); ++ } ++} +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; + state.scheduledWork.cancel("scrollTargetSettle"); ++ state.scheduledWork.cancel("scrollTargetSettleDeadline"); +} +function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; @@ -4437,35 +4861,56 @@ index 914d2da..ce6a0d7 100644 + deadline: now + SETTLE_MAX_MS, + expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), ++ measuredIndex: void 0, + quietPasses: 0, -+ requestedAt: now, + viewOffset, + viewPosition + }; -+} ++ state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline"); + } +- +-// src/core/updateScroll.ts +-function updateScroll(ctx, newScroll, forceUpdate, options) { +function getSettleTargetOffset(ctx, settle, index, position) { + const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; + return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} +function applyScrollTargetCorrection(ctx, id) { -+ var _a3; -+ const state = ctx.state; + var _a3; + const state = ctx.state; +- const { ignoreScrollFromMVCP, lastScrollAdjustForHistory, scrollAdjustHandler, scrollHistory, scrollingTo } = state; +- const prevScroll = state.scroll; +- if ((options == null ? void 0 : options.markHasScrolled) !== false) { +- state.hasScrolled = true; + const settle = state.scrollTargetSettle; + if (!settle || settle.id !== id) { + return; -+ } + } +- const currentTime = Date.now(); +- state.lastBatchingAction = currentTime; +- const adjust = scrollAdjustHandler.getAdjust(); +- const adjustChanged = lastScrollAdjustForHistory !== void 0 && Math.abs(adjust - lastScrollAdjustForHistory) > 0.1; +- if (adjustChanged) { +- scrollHistory.length = 0; + const index = state.indexByKey.get(id); + const position = index === void 0 ? void 0 : state.positions[index]; + if (index === void 0 || position === void 0 || Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return; -+ } + } +- state.lastScrollAdjustForHistory = adjust; +- if (scrollingTo === void 0 && !(scrollHistory.length === 0 && newScroll === state.scroll)) { +- if (!adjustChanged) { +- scrollHistory.push({ scroll: newScroll, time: currentTime }); +- } + if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { + clearScrollTargetSettle(state); + return; -+ } + } +- if (scrollHistory.length > 5) { +- scrollHistory.shift(); + settle.corrections++; -+ settle.requestedAt = Date.now(); ++ settle.measuredIndex = void 0; + scrollTo(ctx, { + animated: false, + index, @@ -4480,21 +4925,71 @@ index 914d2da..ce6a0d7 100644 + }); + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +} -+function settleScrollTarget(ctx, isCompensating) { ++function settleScrollTarget(ctx, options) { ++ var _a3; + const state = ctx.state; + const settle = state.scrollTargetSettle; + if (!settle) { + return false; + } +- if (ignoreScrollFromMVCP && !scrollingTo) { +- const { lt, gt } = ignoreScrollFromMVCP; +- if (lt && newScroll < lt || gt && newScroll > gt) { +- state.ignoreScrollFromMVCPIgnored = true; +- return; ++ const measured = options == null ? void 0 : options.minIndexSizeChanged; ++ if (measured !== void 0) { ++ settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); + } -+ if (isCompensating) { ++ if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return false; -+ } + } + settle.quietPasses = 0; + settle.expiresAt = Date.now() + SETTLE_TTL_MS; + return false; -+ } + } +- state.scrollPrev = prevScroll; +- state.scrollPrevTime = state.scrollTime; +- state.scroll = newScroll; +- state.scrollTime = currentTime; +- const scrollDelta = Math.abs(newScroll - prevScroll); +- const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; +- const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; +- const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); +- const scrollLength = state.scrollLength; +- const isLargeUserScrollJump = scrollLength > 0 && scrollingTo === void 0 && scrollDelta > scrollLength && !state.pendingNativeMVCPAdjust; +- const scrollVelocity = getScrollVelocity(state); +- updateAdaptiveRender(ctx, scrollVelocity, { forceLight: isLargeUserScrollJump }); +- const lastCalculated = state.scrollLastCalculate; +- const useAggressiveItemRecalculation = isInMVCPActiveMode(state); +- const shouldUpdate = useAggressiveItemRecalculation || didResolvePendingNativeMVCPAdjust || allowedEdge !== void 0 || forceUpdate || lastCalculated === void 0 || Math.abs(state.scroll - lastCalculated) > 2; +- if (shouldUpdate) { +- state.scrollLastCalculate = state.scroll; +- state.ignoreScrollFromMVCPIgnored = false; +- state.lastScrollDelta = scrollDelta; +- const runCalculateItems = () => { +- var _a4; +- const calculateItemsParams = { +- doMVCP: scrollingTo !== void 0, +- scrollVelocity +- }; +- if (isLargeUserScrollJump) { +- calculateItemsParams.drawDistanceMode = "visible-first"; +- } +- (_a4 = state.triggerCalculateItemsInView) == null ? void 0 : _a4.call(state, calculateItemsParams); +- checkThresholds(ctx, allowedEdge); +- }; +- if (isLargeUserScrollJump) { +- state.mvcpAnchorLock = void 0; +- state.pendingNativeMVCPAdjust = void 0; +- state.userScrollAnchorReset = { keys: /* @__PURE__ */ new Set() }; +- state.scheduledWork.cancel("mvcpRecalculate"); +- ReactDOM.flushSync(runCalculateItems); +- scheduleFullDrawDistancePrewarm(ctx); +- } else { +- runCalculateItems(); + const index = state.indexByKey.get(settle.id); + const position = index === void 0 ? void 0 : state.positions[index]; + if (index === void 0 || position === void 0) { @@ -4506,13 +5001,21 @@ index 914d2da..ce6a0d7 100644 + clearScrollTargetSettle(state); + return false; + } ++ if (settle.measuredIndex === void 0 || settle.measuredIndex > index) { ++ return false; ++ } + const targetOffset = getSettleTargetOffset(ctx, settle, index, position); + const diff = targetOffset - state.scroll; + if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ settle.measuredIndex = void 0; + settle.quietPasses++; + if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { + clearScrollTargetSettle(state); -+ } + } +- const shouldMaintainScrollAtEndAfterPendingSettle = !!state.pendingMaintainScrollAtEnd || !!((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onDataChange); +- if (didResolvePendingNativeMVCPAdjust && shouldMaintainScrollAtEndAfterPendingSettle) { +- state.pendingMaintainScrollAtEnd = false; +- doMaintainScrollAtEnd(ctx); + return false; + } + settle.quietPasses = 0; @@ -4520,17 +5023,6 @@ index 914d2da..ce6a0d7 100644 + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} -+var SETTLE_ECHO_WINDOW_MS = 150; -+function releaseScrollTargetSettleIfMoved(ctx, _scrollOffset) { -+ const state = ctx.state; -+ const settle = state.scrollTargetSettle; -+ if (!settle) { -+ return; -+ } -+ if (Date.now() - settle.requestedAt > SETTLE_ECHO_WINDOW_MS) { -+ clearScrollTargetSettle(state); -+ } -+} + +// src/core/cancelImperativeScroll.ts +function cancelScrollCompletionChecks({ scheduledWork }) { @@ -4562,7 +5054,9 @@ index 914d2da..ce6a0d7 100644 + nativeEvent: { + ...event.nativeEvent, + contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -+ } + } +- state.dataChangeNeedsScrollUpdate = false; +- state.lastScrollDelta = 0; + }; +} +function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { @@ -4579,9 +5073,13 @@ index 914d2da..ce6a0d7 100644 + (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 + ) + ); -+ } -+} -+ + } + } + +-// src/core/scrollTo.ts +-function getAverageSizeSnapshot(state) { +- if (Object.keys(state.averageSizes).length === 0) { +- return void 0; +// src/utils/setInitialRenderState.ts +function resetInitialRenderState(ctx, { + resetLayout, @@ -4591,18 +5089,31 @@ index 914d2da..ce6a0d7 100644 + if (resetLayout) { + state.didContainersLayout = false; + state.queuedInitialLayout = false; -+ } + } +- const snapshot = {}; +- for (const itemType in state.averageSizes) { +- const averages = state.averageSizes[itemType]; +- snapshot[itemType] = averages.avg; + if (resetInitialScroll) { + state.didFinishInitialScroll = false; } -- if (targetIndex === void 0) { -- const startBottom = getItemBottom(ctx, start); -- if (startBottom === void 0 || startBottom <= viewportStart) { -- return void 0; -- } +- return snapshot; + set$(ctx, "readyToRender", false); + resetAdaptiveRender(ctx); -+} + } +-function syncInitialScrollNativeWatchdog(state, options) { +- var _a3; +- const { isInitialScroll, requestedOffset, targetOffset } = options; +- const existingWatchdog = initialScrollWatchdog.get(state); +- const shouldWatchInitialNativeScroll = !state.didFinishInitialScroll && (isInitialScroll || !!existingWatchdog) && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); +- const shouldClearInitialNativeScrollWatchdog = !state.didFinishInitialScroll && !!existingWatchdog && initialScrollWatchdog.isAtZeroTargetOffset(requestedOffset); +- if (shouldWatchInitialNativeScroll) { +- state.hasScrolled = false; +- initialScrollWatchdog.set(state, { +- startScroll: (_a3 = existingWatchdog == null ? void 0 : existingWatchdog.startScroll) != null ? _a3 : state.scroll, +- targetOffset +- }); +- return; +function setInitialRenderState(ctx, { + didLayout, + didInitialScroll @@ -4615,51 +5126,53 @@ index 914d2da..ce6a0d7 100644 + if (didLayout) { + state.didContainersLayout = true; } -- while (start > 0) { -- const top = state.positions[start]; -- if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { -- break; -- } -- start--; +- if (shouldClearInitialNativeScrollWatchdog) { +- initialScrollWatchdog.clear(state); + if (didInitialScroll) { + state.didFinishInitialScroll = true; } -- while (start > 0) { -- const previousBottom = getItemBottom(ctx, start - 1); -- if (previousBottom === void 0 || previousBottom <= viewportStart) { -- break; +-} +-function findPositionIndexAtOrBeforeOffset(ctx, offset) { +- const state = ctx.state; +- const dataLength = state.props.data.length; +- let low = 0; +- let high = dataLength - 1; +- let match; +- while (low <= high) { +- const mid = Math.floor((low + high) / 2); +- const top = state.positions[mid]; +- if (top === void 0) { +- high = mid - 1; +- } else { +- if (top <= offset) { +- match = mid; +- low = mid + 1; +- } else { +- high = mid - 1; + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); + if (isReadyToRender && !peek$(ctx, "readyToRender")) { + set$(ctx, "readyToRender", true); + setAdaptiveRender(ctx, "normal", "ready"); + if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { + scheduleFullDrawDistancePrewarm(ctx); - } -- start--; -- } -- let end = start; -- while (end + 1 < dataLength) { -- const nextTop = state.positions[end + 1]; -- if (nextTop === void 0 || nextTop > viewportEnd) { -- break; ++ } + if (!state.didLoad) { + state.didLoad = true; + if (onLoad) { + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); -+ } + } } -- end++; } -- return { end, start }; +- return match; } --function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -- if (range) { -- ctx.state.scrollTargetPinnedRange = range; -- ctx.state.scrollForNextCalculateItemsInView = void 0; -- } else { -- ctx.state.scrollTargetPinnedRange = void 0; +-function getItemBottom(ctx, index) { +- var _a3; +- const top = ctx.state.positions[index]; +- if (top === void 0) { +- return void 0; - } +- const itemSize = (_a3 = getItemSizeAtIndex(ctx, index)) != null ? _a3 : 0; +- return top + (Number.isFinite(itemSize) ? itemSize : 0); + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -4668,8 +5181,7 @@ index 914d2da..ce6a0d7 100644 + state.scrollPending = offset; + state.scrollPrev = offset; } --function scrollTo(ctx, params) { -- var _a3, _b; +-function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -4682,53 +5194,104 @@ index 914d2da..ce6a0d7 100644 +function supersedeInitialScroll(ctx) { + var _a3, _b, _c; const state = ctx.state; -- const { noScrollingTo, forceScroll, ...scrollTarget } = params; -- const { -- animated, -- isInitialScroll, -- offset: scrollTargetOffset, -- precomputedWithViewOffset, -- waitForInitialScrollCompletionFrame -- } = scrollTarget; -- const { -- props: { horizontal } -- } = state; -- cancelScrollCompletionChecks(state); -- const requestedOffset = precomputedWithViewOffset ? scrollTargetOffset : calculateOffsetWithOffsetPosition(ctx, scrollTargetOffset, scrollTarget); -- const shouldPreserveRawInitialOffsetRequest = !!isInitialScroll && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; -- const targetOffset = clampScrollOffset(ctx, requestedOffset, scrollTarget); -- const offset = shouldPreserveRawInitialOffsetRequest ? requestedOffset : targetOffset; -- state.scrollHistory.length = 0; -- if (!noScrollingTo) { -- if (isInitialScroll) { -- initialScrollCompletion.resetFlags(state); +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return void 0; +- } +- const viewportStart = Math.max(0, targetOffset); +- const viewportEnd = Math.max(viewportStart, targetOffset + state.scrollLength); +- let start = targetIndex !== void 0 ? Math.max(0, Math.min(dataLength - 1, targetIndex)) : findPositionIndexAtOrBeforeOffset(ctx, viewportStart); +- if (start === void 0) { +- return void 0; +- } +- if (targetIndex !== void 0 && state.positions[start] === void 0) { +- return { end: start, start }; +- } +- if (targetIndex === void 0) { +- const startBottom = getItemBottom(ctx, start); +- if (startBottom === void 0 || startBottom <= viewportStart) { +- return void 0; +- } +- } +- while (start > 0) { +- const top = state.positions[start]; +- if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { +- break; +- } +- start--; +- } +- while (start > 0) { +- const previousBottom = getItemBottom(ctx, start - 1); +- if (previousBottom === void 0 || previousBottom <= viewportStart) { +- break; + const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; + if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { + if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { + cancelAnimationFrame(bootstrapInitialScroll.frameHandle); } -- const averageSizeSnapshot = getAverageSizeSnapshot(state); -- state.scrollingTo = { -- ...scrollTarget, -- ...averageSizeSnapshot ? { averageSizeSnapshot } : {}, -- targetOffset, -- waitForInitialScrollCompletionFrame -- }; -- if (!isInitialScroll) { -- pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); -+ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { -+ cancelScrollCompletionChecks(state); -+ state.scrollingTo = void 0; -+ state.scrollTargetPinnedRange = void 0; -+ } -+ initialScrollCompletion.resetFlags(state); +- start--; +- } +- let end = start; +- while (end + 1 < dataLength) { +- const nextTop = state.positions[end + 1]; +- if (nextTop === void 0 || nextTop > viewportEnd) { +- break; ++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { ++ cancelScrollCompletionChecks(state); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; + } +- end++; +- } +- return { end, start }; +-} +-function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); +- if (range) { +- ctx.state.scrollTargetPinnedRange = range; +- ctx.state.scrollForNextCalculateItemsInView = void 0; +- } else { +- ctx.state.scrollTargetPinnedRange = void 0; ++ initialScrollCompletion.resetFlags(state); + setInitialScrollSession(state, { bootstrap: null }); + finishInitialScroll(ctx); -+ } -+} + } + } +-function scrollTo(ctx, params) { +- var _a3, _b; +function finishInitialScroll(ctx, options) { + var _a3, _b, _c; -+ const state = ctx.state; + const state = ctx.state; +- const { noScrollingTo, forceScroll, ...scrollTarget } = params; +- const { +- animated, +- isInitialScroll, +- offset: scrollTargetOffset, +- precomputedWithViewOffset, +- waitForInitialScrollCompletionFrame +- } = scrollTarget; +- const { +- props: { horizontal } +- } = state; +- cancelScrollCompletionChecks(state); +- const requestedOffset = precomputedWithViewOffset ? scrollTargetOffset : calculateOffsetWithOffsetPosition(ctx, scrollTargetOffset, scrollTarget); +- const shouldPreserveRawInitialOffsetRequest = !!isInitialScroll && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; +- const targetOffset = clampScrollOffset(ctx, requestedOffset, scrollTarget); +- const offset = shouldPreserveRawInitialOffsetRequest ? requestedOffset : targetOffset; +- state.scrollHistory.length = 0; +- if (!noScrollingTo) { +- if (isInitialScroll) { +- initialScrollCompletion.resetFlags(state); +- } +- const averageSizeSnapshot = getAverageSizeSnapshot(state); +- state.scrollingTo = { +- ...scrollTarget, +- ...averageSizeSnapshot ? { averageSizeSnapshot } : {}, +- targetOffset, +- waitForInitialScrollCompletionFrame +- }; +- if (!isInitialScroll) { +- pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); + if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { + syncInitialScrollOffset(state, options.resolvedOffset); + } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { @@ -4797,7 +5360,15 @@ index 914d2da..ce6a0d7 100644 } // src/core/scrollToIndex.ts -@@ -4367,8 +4501,17 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4350,6 +4486,7 @@ function calculateItemsInView(ctx, params = {}) { + startIndex + }); + totalSize = getContentSize(ctx); ++ const minIndexSizeChangedThisPass = dataChanged ? 0 : minIndexSizeChanged; + if (minIndexSizeChanged !== void 0) { + state.minIndexSizeChanged = void 0; + } +@@ -4367,8 +4504,17 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -4811,13 +5382,13 @@ index 914d2da..ce6a0d7 100644 + } + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { -+ settleScrollTarget(ctx, isCompensating); ++ settleScrollTarget(ctx, { isCompensating, minIndexSizeChanged: minIndexSizeChangedThisPass }); + } + if (didMVCPAdjust) { updateScroll2(state.scroll); updateScrollRange(); } -@@ -5915,6 +6058,119 @@ function useRafCoalescer(callback) { +@@ -5915,6 +6061,119 @@ function useRafCoalescer(callback) { return coalescer; } @@ -4937,16 +5508,18 @@ index 914d2da..ce6a0d7 100644 // src/components/webConstants.ts var LEGEND_LIST_CONTENT_CONTAINER_CLASS = "legend-list-content-container"; var LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS = "legend-list-scrollbar-x-hidden"; -@@ -6006,6 +6262,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -6006,6 +6265,10 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; +var SMOOTH_SCROLL_MAX_MS = 2e3; ++var BORROW_MEASURE_ATTEMPTS = 2; ++var USER_INTERACTION_EVENTS = ["wheel", "pointerdown", "touchstart", "keydown"]; var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6074,6 +6332,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6074,6 +6337,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -4958,33 +5531,47 @@ index 914d2da..ce6a0d7 100644 const getMaxScrollOffset = React3.useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6094,6 +6357,59 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,6 +6362,95 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); + const paddedNodeRef = React3.useRef(null); ++ const borrowedExtentsRef = React3.useRef(/* @__PURE__ */ new Map()); ++ const borrowIdRef = React3.useRef(0); ++ const borrowWatchRef = React3.useRef(0); + const animatedPaddingReleaseRef = React3.useRef(void 0); + const withReachableExtent = React3.useCallback( + (offset, maxOffset, animated, run) => { + var _a4; + const contentNode = contentRef.current; -+ const committedMaxOffset = getCommittedMaxScrollOffset(maxOffset); ++ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); ++ const committedMaxOffset = getCommittedMaxScrollOffset(getMaxScrollOffset()); + if (!contentNode || offset <= committedMaxOffset + SCROLL_EXTENT_EPSILON) { + run(clampOffset(offset, maxOffset)); + return; + } -+ const release = addTemporaryEndPadding( -+ contentNode, -+ paddingEndProp, -+ offset - committedMaxOffset + SCROLL_EXTENT_EPSILON -+ ); ++ const releases = []; ++ for (let attempt = 0; attempt < BORROW_MEASURE_ATTEMPTS; attempt++) { ++ const shortfall = offset - getMaxScrollOffset(); ++ if (shortfall <= 0) { ++ break; ++ } ++ releases.push(addTemporaryEndPadding(contentNode, paddingEndProp, shortfall + SCROLL_EXTENT_EPSILON)); ++ } ++ const borrowId = ++borrowIdRef.current; ++ const release = () => { ++ for (const releaseOne of releases) { ++ releaseOne(); ++ } ++ borrowedExtentsRef.current.delete(borrowId); ++ }; ++ borrowedExtentsRef.current.set(borrowId, Math.max(0, getMaxScrollOffset() - committedMaxOffset)); + paddedNodeRef.current = contentNode; + run(offset); + if (!animated) { + scheduleTemporaryEndPaddingRelease(contentNode, paddingEndProp, release); + return; + } -+ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); + const scrollTarget = getScrollTarget(); + const supportsScrollEnd = !!scrollTarget && "onscrollend" in scrollTarget; + const finish = () => { @@ -4993,21 +5580,40 @@ index 914d2da..ce6a0d7 100644 + } + animatedPaddingReleaseRef.current = void 0; + clearTimeout(settleTimeout); -+ scrollTarget == null ? void 0 : scrollTarget.removeEventListener("scrollend", finish); ++ scrollTarget == null ? void 0 : scrollTarget.removeEventListener("scrollend", finishIfArrived); ++ cancelAnimationFrame(borrowWatchRef.current); + release(); + }; ++ const finishIfArrived = () => { ++ if (Math.abs(getCurrentScrollOffset() - offset) <= SCROLL_EXTENT_EPSILON) { ++ finish(); ++ } ++ }; ++ const releaseWhenContentCommits = () => { ++ if (animatedPaddingReleaseRef.current !== finish) { ++ return; ++ } ++ if (getCommittedMaxScrollOffset(getMaxScrollOffset()) + SCROLL_EXTENT_EPSILON >= offset) { ++ finish(); ++ return; ++ } ++ borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); ++ }; + animatedPaddingReleaseRef.current = finish; ++ cancelAnimationFrame(borrowWatchRef.current); ++ borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); + const settleTimeout = setTimeout(finish, SMOOTH_SCROLL_MAX_MS); + if (supportsScrollEnd) { -+ scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finish); ++ scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finishIfArrived); + } + }, -+ [getCommittedMaxScrollOffset, getScrollTarget, paddingEndProp] ++ [getCommittedMaxScrollOffset, getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, paddingEndProp] + ); + React3.useEffect( + () => () => { + var _a4; + (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); ++ cancelAnimationFrame(borrowWatchRef.current); + const paddedNode = paddedNodeRef.current; + if (paddedNode) { + releaseAllTemporaryEndPadding(paddedNode); @@ -5015,10 +5621,13 @@ index 914d2da..ce6a0d7 100644 + }, + [] + ); ++ const reportUserInteraction = React3.useCallback(() => { ++ releaseScrollTargetForUserInteraction(ctx); ++ }, [ctx]); const scrollToLocalOffset = React3.useCallback( (offset, animated) => { const scrollElement = scrollRef.current; -@@ -6116,14 +6432,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6116,14 +6473,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); options.left = left; options.top = top; @@ -5045,7 +5654,7 @@ index 914d2da..ce6a0d7 100644 ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6472,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6149,8 +6513,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -5055,13 +5664,16 @@ index 914d2da..ce6a0d7 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6163,7 +6485,12 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6163,7 +6526,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView if (!onScroll2 || !scrollRef.current) { return; } - const contentSize = getContentSize2(contentRef.current); + const rawContentSize = getContentSize2(contentRef.current); -+ const temporaryPadding = getTemporaryEndPadding(contentRef.current, paddingEndProp); ++ let temporaryPadding = 0; ++ for (const bought of borrowedExtentsRef.current.values()) { ++ temporaryPadding = Math.max(temporaryPadding, bought); ++ } + const contentSize = temporaryPadding ? { + height: horizontal ? rawContentSize.height : rawContentSize.height - temporaryPadding, + width: horizontal ? rawContentSize.width - temporaryPadding : rawContentSize.width @@ -5069,22 +5681,25 @@ index 914d2da..ce6a0d7 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6379,10 +6706,12 @@ function useValueListener$(key, callback) { - } - - // src/components/ScrollAdjust.tsx --function getScrollAdjustAxis(horizontal) { -+function getScrollAdjustAxis(horizontal, rtl = false) { - return horizontal ? { - contentSizeKey: "scrollWidth", -- paddingEndProp: "paddingRight", -+ // The end side under RTL is the left, matching how the list pads its own content and -+ // which property the scroll view borrows room on. -+ paddingEndProp: rtl ? "paddingLeft" : "paddingRight", - viewportSizeKey: "clientWidth", - x: 1, - y: 0 -@@ -6411,8 +6740,6 @@ function ScrollAdjust() { +@@ -6221,11 +6592,17 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + const target = getScrollTarget(); + if (!target) return; + target.addEventListener("scroll", handleScroll, { passive: true }); ++ for (const type of USER_INTERACTION_EVENTS) { ++ target.addEventListener(type, reportUserInteraction, { passive: true }); ++ } + if ("onscrollend" in target) { + target.addEventListener("scrollend", emitScrollEnd); + } + return () => { + target.removeEventListener("scroll", handleScroll); ++ for (const type of USER_INTERACTION_EVENTS) { ++ target.removeEventListener(type, reportUserInteraction); ++ } + if ("onscrollend" in target) { + target.removeEventListener("scrollend", emitScrollEnd); + } +@@ -6411,8 +6788,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3__namespace.useRef(0); const lastScrollAdjustUserOffsetRef = React3__namespace.useRef(0); @@ -5093,16 +5708,7 @@ index 914d2da..ce6a0d7 100644 const contentNodeRef = React3__namespace.useRef(null); const callback = React3__namespace.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6423,7 +6750,7 @@ function ScrollAdjust() { - const target = getScrollAdjustTarget(ctx, contentNodeRef.current); - if (target) { - const horizontal = !!ctx.state.props.horizontal; -- const axis = getScrollAdjustAxis(horizontal); -+ const axis = getScrollAdjustAxis(horizontal, isHorizontalRTL(ctx.state)); - const { contentNode, scrollElement: el } = target; - const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; - const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6438,29 +6765,10 @@ function ScrollAdjust() { +@@ -6438,29 +6813,10 @@ function ScrollAdjust() { const nextScroll = currentScroll + scrollDelta; const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; if (needsTemporaryPadding) { @@ -5134,7 +5740,7 @@ index 914d2da..ce6a0d7 100644 } else { scrollBy(); } -@@ -8288,6 +8596,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8288,6 +8644,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -5143,10 +5749,10 @@ index 914d2da..ce6a0d7 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs -index 95465f2..b028ddc 100644 +index 95465f2..9ac6d3f 100644 --- a/node_modules/@legendapp/list/react-native.web.mjs +++ b/node_modules/@legendapp/list/react-native.web.mjs -@@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; +@@ -413,178 +413,266 @@ var EDGE_POSITION_EPSILON = 1; var ENABLE_DEVMODE = IS_DEV && false; var ENABLE_DEBUG_VIEW = IS_DEV && false; @@ -5156,22 +5762,56 @@ index 95465f2..b028ddc 100644 - scheduledWork.cancel("checkFinishedScrollRetryFrame"); - scheduledWork.cancel("checkFinishedScrollFallback"); - scheduledWork.cancel("platformScrollCompletion"); --} ++// src/core/getStartOffsetAdjustment.ts ++function getStartOffsetAdjustment(ctx) { ++ const { state } = ctx; ++ const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; ++ return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); + } -function settlePendingImperativeScroll(state) { - var _a3, _b; - const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; - state.pendingScrollResolve = void 0; - state.pendingScrollToEnd = void 0; - resolvePendingScroll == null ? void 0 : resolvePendingScroll(); --} ++ ++// src/utils/getId.ts ++function getId(state, index) { ++ const { data, keyExtractor } = state.props; ++ if (!data) { ++ return ""; ++ } ++ const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; ++ const id = ret; ++ state.idCache[index] = id; ++ return id; + } -function cancelImperativeScroll(state) { - cancelScrollCompletionChecks(state); - state.scheduledWork.cancel("imperativeScrollReady"); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; - settlePendingImperativeScroll(state); --} -- ++ ++// src/core/updateContentMetricsState.ts ++function getRawContentLength(ctx) { ++ var _a3, _b, _c; ++ const { state, values } = ctx; ++ return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); ++} ++function getAlignItemsAtEndPadding(ctx) { ++ const { state } = ctx; ++ const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; ++ return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; ++} ++function updateContentMetricsState(ctx) { ++ const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; ++ const nextPadding = getAlignItemsAtEndPadding(ctx); ++ if (previousPadding !== nextPadding) { ++ set$(ctx, "alignItemsAtEndPadding", nextPadding); ++ } + } + -// src/core/deferredPublicOnScroll.ts -function withResolvedContentOffset(state, event, resolvedOffset) { - return { @@ -5179,12 +5819,40 @@ index 95465f2..b028ddc 100644 - nativeEvent: { - ...event.nativeEvent, - contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -- } ++// src/core/addTotalSize.ts ++function addTotalSize(ctx, key, add, notifyTotalSize = true) { ++ const state = ctx.state; ++ const prevTotalSize = state.totalSize; ++ let totalSize = state.totalSize; ++ if (key === null) { ++ totalSize = add; ++ if (state.timeoutSetPaddingTop) { ++ clearTimeout(state.timeoutSetPaddingTop); ++ state.timeoutSetPaddingTop = void 0; + } - }; --} ++ } else { ++ totalSize += add; ++ } ++ if (prevTotalSize !== totalSize) { ++ { ++ state.pendingTotalSize = void 0; ++ state.totalSize = totalSize; ++ if (notifyTotalSize) { ++ set$(ctx, "totalSize", totalSize); ++ } ++ updateContentMetricsState(ctx); ++ } ++ } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { ++ set$(ctx, "totalSize", totalSize); ++ } + } -function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { - var _a3, _b, _c, _d; -- const state = ctx.state; ++ ++// src/core/setSize.ts ++function setSize(ctx, itemKey, size, notifyTotalSize = true) { + const state = ctx.state; - const deferredEvent = state.deferredPublicOnScrollEvent; - state.deferredPublicOnScrollEvent = void 0; - if (deferredEvent) { @@ -5196,9 +5864,15 @@ index 95465f2..b028ddc 100644 - (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 - ) - ); -- } --} -- ++ const { sizes } = state; ++ const previousSize = sizes.get(itemKey); ++ const diff = previousSize !== void 0 ? size - previousSize : size; ++ if (diff !== 0) { ++ addTotalSize(ctx, itemKey, diff, notifyTotalSize); + } ++ sizes.set(itemKey, size); + } + -// src/core/initialScrollSession.ts -var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -function hasInitialScrollSessionCompletion(completion) { @@ -5207,7 +5881,10 @@ index 95465f2..b028ddc 100644 -function clearInitialScrollSession(state) { - state.initialScrollSession = void 0; - return void 0; --} ++// src/utils/helpers.ts ++function isFunction(obj) { ++ return typeof obj === "function"; + } -function createInitialScrollSession(options) { - const { bootstrap, completion, kind, previousDataLength } = options; - return kind === "offset" ? { @@ -5220,7 +5897,9 @@ index 95465f2..b028ddc 100644 - kind, - previousDataLength - }; --} ++function isArray(obj) { ++ return Array.isArray(obj); + } -function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { - var _a4, _b2; - if (!state.initialScrollSession) { @@ -5236,10 +5915,15 @@ index 95465f2..b028ddc 100644 - kind, - previousDataLength: state.initialScrollSession.previousDataLength - }); -- } ++var warned = /* @__PURE__ */ new Set(); ++function warnDevOnce(id, text) { ++ if (IS_DEV && !warned.has(id)) { ++ warned.add(id); ++ console.warn(`[legend-list] ${text}`); + } - (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; - return state.initialScrollSession.completion; --} + } -var initialScrollCompletion = { - didDispatchNativeScroll(state) { - var _a3, _b; @@ -5262,7 +5946,26 @@ index 95465f2..b028ddc 100644 - const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); - completion.didDispatchNativeScroll = void 0; - completion.didRetrySilentInitialScroll = void 0; -- } ++function roundSize(size) { ++ return Math.floor(size * 8) / 8; ++} ++function isNullOrUndefined(value) { ++ return value === null || value === void 0; ++} ++function getPadding(s, type) { ++ var _a3, _b, _c; ++ const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; ++ return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; ++} ++function extractPadding(style, contentContainerStyle, type) { ++ return getPadding(style, type) + getPadding(contentContainerStyle, type); ++} ++function findContainerId(ctx, key) { ++ var _a3, _b; ++ const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); ++ if (directMatch !== void 0) { ++ return directMatch; + } -}; -var initialScrollWatchdog = { - clear(state) { @@ -5286,7 +5989,12 @@ index 95465f2..b028ddc 100644 - var _a3, _b; - if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { - return; -- } ++ const numContainers = peek$(ctx, "numContainers"); ++ for (let i = 0; i < numContainers; i++) { ++ const itemKey = peek$(ctx, `containerItemKey${i}`); ++ if (itemKey === key) { ++ return i; + } - const completion = ensureInitialScrollSessionCompletion(state); - completion.watchdog = watchdog ? { - startScroll: watchdog.startScroll, @@ -5306,7 +6014,7 @@ index 95465f2..b028ddc 100644 - } - if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { - return clearInitialScrollSession(state); -- } + } - const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; - state.initialScrollSession = createInitialScrollSession({ - bootstrap, @@ -5315,227 +6023,164 @@ index 95465f2..b028ddc 100644 - previousDataLength - }); - return state.initialScrollSession; --} -- ++ return -1; + } + -// src/utils/checkThreshold.ts -var HYSTERESIS_MULTIPLIER = 1.3; -function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { - const absDistance = Math.abs(distance); - return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; --} --var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { -- const absDistance = Math.abs(distance); -- const within = atThreshold || threshold > 0 && absDistance <= threshold; -- const updateSnapshot = () => { -- setSnapshot({ -- atThreshold, -- contentSize: context.contentSize, -- dataLength: context.dataLength, -- scrollPosition: context.scrollPosition -- }); -- }; -- if (!wasReached) { -- if (!within) { -- return false; -- } -- onReached(distance); -- updateSnapshot(); -- return true; -- } -- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); -- if (reset) { -- setSnapshot(void 0); -- return false; -- } -- if (within) { -- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; -- if (changed) { -- updateSnapshot(); -- } -- } -- return true; --}; -- --// src/utils/edgeReachedGate.ts --function resetEdgeLatch(ctx, edge) { -- const state = ctx.state; -- if (edge === "start") { -- state.isStartReached = false; -- state.startReachedSnapshot = void 0; -- } else { -- state.isEndReached = false; -- state.endReachedSnapshot = void 0; -- } --} --function resetSharedEdgeGateIfOutsideHysteresis(ctx) { -- const state = ctx.state; -- if (!state.edgeReachedGate) { -- return; -- } -- const contentSize = getContentSize(ctx); -- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); -- const isContentLess = contentSize < state.scrollLength; -- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; -- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; -- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); -- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); -- if (isOutsideStart && isOutsideEnd) { -- state.edgeReachedGate = void 0; -- } --} --function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { -- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; --} --function markReachedEdge(ctx) { -- ctx.state.edgeReachedGate = "closed"; --} --function prepareReachedEdgeForNextUserScroll(ctx) { -- if (ctx.state.edgeReachedGate) { -- ctx.state.edgeReachedGate = "prepared"; -- } --} --function beginReachedEdgeUserScroll(ctx, scrollDelta) { -- const state = ctx.state; -- if (state.edgeReachedGate !== "prepared") { -- return void 0; -- } -- const allowedEdge = scrollDelta < 0 ? "start" : "end"; -- state.edgeReachedGate = "closed"; -- resetEdgeLatch(ctx, allowedEdge); -- return allowedEdge; --} -- --// src/utils/hasActiveInitialScroll.ts --function hasActiveInitialScroll(state) { -- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; --} -- --// src/utils/checkAtBottom.ts --function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -- var _a3; -- const state = ctx.state; -- if (!state) { -- return; -- } -- const { -- queuedInitialLayout, -- scrollLength, -- scroll, -- maintainingScrollAtEnd, -- props: { maintainScrollAtEndThreshold, onEndReachedThreshold } -- } = state; -- const contentSize = getContentSize(ctx); -- resetSharedEdgeGateIfOutsideHysteresis(ctx); -- if (contentSize > 0 && queuedInitialLayout) { -- const insetEnd = getContentInsetEnd(ctx); -- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; -- const isContentLess = contentSize < scrollLength; -- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); -- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); -- set$( -- ctx, -- "isWithinMaintainScrollAtEndThreshold", -- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength -- ); -- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; -- if (!shouldSkipThresholdChecks) { -- state.isEndReached = checkThreshold( -- distanceFromEnd, -- isContentLess, -- onEndReachedThreshold * scrollLength, -- state.isEndReached, -- state.endReachedSnapshot, -- { -- contentSize, -- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, -- scrollPosition: scroll -- }, -- (distance) => { -- var _a4, _b; -- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { -- markReachedEdge(ctx); -- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); -- } -- }, -- (snapshot) => { -- state.endReachedSnapshot = snapshot; -- } -- ); -- } -- } --} -- --// src/utils/checkAtTop.ts --function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -- const state = ctx == null ? void 0 : ctx.state; -- if (!state) { -- return; -- } -- const { -- isStartReached, -- props: { data, onStartReachedThreshold }, -- scroll, -- scrollLength, -- startReachedSnapshot, -- totalSize -- } = state; -- const dataLength = data.length; -- const threshold = onStartReachedThreshold * scrollLength; -- resetSharedEdgeGateIfOutsideHysteresis(ctx); -- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { -- state.isStartReached = false; -- state.startReachedSnapshot = void 0; -- } -- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); -- set$(ctx, "isNearStart", scroll <= threshold); -- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; -- if (!shouldSkipThresholdChecks) { -- state.isStartReached = checkThreshold( -- scroll, -- false, -- threshold, -- state.isStartReached, -- startReachedSnapshot, -- { -- contentSize: totalSize, -- dataLength, -- scrollPosition: scroll -- }, -- (distance) => { -- var _a3, _b; -- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { -- markReachedEdge(ctx); -- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); -- } -- }, -- (snapshot) => { -- state.startReachedSnapshot = snapshot; -- } -- ); -- } --} -- --// src/utils/checkThresholds.ts --function checkThresholds(ctx, allowedEdge) { -- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; -- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); --} -- --// src/core/recalculateSettledScroll.ts --function recalculateSettledScroll(ctx) { -- var _a3, _b; -- const state = ctx.state; -- if ((_a3 = state.props) == null ? void 0 : _a3.data) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); -- } -- checkThresholds(ctx); --} --var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; --var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; --var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; --function clearAdaptiveRenderExitTimeout(ctx) { -- ctx.state.scheduledWork.cancel("adaptiveRender"); ++// src/utils/getItemSize.ts ++function getKnownOrFixedSize(ctx, key, index, data, resolved) { ++ var _a3, _b; ++ const state = ctx.state; ++ const { getFixedItemSize, getItemType } = state.props; ++ let size = key ? state.sizesKnown.get(key) : void 0; ++ if (size === void 0 && key && getFixedItemSize) { ++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; ++ const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); ++ if (fixedSize !== void 0) { ++ size = fixedSize + ctx.scrollAxisGap; ++ state.sizesKnown.set(key, size); ++ } ++ } ++ return size; ++} ++function getKnownOrFixedItemSize(ctx, index) { ++ const key = getId(ctx.state, index); ++ return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); ++} ++function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { ++ for (let index = startIndex; index <= endIndex; index++) { ++ if (getKnownOrFixedItemSize(ctx, index) === void 0) { ++ return false; ++ } ++ } ++ return true; ++} ++function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { ++ var _a3, _b, _c, _d; ++ const state = ctx.state; ++ const { ++ sizes, ++ averageSizes, ++ props: { estimatedItemSize, getItemType }, ++ scrollingTo ++ } = state; ++ const sizeKnown = state.sizesKnown.get(key); ++ if (sizeKnown !== void 0) { ++ return sizeKnown; ++ } ++ let size; ++ const renderedSize = sizes.get(key); ++ if (preferCachedSize) { ++ if (renderedSize !== void 0) { ++ return renderedSize; ++ } ++ } ++ size = getKnownOrFixedSize(ctx, key, index, data, resolved); ++ if (size !== void 0) { ++ setSize(ctx, key, size, notifyTotalSize); ++ return size; ++ } ++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; ++ if (useAverageSize && !scrollingTo) { ++ const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; ++ if (averageSizeForType !== void 0) { ++ size = roundSize(averageSizeForType); ++ } ++ } ++ if (size === void 0 && renderedSize !== void 0) { ++ return renderedSize; ++ } ++ if (size === void 0 && useAverageSize && scrollingTo) { ++ const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; ++ if (averageSizeForType !== void 0) { ++ size = roundSize(averageSizeForType); ++ } ++ } ++ if (size === void 0) { ++ size = estimatedItemSize + ctx.scrollAxisGap; ++ } ++ setSize(ctx, key, size, notifyTotalSize); ++ return size; ++} ++function getItemSizeAtIndex(ctx, index) { ++ if (index === void 0 || index < 0) { ++ return void 0; ++ } ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; ++ } ++ if (index !== void 0) { ++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); ++ if (startOffsetAdjustment) { ++ offset += startOffsetAdjustment; ++ } ++ } ++ if (viewPosition !== void 0 && index !== void 0) { ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return offset; ++ } ++ const isOutOfBounds = index < 0 || index >= dataLength; ++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; ++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); ++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); ++ const trailingInset = getContentInsetEnd(ctx); ++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); ++ if (!isOutOfBounds && index === state.props.data.length - 1) { ++ const footerSize = peek$(ctx, "footerSize") || 0; ++ offset += footerSize; ++ } ++ } ++ return offset; ++} ++ ++// src/core/clampScrollOffset.ts ++function clampScrollOffset(ctx, offset, scrollTarget) { ++ const state = ctx.state; ++ const contentSize = getContentSize(ctx); ++ let clampedOffset = offset; ++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { ++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); ++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; ++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; ++ const maxOffset = baseMaxOffset + extraEndOffset; ++ clampedOffset = Math.min(offset, maxOffset); ++ } ++ clampedOffset = Math.max(0, clampedOffset); ++ return clampedOffset; ++} ++ ++// src/utils/checkThreshold.ts ++var HYSTERESIS_MULTIPLIER = 1.3; ++function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { ++ const absDistance = Math.abs(distance); ++ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; + } + var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { + const absDistance = Math.abs(distance); +@@ -794,642 +882,346 @@ function recalculateSettledScroll(ctx) { + } + checkThresholds(ctx); + } +-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; +-function clearAdaptiveRenderExitTimeout(ctx) { +- ctx.state.scheduledWork.cancel("adaptiveRender"); -} -function scheduleAdaptiveRenderExit(ctx, exitDelay) { - const state = ctx.state; @@ -5555,7 +6200,10 @@ index 95465f2..b028ddc 100644 - } -} -function resetAdaptiveRender(ctx) { -- var _a3, _b; ++ ++// src/core/finishScrollTo.ts ++function finishScrollTo(ctx) { + var _a3, _b; - clearAdaptiveRenderExitTimeout(ctx); - const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; - if (peek$(ctx, "adaptiveRender") !== mode) { @@ -5564,7 +6212,7 @@ index 95465f2..b028ddc 100644 -} -function updateAdaptiveRender(ctx, scrollVelocity, options) { - var _a3, _b, _c; -- const state = ctx.state; + const state = ctx.state; - const adaptiveRender = state.props.adaptiveRender; - const currentMode = peek$(ctx, "adaptiveRender"); - if (peek$(ctx, "readyToRender")) { @@ -5585,10 +6233,40 @@ index 95465f2..b028ddc 100644 - } - } else { - resetAdaptiveRender(ctx); -- } -- } --} -- ++ if (state == null ? void 0 : state.scrollingTo) { ++ cancelScrollCompletionChecks(state); ++ const resolvePendingScroll = state.pendingScrollResolve; ++ state.pendingScrollResolve = void 0; ++ const scrollingTo = state.scrollingTo; ++ state.scrollHistory.length = 0; ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ if (state.pendingTotalSize !== void 0) { ++ addTotalSize(ctx, null, state.pendingTotalSize); ++ } ++ { ++ state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); ++ } ++ if (scrollingTo.isInitialScroll || state.initialScroll) { ++ const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; ++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; ++ finishInitialScroll(ctx, { ++ onFinished: () => { ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); ++ }, ++ preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, ++ recalculateItems: true, ++ schedulePreservedTargetClear: shouldPreserveResizeTarget, ++ syncObservedOffset: isOffsetSession, ++ waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame ++ }); ++ return; + } ++ recalculateSettledScroll(ctx); ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); + } + } + -// src/utils/getEffectiveDrawDistance.ts -var INITIAL_DRAW_DISTANCE = 50; -function getEffectiveDrawDistance(ctx, mode) { @@ -5602,8 +6280,21 @@ index 95465f2..b028ddc 100644 -function scheduleFullDrawDistancePrewarm(ctx) { - const { state } = ctx; - if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { -- return; -- } ++// src/core/doScrollTo.ts ++var SCROLL_END_IDLE_MS = 80; ++var SCROLL_END_MAX_MS = 1500; ++var SMOOTH_SCROLL_DURATION_MS = 320; ++var SCROLL_END_TARGET_EPSILON = 1; ++function doScrollTo(ctx, params) { ++ var _a3, _b; ++ const state = ctx.state; ++ const { animated, horizontal, offset } = params; ++ state.scheduledWork.cancel("platformScrollCompletion"); ++ const scroller = state.refScroller.current; ++ const node = scroller == null ? void 0 : scroller.getScrollableNode(); ++ if (!scroller || !node) { + return; + } - state.scheduledWork.frame(() => { - var _a3; - return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); @@ -5622,10 +6313,35 @@ index 95465f2..b028ddc 100644 - } - if (resetInitialScroll) { - state.didFinishInitialScroll = false; -- } ++ const isAnimated = !!animated; ++ const isHorizontal = !!horizontal; ++ const contentSize = isHorizontal ? getContentSize(ctx) : void 0; ++ const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; ++ const top = isHorizontal ? 0 : offset; ++ scroller.scrollTo({ animated: isAnimated, x: left, y: top }); ++ if (isAnimated) { ++ const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; ++ listenForScrollEnd(ctx, { ++ readOffset: () => scroller.getCurrentScrollOffset(), ++ target, ++ targetOffset: offset ++ }); ++ } else { ++ state.scroll = offset; ++ const targetToken = state.scrollingTo; ++ state.scheduledWork.timeout( ++ () => { ++ if (targetToken === state.scrollingTo) { ++ finishScrollTo(ctx); ++ } ++ }, ++ 100, ++ "platformScrollCompletion" ++ ); + } - set$(ctx, "readyToRender", false); - resetAdaptiveRender(ctx); --} + } -function setInitialRenderState(ctx, { - didLayout, - didInitialScroll @@ -5654,7 +6370,12 @@ index 95465f2..b028ddc 100644 - onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); - } - } -- } ++function listenForScrollEnd(ctx, params) { ++ const { readOffset, target, targetOffset } = params; ++ if (!target) { ++ finishScrollTo(ctx); ++ return; + } -} - -// src/core/finishInitialScroll.ts @@ -5680,12 +6401,23 @@ index 95465f2..b028ddc 100644 - if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { - if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { - cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -- } ++ const supportsScrollEnd = "onscrollend" in target; ++ let idleTimeout; ++ let settled = false; ++ const { scheduledWork, scrollingTo: targetToken } = ctx.state; ++ const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); ++ const cleanup = () => { ++ target.removeEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.removeEventListener("scrollend", onScrollEnd); + } - if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { - cancelScrollCompletionChecks(state); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; -- } ++ if (idleTimeout !== void 0) { ++ clearTimeout(idleTimeout); + } - initialScrollCompletion.resetFlags(state); - setInitialScrollSession(state, { bootstrap: null }); - finishInitialScroll(ctx); @@ -5700,7 +6432,13 @@ index 95465f2..b028ddc 100644 - const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); - if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { - syncInitialScrollOffset(state, observedOffset); -- } ++ clearTimeout(maxTimeout); ++ }; ++ const cancel = () => { ++ if (!settled) { ++ settled = true; ++ cleanup(); + } - } - const complete = () => { - var _a4, _b2, _c2, _d, _e; @@ -5726,240 +6464,422 @@ index 95465f2..b028ddc 100644 - } - } else { - clearPreservedInitialScrollTarget(state); -- } ++ }; ++ const finish = (reason) => { ++ if (settled) return; ++ if (targetToken !== ctx.state.scrollingTo) { ++ scheduledWork.cancel("platformScrollCompletion"); ++ return; + } - if (options == null ? void 0 : options.recalculateItems) { - recalculateSettledScroll(ctx); -- } ++ const currentOffset = readOffset(); ++ const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; ++ if (reason === "scrollend" && !isNearTarget) { ++ return; + } - setInitialRenderState(ctx, { didInitialScroll: true }); - if (shouldReleaseDeferredPublicOnScroll) { - releaseDeferredPublicOnScroll(ctx, finalScrollOffset); -- } ++ scheduledWork.cancel("platformScrollCompletion"); ++ finishScrollTo(ctx); ++ }; ++ const onScroll2 = () => { ++ if (idleTimeout !== void 0) { ++ clearTimeout(idleTimeout); + } - (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); -- }; ++ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); + }; - if (options == null ? void 0 : options.waitForCompletionFrame) { - requestAnimationFrame(complete); - return; -- } ++ const onScrollEnd = () => finish("scrollend"); ++ target.addEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.addEventListener("scrollend", onScrollEnd); ++ } else { ++ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); + } - complete(); --} -- ++ scheduledWork.register("platformScrollCompletion", cancel); + } + -// src/core/calculateOffsetForIndex.ts -function calculateOffsetForIndex(ctx, index) { - const state = ctx.state; - return index !== void 0 ? state.positions[index] || 0 : 0; --} ++// src/core/initialScrollSession.ts ++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; ++function hasInitialScrollSessionCompletion(completion) { ++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); + } - - // src/core/getStartOffsetAdjustment.ts - function getStartOffsetAdjustment(ctx) { - const { state } = ctx; -@@ -1213,58 +614,273 @@ function getItemSizeAtIndex(ctx, index) { - if (index === void 0 || index < 0) { - return void 0; - } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+ const targetId = getId(ctx.state, index); -+ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+} -+ -+// src/core/calculateOffsetWithOffsetPosition.ts -+function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -+ var _a3; -+ const state = ctx.state; -+ const { index, viewOffset, viewPosition } = params; -+ let offset = offsetParam; -+ if (viewOffset) { -+ offset -= viewOffset; -+ } -+ if (index !== void 0) { -+ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -+ if (startOffsetAdjustment) { -+ offset += startOffsetAdjustment; -+ } -+ } -+ if (viewPosition !== void 0 && index !== void 0) { -+ const dataLength = state.props.data.length; -+ if (dataLength === 0) { -+ return offset; -+ } -+ const isOutOfBounds = index < 0 || index >= dataLength; -+ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -+ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -+ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -+ const trailingInset = getContentInsetEnd(ctx); -+ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -+ if (!isOutOfBounds && index === state.props.data.length - 1) { -+ const footerSize = peek$(ctx, "footerSize") || 0; -+ offset += footerSize; -+ } -+ } -+ return offset; -+} -+ -+// src/core/clampScrollOffset.ts -+function clampScrollOffset(ctx, offset, scrollTarget) { -+ const state = ctx.state; -+ const contentSize = getContentSize(ctx); -+ let clampedOffset = offset; -+ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { -+ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); -+ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; -+ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; -+ const maxOffset = baseMaxOffset + extraEndOffset; -+ clampedOffset = Math.min(offset, maxOffset); -+ } -+ clampedOffset = Math.max(0, clampedOffset); -+ return clampedOffset; -+} -+ -+// src/utils/checkThreshold.ts -+var HYSTERESIS_MULTIPLIER = 1.3; -+function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { -+ const absDistance = Math.abs(distance); -+ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; -+} -+var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { -+ const absDistance = Math.abs(distance); -+ const within = atThreshold || threshold > 0 && absDistance <= threshold; -+ const updateSnapshot = () => { -+ setSnapshot({ -+ atThreshold, -+ contentSize: context.contentSize, -+ dataLength: context.dataLength, -+ scrollPosition: context.scrollPosition -+ }); +-// src/core/getStartOffsetAdjustment.ts +-function getStartOffsetAdjustment(ctx) { +- const { state } = ctx; +- const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; +- return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); ++function clearInitialScrollSession(state) { ++ state.initialScrollSession = void 0; ++ return void 0; + } +- +-// src/utils/getId.ts +-function getId(state, index) { +- const { data, keyExtractor } = state.props; +- if (!data) { +- return ""; +- } +- const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; +- const id = ret; +- state.idCache[index] = id; +- return id; ++function createInitialScrollSession(options) { ++ const { bootstrap, completion, kind, previousDataLength } = options; ++ return kind === "offset" ? { ++ completion, ++ kind, ++ previousDataLength ++ } : { ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength + }; -+ if (!wasReached) { -+ if (!within) { -+ return false; -+ } -+ onReached(distance); -+ updateSnapshot(); -+ return true; -+ } -+ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); -+ if (reset) { -+ setSnapshot(void 0); -+ return false; -+ } -+ if (within) { -+ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; -+ if (changed) { -+ updateSnapshot(); -+ } + } +- +-// src/core/updateContentMetricsState.ts +-function getRawContentLength(ctx) { +- var _a3, _b, _c; +- const { state, values } = ctx; +- return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); ++function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { ++ var _a4, _b2; ++ if (!state.initialScrollSession) { ++ state.initialScrollSession = createInitialScrollSession({ ++ completion: {}, ++ kind, ++ previousDataLength: 0 ++ }); ++ } else if (state.initialScrollSession.kind !== kind) { ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, ++ completion: state.initialScrollSession.completion, ++ kind, ++ previousDataLength: state.initialScrollSession.previousDataLength ++ }); + } -+ return true; ++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; ++ return state.initialScrollSession.completion; + } +-function getAlignItemsAtEndPadding(ctx) { +- const { state } = ctx; +- const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; +- return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; +-} +-function updateContentMetricsState(ctx) { +- const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; +- const nextPadding = getAlignItemsAtEndPadding(ctx); +- if (previousPadding !== nextPadding) { +- set$(ctx, "alignItemsAtEndPadding", nextPadding); +- } +-} +- +-// src/core/addTotalSize.ts +-function addTotalSize(ctx, key, add, notifyTotalSize = true) { +- const state = ctx.state; +- const prevTotalSize = state.totalSize; +- let totalSize = state.totalSize; +- if (key === null) { +- totalSize = add; +- if (state.timeoutSetPaddingTop) { +- clearTimeout(state.timeoutSetPaddingTop); +- state.timeoutSetPaddingTop = void 0; ++var initialScrollCompletion = { ++ didDispatchNativeScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); ++ }, ++ didRetrySilentInitialScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); ++ }, ++ markInitialScrollNativeDispatch(state) { ++ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; ++ }, ++ markSilentInitialScrollRetry(state) { ++ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; ++ }, ++ resetFlags(state) { ++ if (!state.initialScrollSession) { ++ return; + } +- } else { +- totalSize += add; ++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); ++ completion.didDispatchNativeScroll = void 0; ++ completion.didRetrySilentInitialScroll = void 0; + } +- if (prevTotalSize !== totalSize) { +- { +- state.pendingTotalSize = void 0; +- state.totalSize = totalSize; +- if (notifyTotalSize) { +- set$(ctx, "totalSize", totalSize); +- } +- updateContentMetricsState(ctx); +}; -+ -+// src/utils/edgeReachedGate.ts -+function resetEdgeLatch(ctx, edge) { ++var initialScrollWatchdog = { ++ clear(state) { ++ initialScrollWatchdog.set(state, void 0); ++ }, ++ didReachTarget(newScroll, watchdog) { ++ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); ++ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ get(state) { ++ var _a3, _b; ++ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; ++ }, ++ hasNonZeroTargetOffset(targetOffset) { ++ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ isAtZeroTargetOffset(targetOffset) { ++ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ set(state, watchdog) { ++ var _a3, _b; ++ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { ++ return; + } +- } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { +- set$(ctx, "totalSize", totalSize); ++ const completion = ensureInitialScrollSessionCompletion(state); ++ completion.watchdog = watchdog ? { ++ startScroll: watchdog.startScroll, ++ targetOffset: watchdog.targetOffset ++ } : void 0; + } +-} +- +-// src/core/setSize.ts +-function setSize(ctx, itemKey, size, notifyTotalSize = true) { +- const state = ctx.state; +- const { sizes } = state; +- const previousSize = sizes.get(itemKey); +- const diff = previousSize !== void 0 ? size - previousSize : size; +- if (diff !== 0) { +- addTotalSize(ctx, itemKey, diff, notifyTotalSize); ++}; ++function setInitialScrollSession(state, options = {}) { ++ var _a3, _b, _c, _d; ++ const existingSession = state.initialScrollSession; ++ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; ++ const completion = existingSession == null ? void 0 : existingSession.completion; ++ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; ++ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; ++ if (!kind) { ++ return clearInitialScrollSession(state); + } +- sizes.set(itemKey, size); +-} +- +-// src/utils/helpers.ts +-function isFunction(obj) { +- return typeof obj === "function"; +-} +-function isArray(obj) { +- return Array.isArray(obj); +-} +-var warned = /* @__PURE__ */ new Set(); +-function warnDevOnce(id, text) { +- if (IS_DEV && !warned.has(id)) { +- warned.add(id); +- console.warn(`[legend-list] ${text}`); ++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { ++ return clearInitialScrollSession(state); + } ++ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }); ++ return state.initialScrollSession; + } +-function roundSize(size) { +- return Math.floor(size * 8) / 8; +-} +-function isNullOrUndefined(value) { +- return value === null || value === void 0; +-} +-function getPadding(s, type) { +- var _a3, _b, _c; +- const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; +- return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; ++var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; ++function clearAdaptiveRenderExitTimeout(ctx) { ++ ctx.state.scheduledWork.cancel("adaptiveRender"); + } +-function extractPadding(style, contentContainerStyle, type) { +- return getPadding(style, type) + getPadding(contentContainerStyle, type); ++function scheduleAdaptiveRenderExit(ctx, exitDelay) { + const state = ctx.state; -+ if (edge === "start") { -+ state.isStartReached = false; -+ state.startReachedSnapshot = void 0; ++ clearAdaptiveRenderExitTimeout(ctx); ++ if (exitDelay <= 0) { ++ setAdaptiveRender(ctx, "normal", "scroll"); + } else { -+ state.isEndReached = false; -+ state.endReachedSnapshot = void 0; ++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); + } -+} -+function resetSharedEdgeGateIfOutsideHysteresis(ctx) { -+ const state = ctx.state; -+ if (!state.edgeReachedGate) { -+ return; -+ } -+ const contentSize = getContentSize(ctx); -+ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); -+ const isContentLess = contentSize < state.scrollLength; -+ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; -+ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; -+ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); -+ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); -+ if (isOutsideStart && isOutsideEnd) { -+ state.edgeReachedGate = void 0; -+ } -+} -+function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { -+ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; -+} -+function markReachedEdge(ctx) { -+ ctx.state.edgeReachedGate = "closed"; -+} -+function prepareReachedEdgeForNextUserScroll(ctx) { -+ if (ctx.state.edgeReachedGate) { -+ ctx.state.edgeReachedGate = "prepared"; -+ } -+} -+function beginReachedEdgeUserScroll(ctx, scrollDelta) { -+ const state = ctx.state; -+ if (state.edgeReachedGate !== "prepared") { -+ return void 0; -+ } -+ const allowedEdge = scrollDelta < 0 ? "start" : "end"; -+ state.edgeReachedGate = "closed"; -+ resetEdgeLatch(ctx, allowedEdge); -+ return allowedEdge; -+} -+ -+// src/utils/hasActiveInitialScroll.ts -+function hasActiveInitialScroll(state) { -+ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; -+} -+ -+// src/utils/checkAtBottom.ts -+function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -+ var _a3; + } +-function findContainerId(ctx, key) { ++function setAdaptiveRender(ctx, mode, reason) { + var _a3, _b; +- const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); +- if (directMatch !== void 0) { +- return directMatch; +- } +- const numContainers = peek$(ctx, "numContainers"); +- for (let i = 0; i < numContainers; i++) { +- const itemKey = peek$(ctx, `containerItemKey${i}`); +- if (itemKey === key) { +- return i; +- } ++ const previousMode = peek$(ctx, "adaptiveRender"); ++ if (previousMode !== mode) { ++ set$(ctx, "adaptiveRender", mode); ++ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); + } +- return -1; + } +- +-// src/utils/getItemSize.ts +-function getKnownOrFixedSize(ctx, key, index, data, resolved) { ++function resetAdaptiveRender(ctx) { + var _a3, _b; +- const state = ctx.state; +- const { getFixedItemSize, getItemType } = state.props; +- let size = key ? state.sizesKnown.get(key) : void 0; +- if (size === void 0 && key && getFixedItemSize) { +- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; +- const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); +- if (fixedSize !== void 0) { +- size = fixedSize + ctx.scrollAxisGap; +- state.sizesKnown.set(key, size); +- } ++ clearAdaptiveRenderExitTimeout(ctx); ++ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; ++ if (peek$(ctx, "adaptiveRender") !== mode) { ++ setAdaptiveRender(ctx, mode, "initial"); + } +- return size; +-} +-function getKnownOrFixedItemSize(ctx, index) { +- const key = getId(ctx.state, index); +- return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); + } +-function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { +- for (let index = startIndex; index <= endIndex; index++) { +- if (getKnownOrFixedItemSize(ctx, index) === void 0) { +- return false; ++function updateAdaptiveRender(ctx, scrollVelocity, options) { ++ var _a3, _b, _c; + const state = ctx.state; -+ if (!state) { -+ return; -+ } -+ const { -+ queuedInitialLayout, -+ scrollLength, -+ scroll, -+ maintainingScrollAtEnd, -+ props: { maintainScrollAtEndThreshold, onEndReachedThreshold } -+ } = state; -+ const contentSize = getContentSize(ctx); -+ resetSharedEdgeGateIfOutsideHysteresis(ctx); -+ if (contentSize > 0 && queuedInitialLayout) { -+ const insetEnd = getContentInsetEnd(ctx); -+ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; -+ const isContentLess = contentSize < scrollLength; -+ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); -+ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); -+ set$( -+ ctx, -+ "isWithinMaintainScrollAtEndThreshold", -+ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength -+ ); -+ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; -+ if (!shouldSkipThresholdChecks) { -+ state.isEndReached = checkThreshold( -+ distanceFromEnd, -+ isContentLess, -+ onEndReachedThreshold * scrollLength, -+ state.isEndReached, -+ state.endReachedSnapshot, -+ { -+ contentSize, -+ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, -+ scrollPosition: scroll -+ }, -+ (distance) => { -+ var _a4, _b; -+ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { -+ markReachedEdge(ctx); -+ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); -+ } -+ }, -+ (snapshot) => { -+ state.endReachedSnapshot = snapshot; ++ const adaptiveRender = state.props.adaptiveRender; ++ const currentMode = peek$(ctx, "adaptiveRender"); ++ if (peek$(ctx, "readyToRender")) { ++ if (adaptiveRender) { ++ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; ++ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; ++ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; ++ const threshold = currentMode === "light" ? exitVelocity : enterVelocity; ++ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; ++ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; ++ if (nextMode !== previousMode) { ++ if (nextMode === "light") { ++ setAdaptiveRender(ctx, "light", "scroll"); ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } else if (currentMode === "light") { ++ scheduleAdaptiveRenderExit(ctx, exitDelay); + } -+ ); -+ } -+ } ++ } ++ } else { ++ resetAdaptiveRender(ctx); + } + } +- return true; } - +-function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { +- var _a3, _b, _c, _d; ++ ++// src/core/doMaintainScrollAtEnd.ts ++function doMaintainScrollAtEnd(ctx) { + const state = ctx.state; + const { +- sizes, +- averageSizes, +- props: { estimatedItemSize, getItemType }, +- scrollingTo ++ didContainersLayout, ++ pendingNativeMVCPAdjust, ++ refScroller, ++ props: { maintainScrollAtEnd } + } = state; +- const sizeKnown = state.sizesKnown.get(key); +- if (sizeKnown !== void 0) { +- return sizeKnown; ++ const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); ++ const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout); ++ if (pendingNativeMVCPAdjust) { ++ state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd; ++ return false; + } +- let size; +- const renderedSize = sizes.get(key); +- if (preferCachedSize) { +- if (renderedSize !== void 0) { +- return renderedSize; +- } +- } +- size = getKnownOrFixedSize(ctx, key, index, data, resolved); +- if (size !== void 0) { +- setSize(ctx, key, size, notifyTotalSize); +- return size; +- } +- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; +- if (useAverageSize && !scrollingTo) { +- const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; +- if (averageSizeForType !== void 0) { +- size = roundSize(averageSizeForType); +- } +- } +- if (size === void 0 && renderedSize !== void 0) { +- return renderedSize; +- } +- if (size === void 0 && useAverageSize && scrollingTo) { +- const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; +- if (averageSizeForType !== void 0) { +- size = roundSize(averageSizeForType); +- } +- } +- if (size === void 0) { +- size = estimatedItemSize + ctx.scrollAxisGap; +- } +- setSize(ctx, key, size, notifyTotalSize); +- return size; +-} +-function getItemSizeAtIndex(ctx, index) { +- if (index === void 0 || index < 0) { +- return void 0; +- } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); +-} +- -// src/core/calculateOffsetWithOffsetPosition.ts -function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { - var _a3; @@ -5968,32 +6888,13 @@ index 95465f2..b028ddc 100644 - let offset = offsetParam; - if (viewOffset) { - offset -= viewOffset; -+// src/utils/checkAtTop.ts -+function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -+ const state = ctx == null ? void 0 : ctx.state; -+ if (!state) { -+ return; - } +- } - if (index !== void 0) { - const startOffsetAdjustment = getStartOffsetAdjustment(ctx); - if (startOffsetAdjustment) { - offset += startOffsetAdjustment; - } -+ const { -+ isStartReached, -+ props: { data, onStartReachedThreshold }, -+ scroll, -+ scrollLength, -+ startReachedSnapshot, -+ totalSize -+ } = state; -+ const dataLength = data.length; -+ const threshold = onStartReachedThreshold * scrollLength; -+ resetSharedEdgeGateIfOutsideHysteresis(ctx); -+ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { -+ state.isStartReached = false; -+ state.startReachedSnapshot = void 0; - } +- } - if (viewPosition !== void 0 && index !== void 0) { - const dataLength = state.props.data.length; - if (dataLength === 0) { @@ -6009,49 +6910,13 @@ index 95465f2..b028ddc 100644 - const footerSize = peek$(ctx, "footerSize") || 0; - offset += footerSize; - } -+ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); -+ set$(ctx, "isNearStart", scroll <= threshold); -+ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; -+ if (!shouldSkipThresholdChecks) { -+ state.isStartReached = checkThreshold( -+ scroll, -+ false, -+ threshold, -+ state.isStartReached, -+ startReachedSnapshot, -+ { -+ contentSize: totalSize, -+ dataLength, -+ scrollPosition: scroll -+ }, -+ (distance) => { -+ var _a3, _b; -+ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { -+ markReachedEdge(ctx); -+ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); -+ } -+ }, -+ (snapshot) => { -+ state.startReachedSnapshot = snapshot; -+ } -+ ); - } +- } - return offset; - } - +-} +- -// src/core/clampScrollOffset.ts -function clampScrollOffset(ctx, offset, scrollTarget) { -+// src/utils/checkThresholds.ts -+function checkThresholds(ctx, allowedEdge) { -+ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; -+ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+} -+ -+// src/core/recalculateSettledScroll.ts -+function recalculateSettledScroll(ctx) { -+ var _a3, _b; - const state = ctx.state; +- const state = ctx.state; - const contentSize = getContentSize(ctx); - let clampedOffset = offset; - if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { @@ -6060,217 +6925,176 @@ index 95465f2..b028ddc 100644 - const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; - const maxOffset = baseMaxOffset + extraEndOffset; - clampedOffset = Math.min(offset, maxOffset); -+ if ((_a3 = state.props) == null ? void 0 : _a3.data) { -+ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); - } +- } - clampedOffset = Math.max(0, clampedOffset); - return clampedOffset; -+ checkThresholds(ctx); - } - - // src/core/finishScrollTo.ts -@@ -1392,16 +1008,191 @@ function listenForScrollEnd(ctx, params) { - if (idleTimeout !== void 0) { - clearTimeout(idleTimeout); - } -- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); -- }; -- const onScrollEnd = () => finish("scrollend"); -- target.addEventListener("scroll", onScroll2); -- if (supportsScrollEnd) { -- target.addEventListener("scrollend", onScrollEnd); -- } else { -- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); -+ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); -+ }; -+ const onScrollEnd = () => finish("scrollend"); -+ target.addEventListener("scroll", onScroll2); -+ if (supportsScrollEnd) { -+ target.addEventListener("scrollend", onScrollEnd); -+ } else { -+ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); -+ } -+ scheduledWork.register("platformScrollCompletion", cancel); -+} -+ -+// src/core/initialScrollSession.ts -+var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -+function hasInitialScrollSessionCompletion(completion) { -+ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); -+} -+function clearInitialScrollSession(state) { -+ state.initialScrollSession = void 0; -+ return void 0; -+} -+function createInitialScrollSession(options) { -+ const { bootstrap, completion, kind, previousDataLength } = options; -+ return kind === "offset" ? { -+ completion, -+ kind, -+ previousDataLength -+ } : { -+ bootstrap, -+ completion, -+ kind, -+ previousDataLength -+ }; -+} -+function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { -+ var _a4, _b2; -+ if (!state.initialScrollSession) { -+ state.initialScrollSession = createInitialScrollSession({ -+ completion: {}, -+ kind, -+ previousDataLength: 0 -+ }); -+ } else if (state.initialScrollSession.kind !== kind) { -+ state.initialScrollSession = createInitialScrollSession({ -+ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, -+ completion: state.initialScrollSession.completion, -+ kind, -+ previousDataLength: state.initialScrollSession.previousDataLength -+ }); -+ } -+ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; -+ return state.initialScrollSession.completion; -+} -+var initialScrollCompletion = { -+ didDispatchNativeScroll(state) { -+ var _a3, _b; -+ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); -+ }, -+ didRetrySilentInitialScroll(state) { -+ var _a3, _b; -+ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); -+ }, -+ markInitialScrollNativeDispatch(state) { -+ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; -+ }, -+ markSilentInitialScrollRetry(state) { -+ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; -+ }, -+ resetFlags(state) { -+ if (!state.initialScrollSession) { -+ return; -+ } -+ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); -+ completion.didDispatchNativeScroll = void 0; -+ completion.didRetrySilentInitialScroll = void 0; -+ } -+}; -+var initialScrollWatchdog = { -+ clear(state) { -+ initialScrollWatchdog.set(state, void 0); -+ }, -+ didReachTarget(newScroll, watchdog) { -+ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); -+ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ get(state) { -+ var _a3, _b; -+ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; -+ }, -+ hasNonZeroTargetOffset(targetOffset) { -+ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ isAtZeroTargetOffset(targetOffset) { -+ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ set(state, watchdog) { -+ var _a3, _b; -+ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { -+ return; -+ } -+ const completion = ensureInitialScrollSessionCompletion(state); -+ completion.watchdog = watchdog ? { -+ startScroll: watchdog.startScroll, -+ targetOffset: watchdog.targetOffset -+ } : void 0; -+ } -+}; -+function setInitialScrollSession(state, options = {}) { -+ var _a3, _b, _c, _d; -+ const existingSession = state.initialScrollSession; -+ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; -+ const completion = existingSession == null ? void 0 : existingSession.completion; -+ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; -+ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; -+ if (!kind) { -+ return clearInitialScrollSession(state); -+ } -+ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { -+ return clearInitialScrollSession(state); -+ } -+ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; -+ state.initialScrollSession = createInitialScrollSession({ -+ bootstrap, -+ completion, -+ kind, -+ previousDataLength -+ }); -+ return state.initialScrollSession; -+} -+var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; -+var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; -+var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; -+function clearAdaptiveRenderExitTimeout(ctx) { -+ ctx.state.scheduledWork.cancel("adaptiveRender"); -+} -+function scheduleAdaptiveRenderExit(ctx, exitDelay) { -+ const state = ctx.state; -+ clearAdaptiveRenderExitTimeout(ctx); -+ if (exitDelay <= 0) { -+ setAdaptiveRender(ctx, "normal", "scroll"); -+ } else { -+ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); -+ } -+} -+function setAdaptiveRender(ctx, mode, reason) { -+ var _a3, _b; -+ const previousMode = peek$(ctx, "adaptiveRender"); -+ if (previousMode !== mode) { -+ set$(ctx, "adaptiveRender", mode); -+ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); -+ } -+} -+function resetAdaptiveRender(ctx) { -+ var _a3, _b; -+ clearAdaptiveRenderExitTimeout(ctx); -+ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; -+ if (peek$(ctx, "adaptiveRender") !== mode) { -+ setAdaptiveRender(ctx, mode, "initial"); -+ } -+} -+function updateAdaptiveRender(ctx, scrollVelocity, options) { -+ var _a3, _b, _c; -+ const state = ctx.state; -+ const adaptiveRender = state.props.adaptiveRender; -+ const currentMode = peek$(ctx, "adaptiveRender"); -+ if (peek$(ctx, "readyToRender")) { -+ if (adaptiveRender) { -+ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; -+ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; -+ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; -+ const threshold = currentMode === "light" ? exitVelocity : enterVelocity; -+ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; -+ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; -+ if (nextMode !== previousMode) { -+ if (nextMode === "light") { -+ setAdaptiveRender(ctx, "light", "scroll"); -+ scheduleAdaptiveRenderExit(ctx, exitDelay); -+ } else if (currentMode === "light") { -+ scheduleAdaptiveRenderExit(ctx, exitDelay); -+ } -+ } -+ } else { -+ resetAdaptiveRender(ctx); -+ } - } +-} +- +-// src/core/finishScrollTo.ts +-function finishScrollTo(ctx) { +- var _a3, _b; +- const state = ctx.state; +- if (state == null ? void 0 : state.scrollingTo) { +- cancelScrollCompletionChecks(state); +- const resolvePendingScroll = state.pendingScrollResolve; +- state.pendingScrollResolve = void 0; +- const scrollingTo = state.scrollingTo; +- state.scrollHistory.length = 0; +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- if (state.pendingTotalSize !== void 0) { +- addTotalSize(ctx, null, state.pendingTotalSize); +- } +- { +- state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); +- } +- if (scrollingTo.isInitialScroll || state.initialScroll) { +- const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; +- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; +- finishInitialScroll(ctx, { +- onFinished: () => { +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +- }, +- preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, +- recalculateItems: true, +- schedulePreservedTargetClear: shouldPreserveResizeTarget, +- syncObservedOffset: isOffsetSession, +- waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame +- }); +- return; +- } +- recalculateSettledScroll(ctx); +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +- } +-} +- +-// src/core/doScrollTo.ts +-var SCROLL_END_IDLE_MS = 80; +-var SCROLL_END_MAX_MS = 1500; +-var SMOOTH_SCROLL_DURATION_MS = 320; +-var SCROLL_END_TARGET_EPSILON = 1; +-function doScrollTo(ctx, params) { +- var _a3, _b; +- const state = ctx.state; +- const { animated, horizontal, offset } = params; +- state.scheduledWork.cancel("platformScrollCompletion"); +- const scroller = state.refScroller.current; +- const node = scroller == null ? void 0 : scroller.getScrollableNode(); +- if (!scroller || !node) { +- return; +- } +- const isAnimated = !!animated; +- const isHorizontal = !!horizontal; +- const contentSize = isHorizontal ? getContentSize(ctx) : void 0; +- const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; +- const top = isHorizontal ? 0 : offset; +- scroller.scrollTo({ animated: isAnimated, x: left, y: top }); +- if (isAnimated) { +- const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; +- listenForScrollEnd(ctx, { +- readOffset: () => scroller.getCurrentScrollOffset(), +- target, +- targetOffset: offset +- }); +- } else { +- state.scroll = offset; +- const targetToken = state.scrollingTo; +- state.scheduledWork.timeout( +- () => { +- if (targetToken === state.scrollingTo) { +- finishScrollTo(ctx); +- } +- }, +- 100, +- "platformScrollCompletion" +- ); +- } +-} +-function listenForScrollEnd(ctx, params) { +- const { readOffset, target, targetOffset } = params; +- if (!target) { +- finishScrollTo(ctx); +- return; +- } +- const supportsScrollEnd = "onscrollend" in target; +- let idleTimeout; +- let settled = false; +- const { scheduledWork, scrollingTo: targetToken } = ctx.state; +- const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); +- const cleanup = () => { +- target.removeEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.removeEventListener("scrollend", onScrollEnd); +- } +- if (idleTimeout !== void 0) { +- clearTimeout(idleTimeout); +- } +- clearTimeout(maxTimeout); +- }; +- const cancel = () => { +- if (!settled) { +- settled = true; +- cleanup(); +- } +- }; +- const finish = (reason) => { +- if (settled) return; +- if (targetToken !== ctx.state.scrollingTo) { +- scheduledWork.cancel("platformScrollCompletion"); +- return; +- } +- const currentOffset = readOffset(); +- const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; +- if (reason === "scrollend" && !isNearTarget) { +- return; +- } +- scheduledWork.cancel("platformScrollCompletion"); +- finishScrollTo(ctx); +- }; +- const onScroll2 = () => { +- if (idleTimeout !== void 0) { +- clearTimeout(idleTimeout); +- } +- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); +- }; +- const onScrollEnd = () => finish("scrollend"); +- target.addEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.addEventListener("scrollend", onScrollEnd); +- } else { +- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); +- } - scheduledWork.register("platformScrollCompletion", cancel); - } - - // src/core/doMaintainScrollAtEnd.ts -@@ -1430,6 +1221,7 @@ function doMaintainScrollAtEnd(ctx) { +-} +- +-// src/core/doMaintainScrollAtEnd.ts +-function doMaintainScrollAtEnd(ctx) { +- const state = ctx.state; +- const { +- didContainersLayout, +- pendingNativeMVCPAdjust, +- refScroller, +- props: { maintainScrollAtEnd } +- } = state; +- const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); +- const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout); +- if (pendingNativeMVCPAdjust) { +- state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd; +- return false; +- } +- if (shouldMaintainScrollAtEnd) { +- state.pendingMaintainScrollAtEnd = false; +- const contentSize = getContentSize(ctx); +- if (contentSize < state.scrollLength) { +- state.scroll = 0; ++ if (shouldMaintainScrollAtEnd) { ++ state.pendingMaintainScrollAtEnd = false; ++ const contentSize = getContentSize(ctx); ++ if (contentSize < state.scrollLength) { ++ state.scroll = 0; + } + if (!state.maintainingScrollAtEnd) { + const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; state.maintainingScrollAtEnd = pendingState; @@ -6278,10 +7102,27 @@ index 95465f2..b028ddc 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1788,6 +1580,27 @@ function prepareMVCP(ctx, dataChanged) { +@@ -1788,23 +1580,44 @@ function prepareMVCP(ctx, dataChanged) { } } +-// src/utils/getScrollVelocity.ts +-var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; +-var SCROLL_VELOCITY_HALF_LIFE_MS = 200; +-var getScrollVelocity = (state) => { +- const { scrollHistory } = state; +- const newestIndex = scrollHistory.length - 1; +- if (newestIndex < 1) { +- return 0; +- } +- const newest = scrollHistory[newestIndex]; +- if (Date.now() - newest.time > MAX_SCROLL_VELOCITY_WINDOW_MS) { +- return 0; +- } +- let direction = 0; +- let weightedVelocity = 0; +- let totalWeight = 0; +- for (let i = newestIndex; i > 0; i--) { +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { @@ -6303,43 +7144,243 @@ index 95465f2..b028ddc 100644 + }, "fullDrawDistancePrewarm"); +} + - // src/utils/getScrollVelocity.ts - var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; - var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -1885,6 +1698,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { - state.scrollPrevTime = state.scrollTime; - state.scroll = newScroll; - state.scrollTime = currentTime; -+ if ((options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust) { -+ releaseScrollTargetSettleIfMoved(ctx); -+ } - const scrollDelta = Math.abs(newScroll - prevScroll); - const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; - const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; -@@ -2005,99 +1821,417 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { - if (start === void 0) { - return void 0; - } -- if (targetIndex !== void 0 && state.positions[start] === void 0) { -- return { end: start, start }; -+ if (targetIndex !== void 0 && state.positions[start] === void 0) { -+ return { end: start, start }; -+ } -+ if (targetIndex === void 0) { -+ const startBottom = getItemBottom(ctx, start); -+ if (startBottom === void 0 || startBottom <= viewportStart) { -+ return void 0; -+ } ++// src/utils/getScrollVelocity.ts ++var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; ++var SCROLL_VELOCITY_HALF_LIFE_MS = 200; ++var getScrollVelocity = (state) => { ++ const { scrollHistory } = state; ++ const newestIndex = scrollHistory.length - 1; ++ if (newestIndex < 1) { ++ return 0; + } -+ while (start > 0) { -+ const top = state.positions[start]; -+ if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { -+ break; -+ } -+ start--; ++ const newest = scrollHistory[newestIndex]; ++ if (Date.now() - newest.time > MAX_SCROLL_VELOCITY_WINDOW_MS) { ++ return 0; + } -+ while (start > 0) { -+ const previousBottom = getItemBottom(ctx, start - 1); ++ let direction = 0; ++ let weightedVelocity = 0; ++ let totalWeight = 0; ++ for (let i = newestIndex; i > 0; i--) { + const current = scrollHistory[i]; + const previous = scrollHistory[i - 1]; + const scrollDiff = current.scroll - previous.scroll; +@@ -1823,281 +1636,604 @@ var getScrollVelocity = (state) => { + if (scrollDiff === 0 || timeDiff <= 0) { + continue; + } +- const age = newest.time - current.time; +- const weight = Math.exp(-age / SCROLL_VELOCITY_HALF_LIFE_MS); +- weightedVelocity += scrollDiff / timeDiff * weight; +- totalWeight += weight; +- } +- return totalWeight > 0 ? weightedVelocity / totalWeight : 0; +-}; +- +-// src/utils/hasActiveMVCPAnchorLock.ts +-function hasActiveMVCPAnchorLock(state) { +- const lock = state.mvcpAnchorLock; +- if (!lock) { +- return false; ++ const age = newest.time - current.time; ++ const weight = Math.exp(-age / SCROLL_VELOCITY_HALF_LIFE_MS); ++ weightedVelocity += scrollDiff / timeDiff * weight; ++ totalWeight += weight; ++ } ++ return totalWeight > 0 ? weightedVelocity / totalWeight : 0; ++}; ++ ++// src/utils/hasActiveMVCPAnchorLock.ts ++function hasActiveMVCPAnchorLock(state) { ++ const lock = state.mvcpAnchorLock; ++ if (!lock) { ++ return false; ++ } ++ if (Date.now() > lock.expiresAt) { ++ state.mvcpAnchorLock = void 0; ++ return false; ++ } ++ return true; ++} ++ ++// src/utils/isInMVCPActiveMode.ts ++function isInMVCPActiveMode(state) { ++ return state.dataChangeNeedsScrollUpdate || hasActiveMVCPAnchorLock(state); ++} ++ ++// src/core/updateScroll.ts ++function updateScroll(ctx, newScroll, forceUpdate, options) { ++ var _a3; ++ const state = ctx.state; ++ const { ignoreScrollFromMVCP, lastScrollAdjustForHistory, scrollAdjustHandler, scrollHistory, scrollingTo } = state; ++ const prevScroll = state.scroll; ++ if ((options == null ? void 0 : options.markHasScrolled) !== false) { ++ state.hasScrolled = true; ++ } ++ const currentTime = Date.now(); ++ state.lastBatchingAction = currentTime; ++ const adjust = scrollAdjustHandler.getAdjust(); ++ const adjustChanged = lastScrollAdjustForHistory !== void 0 && Math.abs(adjust - lastScrollAdjustForHistory) > 0.1; ++ if (adjustChanged) { ++ scrollHistory.length = 0; ++ } ++ state.lastScrollAdjustForHistory = adjust; ++ if (scrollingTo === void 0 && !(scrollHistory.length === 0 && newScroll === state.scroll)) { ++ if (!adjustChanged) { ++ scrollHistory.push({ scroll: newScroll, time: currentTime }); ++ } ++ } ++ if (scrollHistory.length > 5) { ++ scrollHistory.shift(); ++ } ++ if (ignoreScrollFromMVCP && !scrollingTo) { ++ const { lt, gt } = ignoreScrollFromMVCP; ++ if (lt && newScroll < lt || gt && newScroll > gt) { ++ state.ignoreScrollFromMVCPIgnored = true; ++ return; ++ } ++ } ++ state.scrollPrev = prevScroll; ++ state.scrollPrevTime = state.scrollTime; ++ state.scroll = newScroll; ++ state.scrollTime = currentTime; ++ const scrollDelta = Math.abs(newScroll - prevScroll); ++ const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; ++ const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; ++ const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); ++ const scrollLength = state.scrollLength; ++ const isLargeUserScrollJump = scrollLength > 0 && scrollingTo === void 0 && scrollDelta > scrollLength && !state.pendingNativeMVCPAdjust; ++ const scrollVelocity = getScrollVelocity(state); ++ updateAdaptiveRender(ctx, scrollVelocity, { forceLight: isLargeUserScrollJump }); ++ const lastCalculated = state.scrollLastCalculate; ++ const useAggressiveItemRecalculation = isInMVCPActiveMode(state); ++ const shouldUpdate = useAggressiveItemRecalculation || didResolvePendingNativeMVCPAdjust || allowedEdge !== void 0 || forceUpdate || lastCalculated === void 0 || Math.abs(state.scroll - lastCalculated) > 2; ++ if (shouldUpdate) { ++ state.scrollLastCalculate = state.scroll; ++ state.ignoreScrollFromMVCPIgnored = false; ++ state.lastScrollDelta = scrollDelta; ++ const runCalculateItems = () => { ++ var _a4; ++ const calculateItemsParams = { ++ doMVCP: scrollingTo !== void 0, ++ scrollVelocity ++ }; ++ if (isLargeUserScrollJump) { ++ calculateItemsParams.drawDistanceMode = "visible-first"; ++ } ++ (_a4 = state.triggerCalculateItemsInView) == null ? void 0 : _a4.call(state, calculateItemsParams); ++ checkThresholds(ctx, allowedEdge); ++ }; ++ if (isLargeUserScrollJump) { ++ state.mvcpAnchorLock = void 0; ++ state.pendingNativeMVCPAdjust = void 0; ++ state.userScrollAnchorReset = { keys: /* @__PURE__ */ new Set() }; ++ state.scheduledWork.cancel("mvcpRecalculate"); ++ flushSync(runCalculateItems); ++ scheduleFullDrawDistancePrewarm(ctx); ++ } else { ++ runCalculateItems(); ++ } ++ const shouldMaintainScrollAtEndAfterPendingSettle = !!state.pendingMaintainScrollAtEnd || !!((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onDataChange); ++ if (didResolvePendingNativeMVCPAdjust && shouldMaintainScrollAtEndAfterPendingSettle) { ++ state.pendingMaintainScrollAtEnd = false; ++ doMaintainScrollAtEnd(ctx); ++ } ++ state.dataChangeNeedsScrollUpdate = false; ++ state.lastScrollDelta = 0; ++ } ++} ++ ++// src/core/scrollTo.ts ++function getAverageSizeSnapshot(state) { ++ if (Object.keys(state.averageSizes).length === 0) { ++ return void 0; ++ } ++ const snapshot = {}; ++ for (const itemType in state.averageSizes) { ++ const averages = state.averageSizes[itemType]; ++ snapshot[itemType] = averages.avg; ++ } ++ return snapshot; ++} ++function syncInitialScrollNativeWatchdog(state, options) { ++ var _a3; ++ const { isInitialScroll, requestedOffset, targetOffset } = options; ++ const existingWatchdog = initialScrollWatchdog.get(state); ++ const shouldWatchInitialNativeScroll = !state.didFinishInitialScroll && (isInitialScroll || !!existingWatchdog) && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); ++ const shouldClearInitialNativeScrollWatchdog = !state.didFinishInitialScroll && !!existingWatchdog && initialScrollWatchdog.isAtZeroTargetOffset(requestedOffset); ++ if (shouldWatchInitialNativeScroll) { ++ state.hasScrolled = false; ++ initialScrollWatchdog.set(state, { ++ startScroll: (_a3 = existingWatchdog == null ? void 0 : existingWatchdog.startScroll) != null ? _a3 : state.scroll, ++ targetOffset ++ }); ++ return; ++ } ++ if (shouldClearInitialNativeScrollWatchdog) { ++ initialScrollWatchdog.clear(state); ++ } ++} ++function findPositionIndexAtOrBeforeOffset(ctx, offset) { ++ const state = ctx.state; ++ const dataLength = state.props.data.length; ++ let low = 0; ++ let high = dataLength - 1; ++ let match; ++ while (low <= high) { ++ const mid = Math.floor((low + high) / 2); ++ const top = state.positions[mid]; ++ if (top === void 0) { ++ high = mid - 1; ++ } else { ++ if (top <= offset) { ++ match = mid; ++ low = mid + 1; ++ } else { ++ high = mid - 1; ++ } ++ } ++ } ++ return match; ++} ++function getItemBottom(ctx, index) { ++ var _a3; ++ const top = ctx.state.positions[index]; ++ if (top === void 0) { ++ return void 0; ++ } ++ const itemSize = (_a3 = getItemSizeAtIndex(ctx, index)) != null ? _a3 : 0; ++ return top + (Number.isFinite(itemSize) ? itemSize : 0); ++} ++function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { ++ const state = ctx.state; ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return void 0; ++ } ++ const viewportStart = Math.max(0, targetOffset); ++ const viewportEnd = Math.max(viewportStart, targetOffset + state.scrollLength); ++ let start = targetIndex !== void 0 ? Math.max(0, Math.min(dataLength - 1, targetIndex)) : findPositionIndexAtOrBeforeOffset(ctx, viewportStart); ++ if (start === void 0) { ++ return void 0; ++ } ++ if (targetIndex !== void 0 && state.positions[start] === void 0) { ++ return { end: start, start }; ++ } ++ if (targetIndex === void 0) { ++ const startBottom = getItemBottom(ctx, start); ++ if (startBottom === void 0 || startBottom <= viewportStart) { ++ return void 0; ++ } ++ } ++ while (start > 0) { ++ const top = state.positions[start]; ++ if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { ++ break; ++ } ++ start--; ++ } ++ while (start > 0) { ++ const previousBottom = getItemBottom(ctx, start - 1); + if (previousBottom === void 0 || previousBottom <= viewportStart) { + break; + } @@ -6418,23 +7459,36 @@ index 95465f2..b028ddc 100644 + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); + } -+ } + } +- if (Date.now() > lock.expiresAt) { +- state.mvcpAnchorLock = void 0; +- return false; + if (forceScroll || !isInitialScroll || Platform.OS === "android") { + doScrollTo(ctx, { animated, horizontal, offset }); + } else { + state.scroll = offset; -+ } -+} -+ + } +- return true; + } + +-// src/utils/isInMVCPActiveMode.ts +-function isInMVCPActiveMode(state) { +- return state.dataChangeNeedsScrollUpdate || hasActiveMVCPAnchorLock(state); +// src/core/scrollTargetSettle.ts +var SETTLE_POSITION_EPSILON = 0.5; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; +var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; ++function releaseScrollTargetForUserInteraction(ctx) { ++ if (ctx.state.scrollTargetSettle) { ++ clearScrollTargetSettle(ctx.state); ++ } ++} +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; + state.scheduledWork.cancel("scrollTargetSettle"); ++ state.scheduledWork.cancel("scrollTargetSettleDeadline"); +} +function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; @@ -6451,35 +7505,56 @@ index 95465f2..b028ddc 100644 + deadline: now + SETTLE_MAX_MS, + expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), ++ measuredIndex: void 0, + quietPasses: 0, -+ requestedAt: now, + viewOffset, + viewPosition + }; -+} ++ state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline"); + } +- +-// src/core/updateScroll.ts +-function updateScroll(ctx, newScroll, forceUpdate, options) { +function getSettleTargetOffset(ctx, settle, index, position) { + const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; + return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} +function applyScrollTargetCorrection(ctx, id) { -+ var _a3; -+ const state = ctx.state; + var _a3; + const state = ctx.state; +- const { ignoreScrollFromMVCP, lastScrollAdjustForHistory, scrollAdjustHandler, scrollHistory, scrollingTo } = state; +- const prevScroll = state.scroll; +- if ((options == null ? void 0 : options.markHasScrolled) !== false) { +- state.hasScrolled = true; + const settle = state.scrollTargetSettle; + if (!settle || settle.id !== id) { + return; -+ } + } +- const currentTime = Date.now(); +- state.lastBatchingAction = currentTime; +- const adjust = scrollAdjustHandler.getAdjust(); +- const adjustChanged = lastScrollAdjustForHistory !== void 0 && Math.abs(adjust - lastScrollAdjustForHistory) > 0.1; +- if (adjustChanged) { +- scrollHistory.length = 0; + const index = state.indexByKey.get(id); + const position = index === void 0 ? void 0 : state.positions[index]; + if (index === void 0 || position === void 0 || Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return; -+ } + } +- state.lastScrollAdjustForHistory = adjust; +- if (scrollingTo === void 0 && !(scrollHistory.length === 0 && newScroll === state.scroll)) { +- if (!adjustChanged) { +- scrollHistory.push({ scroll: newScroll, time: currentTime }); +- } + if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { + clearScrollTargetSettle(state); + return; -+ } + } +- if (scrollHistory.length > 5) { +- scrollHistory.shift(); + settle.corrections++; -+ settle.requestedAt = Date.now(); ++ settle.measuredIndex = void 0; + scrollTo(ctx, { + animated: false, + index, @@ -6494,21 +7569,71 @@ index 95465f2..b028ddc 100644 + }); + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +} -+function settleScrollTarget(ctx, isCompensating) { ++function settleScrollTarget(ctx, options) { ++ var _a3; + const state = ctx.state; + const settle = state.scrollTargetSettle; + if (!settle) { + return false; + } +- if (ignoreScrollFromMVCP && !scrollingTo) { +- const { lt, gt } = ignoreScrollFromMVCP; +- if (lt && newScroll < lt || gt && newScroll > gt) { +- state.ignoreScrollFromMVCPIgnored = true; +- return; ++ const measured = options == null ? void 0 : options.minIndexSizeChanged; ++ if (measured !== void 0) { ++ settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); + } -+ if (isCompensating) { ++ if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return false; -+ } + } + settle.quietPasses = 0; + settle.expiresAt = Date.now() + SETTLE_TTL_MS; + return false; -+ } + } +- state.scrollPrev = prevScroll; +- state.scrollPrevTime = state.scrollTime; +- state.scroll = newScroll; +- state.scrollTime = currentTime; +- const scrollDelta = Math.abs(newScroll - prevScroll); +- const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; +- const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; +- const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); +- const scrollLength = state.scrollLength; +- const isLargeUserScrollJump = scrollLength > 0 && scrollingTo === void 0 && scrollDelta > scrollLength && !state.pendingNativeMVCPAdjust; +- const scrollVelocity = getScrollVelocity(state); +- updateAdaptiveRender(ctx, scrollVelocity, { forceLight: isLargeUserScrollJump }); +- const lastCalculated = state.scrollLastCalculate; +- const useAggressiveItemRecalculation = isInMVCPActiveMode(state); +- const shouldUpdate = useAggressiveItemRecalculation || didResolvePendingNativeMVCPAdjust || allowedEdge !== void 0 || forceUpdate || lastCalculated === void 0 || Math.abs(state.scroll - lastCalculated) > 2; +- if (shouldUpdate) { +- state.scrollLastCalculate = state.scroll; +- state.ignoreScrollFromMVCPIgnored = false; +- state.lastScrollDelta = scrollDelta; +- const runCalculateItems = () => { +- var _a4; +- const calculateItemsParams = { +- doMVCP: scrollingTo !== void 0, +- scrollVelocity +- }; +- if (isLargeUserScrollJump) { +- calculateItemsParams.drawDistanceMode = "visible-first"; +- } +- (_a4 = state.triggerCalculateItemsInView) == null ? void 0 : _a4.call(state, calculateItemsParams); +- checkThresholds(ctx, allowedEdge); +- }; +- if (isLargeUserScrollJump) { +- state.mvcpAnchorLock = void 0; +- state.pendingNativeMVCPAdjust = void 0; +- state.userScrollAnchorReset = { keys: /* @__PURE__ */ new Set() }; +- state.scheduledWork.cancel("mvcpRecalculate"); +- flushSync(runCalculateItems); +- scheduleFullDrawDistancePrewarm(ctx); +- } else { +- runCalculateItems(); + const index = state.indexByKey.get(settle.id); + const position = index === void 0 ? void 0 : state.positions[index]; + if (index === void 0 || position === void 0) { @@ -6520,13 +7645,21 @@ index 95465f2..b028ddc 100644 + clearScrollTargetSettle(state); + return false; + } ++ if (settle.measuredIndex === void 0 || settle.measuredIndex > index) { ++ return false; ++ } + const targetOffset = getSettleTargetOffset(ctx, settle, index, position); + const diff = targetOffset - state.scroll; + if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ settle.measuredIndex = void 0; + settle.quietPasses++; + if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { + clearScrollTargetSettle(state); -+ } + } +- const shouldMaintainScrollAtEndAfterPendingSettle = !!state.pendingMaintainScrollAtEnd || !!((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onDataChange); +- if (didResolvePendingNativeMVCPAdjust && shouldMaintainScrollAtEndAfterPendingSettle) { +- state.pendingMaintainScrollAtEnd = false; +- doMaintainScrollAtEnd(ctx); + return false; + } + settle.quietPasses = 0; @@ -6534,17 +7667,6 @@ index 95465f2..b028ddc 100644 + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} -+var SETTLE_ECHO_WINDOW_MS = 150; -+function releaseScrollTargetSettleIfMoved(ctx, _scrollOffset) { -+ const state = ctx.state; -+ const settle = state.scrollTargetSettle; -+ if (!settle) { -+ return; -+ } -+ if (Date.now() - settle.requestedAt > SETTLE_ECHO_WINDOW_MS) { -+ clearScrollTargetSettle(state); -+ } -+} + +// src/core/cancelImperativeScroll.ts +function cancelScrollCompletionChecks({ scheduledWork }) { @@ -6576,7 +7698,9 @@ index 95465f2..b028ddc 100644 + nativeEvent: { + ...event.nativeEvent, + contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -+ } + } +- state.dataChangeNeedsScrollUpdate = false; +- state.lastScrollDelta = 0; + }; +} +function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { @@ -6593,9 +7717,13 @@ index 95465f2..b028ddc 100644 + (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 + ) + ); -+ } -+} -+ + } + } + +-// src/core/scrollTo.ts +-function getAverageSizeSnapshot(state) { +- if (Object.keys(state.averageSizes).length === 0) { +- return void 0; +// src/utils/setInitialRenderState.ts +function resetInitialRenderState(ctx, { + resetLayout, @@ -6605,18 +7733,31 @@ index 95465f2..b028ddc 100644 + if (resetLayout) { + state.didContainersLayout = false; + state.queuedInitialLayout = false; -+ } + } +- const snapshot = {}; +- for (const itemType in state.averageSizes) { +- const averages = state.averageSizes[itemType]; +- snapshot[itemType] = averages.avg; + if (resetInitialScroll) { + state.didFinishInitialScroll = false; } -- if (targetIndex === void 0) { -- const startBottom = getItemBottom(ctx, start); -- if (startBottom === void 0 || startBottom <= viewportStart) { -- return void 0; -- } +- return snapshot; + set$(ctx, "readyToRender", false); + resetAdaptiveRender(ctx); -+} + } +-function syncInitialScrollNativeWatchdog(state, options) { +- var _a3; +- const { isInitialScroll, requestedOffset, targetOffset } = options; +- const existingWatchdog = initialScrollWatchdog.get(state); +- const shouldWatchInitialNativeScroll = !state.didFinishInitialScroll && (isInitialScroll || !!existingWatchdog) && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); +- const shouldClearInitialNativeScrollWatchdog = !state.didFinishInitialScroll && !!existingWatchdog && initialScrollWatchdog.isAtZeroTargetOffset(requestedOffset); +- if (shouldWatchInitialNativeScroll) { +- state.hasScrolled = false; +- initialScrollWatchdog.set(state, { +- startScroll: (_a3 = existingWatchdog == null ? void 0 : existingWatchdog.startScroll) != null ? _a3 : state.scroll, +- targetOffset +- }); +- return; +function setInitialRenderState(ctx, { + didLayout, + didInitialScroll @@ -6629,51 +7770,53 @@ index 95465f2..b028ddc 100644 + if (didLayout) { + state.didContainersLayout = true; } -- while (start > 0) { -- const top = state.positions[start]; -- if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { -- break; -- } -- start--; +- if (shouldClearInitialNativeScrollWatchdog) { +- initialScrollWatchdog.clear(state); + if (didInitialScroll) { + state.didFinishInitialScroll = true; } -- while (start > 0) { -- const previousBottom = getItemBottom(ctx, start - 1); -- if (previousBottom === void 0 || previousBottom <= viewportStart) { -- break; +-} +-function findPositionIndexAtOrBeforeOffset(ctx, offset) { +- const state = ctx.state; +- const dataLength = state.props.data.length; +- let low = 0; +- let high = dataLength - 1; +- let match; +- while (low <= high) { +- const mid = Math.floor((low + high) / 2); +- const top = state.positions[mid]; +- if (top === void 0) { +- high = mid - 1; +- } else { +- if (top <= offset) { +- match = mid; +- low = mid + 1; +- } else { +- high = mid - 1; + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); + if (isReadyToRender && !peek$(ctx, "readyToRender")) { + set$(ctx, "readyToRender", true); + setAdaptiveRender(ctx, "normal", "ready"); + if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { + scheduleFullDrawDistancePrewarm(ctx); - } -- start--; -- } -- let end = start; -- while (end + 1 < dataLength) { -- const nextTop = state.positions[end + 1]; -- if (nextTop === void 0 || nextTop > viewportEnd) { -- break; ++ } + if (!state.didLoad) { + state.didLoad = true; + if (onLoad) { + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); -+ } + } } -- end++; } -- return { end, start }; +- return match; } --function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -- if (range) { -- ctx.state.scrollTargetPinnedRange = range; -- ctx.state.scrollForNextCalculateItemsInView = void 0; -- } else { -- ctx.state.scrollTargetPinnedRange = void 0; +-function getItemBottom(ctx, index) { +- var _a3; +- const top = ctx.state.positions[index]; +- if (top === void 0) { +- return void 0; - } +- const itemSize = (_a3 = getItemSizeAtIndex(ctx, index)) != null ? _a3 : 0; +- return top + (Number.isFinite(itemSize) ? itemSize : 0); + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -6682,8 +7825,7 @@ index 95465f2..b028ddc 100644 + state.scrollPending = offset; + state.scrollPrev = offset; } --function scrollTo(ctx, params) { -- var _a3, _b; +-function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -6694,6 +7836,74 @@ index 95465f2..b028ddc 100644 + setInitialScrollSession(state); +} +function supersedeInitialScroll(ctx) { ++ var _a3, _b, _c; + const state = ctx.state; +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return void 0; +- } +- const viewportStart = Math.max(0, targetOffset); +- const viewportEnd = Math.max(viewportStart, targetOffset + state.scrollLength); +- let start = targetIndex !== void 0 ? Math.max(0, Math.min(dataLength - 1, targetIndex)) : findPositionIndexAtOrBeforeOffset(ctx, viewportStart); +- if (start === void 0) { +- return void 0; +- } +- if (targetIndex !== void 0 && state.positions[start] === void 0) { +- return { end: start, start }; +- } +- if (targetIndex === void 0) { +- const startBottom = getItemBottom(ctx, start); +- if (startBottom === void 0 || startBottom <= viewportStart) { +- return void 0; +- } +- } +- while (start > 0) { +- const top = state.positions[start]; +- if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { +- break; +- } +- start--; +- } +- while (start > 0) { +- const previousBottom = getItemBottom(ctx, start - 1); +- if (previousBottom === void 0 || previousBottom <= viewportStart) { +- break; ++ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; ++ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { ++ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { ++ cancelAnimationFrame(bootstrapInitialScroll.frameHandle); + } +- start--; +- } +- let end = start; +- while (end + 1 < dataLength) { +- const nextTop = state.positions[end + 1]; +- if (nextTop === void 0 || nextTop > viewportEnd) { +- break; ++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { ++ cancelScrollCompletionChecks(state); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; + } +- end++; +- } +- return { end, start }; +-} +-function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); +- if (range) { +- ctx.state.scrollTargetPinnedRange = range; +- ctx.state.scrollForNextCalculateItemsInView = void 0; +- } else { +- ctx.state.scrollTargetPinnedRange = void 0; ++ initialScrollCompletion.resetFlags(state); ++ setInitialScrollSession(state, { bootstrap: null }); ++ finishInitialScroll(ctx); + } + } +-function scrollTo(ctx, params) { +- var _a3, _b; ++function finishInitialScroll(ctx, options) { + var _a3, _b, _c; const state = ctx.state; - const { noScrollingTo, forceScroll, ...scrollTarget } = params; @@ -6716,11 +7926,7 @@ index 95465f2..b028ddc 100644 - if (!noScrollingTo) { - if (isInitialScroll) { - initialScrollCompletion.resetFlags(state); -+ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; -+ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { -+ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { -+ cancelAnimationFrame(bootstrapInitialScroll.frameHandle); - } +- } - const averageSizeSnapshot = getAverageSizeSnapshot(state); - state.scrollingTo = { - ...scrollTarget, @@ -6730,19 +7936,6 @@ index 95465f2..b028ddc 100644 - }; - if (!isInitialScroll) { - pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); -+ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { -+ cancelScrollCompletionChecks(state); -+ state.scrollingTo = void 0; -+ state.scrollTargetPinnedRange = void 0; -+ } -+ initialScrollCompletion.resetFlags(state); -+ setInitialScrollSession(state, { bootstrap: null }); -+ finishInitialScroll(ctx); -+ } -+} -+function finishInitialScroll(ctx, options) { -+ var _a3, _b, _c; -+ const state = ctx.state; + if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { + syncInitialScrollOffset(state, options.resolvedOffset); + } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { @@ -6811,7 +8004,15 @@ index 95465f2..b028ddc 100644 } // src/core/scrollToIndex.ts -@@ -4346,8 +4480,17 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4329,6 +4465,7 @@ function calculateItemsInView(ctx, params = {}) { + startIndex + }); + totalSize = getContentSize(ctx); ++ const minIndexSizeChangedThisPass = dataChanged ? 0 : minIndexSizeChanged; + if (minIndexSizeChanged !== void 0) { + state.minIndexSizeChanged = void 0; + } +@@ -4346,8 +4483,17 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -6825,13 +8026,13 @@ index 95465f2..b028ddc 100644 + } + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { -+ settleScrollTarget(ctx, isCompensating); ++ settleScrollTarget(ctx, { isCompensating, minIndexSizeChanged: minIndexSizeChangedThisPass }); + } + if (didMVCPAdjust) { updateScroll2(state.scroll); updateScrollRange(); } -@@ -5894,6 +6037,119 @@ function useRafCoalescer(callback) { +@@ -5894,6 +6040,119 @@ function useRafCoalescer(callback) { return coalescer; } @@ -6951,16 +8152,18 @@ index 95465f2..b028ddc 100644 // src/components/webConstants.ts var LEGEND_LIST_CONTENT_CONTAINER_CLASS = "legend-list-content-container"; var LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS = "legend-list-scrollbar-x-hidden"; -@@ -5985,6 +6241,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -5985,6 +6244,10 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; +var SMOOTH_SCROLL_MAX_MS = 2e3; ++var BORROW_MEASURE_ATTEMPTS = 2; ++var USER_INTERACTION_EVENTS = ["wheel", "pointerdown", "touchstart", "keydown"]; var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6053,6 +6311,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6053,6 +6316,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -6972,33 +8175,47 @@ index 95465f2..b028ddc 100644 const getMaxScrollOffset = useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6073,6 +6336,59 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,6 +6341,95 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); + const paddedNodeRef = useRef(null); ++ const borrowedExtentsRef = useRef(/* @__PURE__ */ new Map()); ++ const borrowIdRef = useRef(0); ++ const borrowWatchRef = useRef(0); + const animatedPaddingReleaseRef = useRef(void 0); + const withReachableExtent = useCallback( + (offset, maxOffset, animated, run) => { + var _a4; + const contentNode = contentRef.current; -+ const committedMaxOffset = getCommittedMaxScrollOffset(maxOffset); ++ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); ++ const committedMaxOffset = getCommittedMaxScrollOffset(getMaxScrollOffset()); + if (!contentNode || offset <= committedMaxOffset + SCROLL_EXTENT_EPSILON) { + run(clampOffset(offset, maxOffset)); + return; + } -+ const release = addTemporaryEndPadding( -+ contentNode, -+ paddingEndProp, -+ offset - committedMaxOffset + SCROLL_EXTENT_EPSILON -+ ); ++ const releases = []; ++ for (let attempt = 0; attempt < BORROW_MEASURE_ATTEMPTS; attempt++) { ++ const shortfall = offset - getMaxScrollOffset(); ++ if (shortfall <= 0) { ++ break; ++ } ++ releases.push(addTemporaryEndPadding(contentNode, paddingEndProp, shortfall + SCROLL_EXTENT_EPSILON)); ++ } ++ const borrowId = ++borrowIdRef.current; ++ const release = () => { ++ for (const releaseOne of releases) { ++ releaseOne(); ++ } ++ borrowedExtentsRef.current.delete(borrowId); ++ }; ++ borrowedExtentsRef.current.set(borrowId, Math.max(0, getMaxScrollOffset() - committedMaxOffset)); + paddedNodeRef.current = contentNode; + run(offset); + if (!animated) { + scheduleTemporaryEndPaddingRelease(contentNode, paddingEndProp, release); + return; + } -+ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); + const scrollTarget = getScrollTarget(); + const supportsScrollEnd = !!scrollTarget && "onscrollend" in scrollTarget; + const finish = () => { @@ -7007,21 +8224,40 @@ index 95465f2..b028ddc 100644 + } + animatedPaddingReleaseRef.current = void 0; + clearTimeout(settleTimeout); -+ scrollTarget == null ? void 0 : scrollTarget.removeEventListener("scrollend", finish); ++ scrollTarget == null ? void 0 : scrollTarget.removeEventListener("scrollend", finishIfArrived); ++ cancelAnimationFrame(borrowWatchRef.current); + release(); + }; ++ const finishIfArrived = () => { ++ if (Math.abs(getCurrentScrollOffset() - offset) <= SCROLL_EXTENT_EPSILON) { ++ finish(); ++ } ++ }; ++ const releaseWhenContentCommits = () => { ++ if (animatedPaddingReleaseRef.current !== finish) { ++ return; ++ } ++ if (getCommittedMaxScrollOffset(getMaxScrollOffset()) + SCROLL_EXTENT_EPSILON >= offset) { ++ finish(); ++ return; ++ } ++ borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); ++ }; + animatedPaddingReleaseRef.current = finish; ++ cancelAnimationFrame(borrowWatchRef.current); ++ borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); + const settleTimeout = setTimeout(finish, SMOOTH_SCROLL_MAX_MS); + if (supportsScrollEnd) { -+ scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finish); ++ scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finishIfArrived); + } + }, -+ [getCommittedMaxScrollOffset, getScrollTarget, paddingEndProp] ++ [getCommittedMaxScrollOffset, getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, paddingEndProp] + ); + useEffect( + () => () => { + var _a4; + (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); ++ cancelAnimationFrame(borrowWatchRef.current); + const paddedNode = paddedNodeRef.current; + if (paddedNode) { + releaseAllTemporaryEndPadding(paddedNode); @@ -7029,10 +8265,13 @@ index 95465f2..b028ddc 100644 + }, + [] + ); ++ const reportUserInteraction = useCallback(() => { ++ releaseScrollTargetForUserInteraction(ctx); ++ }, [ctx]); const scrollToLocalOffset = useCallback( (offset, animated) => { const scrollElement = scrollRef.current; -@@ -6095,14 +6411,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6095,14 +6452,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); options.left = left; options.top = top; @@ -7059,7 +8298,7 @@ index 95465f2..b028ddc 100644 ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6451,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6128,8 +6492,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -7069,13 +8308,16 @@ index 95465f2..b028ddc 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6142,7 +6464,12 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6142,7 +6505,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ if (!onScroll2 || !scrollRef.current) { return; } - const contentSize = getContentSize2(contentRef.current); + const rawContentSize = getContentSize2(contentRef.current); -+ const temporaryPadding = getTemporaryEndPadding(contentRef.current, paddingEndProp); ++ let temporaryPadding = 0; ++ for (const bought of borrowedExtentsRef.current.values()) { ++ temporaryPadding = Math.max(temporaryPadding, bought); ++ } + const contentSize = temporaryPadding ? { + height: horizontal ? rawContentSize.height : rawContentSize.height - temporaryPadding, + width: horizontal ? rawContentSize.width - temporaryPadding : rawContentSize.width @@ -7083,22 +8325,25 @@ index 95465f2..b028ddc 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6358,10 +6685,12 @@ function useValueListener$(key, callback) { - } - - // src/components/ScrollAdjust.tsx --function getScrollAdjustAxis(horizontal) { -+function getScrollAdjustAxis(horizontal, rtl = false) { - return horizontal ? { - contentSizeKey: "scrollWidth", -- paddingEndProp: "paddingRight", -+ // The end side under RTL is the left, matching how the list pads its own content and -+ // which property the scroll view borrows room on. -+ paddingEndProp: rtl ? "paddingLeft" : "paddingRight", - viewportSizeKey: "clientWidth", - x: 1, - y: 0 -@@ -6390,8 +6719,6 @@ function ScrollAdjust() { +@@ -6200,11 +6571,17 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + const target = getScrollTarget(); + if (!target) return; + target.addEventListener("scroll", handleScroll, { passive: true }); ++ for (const type of USER_INTERACTION_EVENTS) { ++ target.addEventListener(type, reportUserInteraction, { passive: true }); ++ } + if ("onscrollend" in target) { + target.addEventListener("scrollend", emitScrollEnd); + } + return () => { + target.removeEventListener("scroll", handleScroll); ++ for (const type of USER_INTERACTION_EVENTS) { ++ target.removeEventListener(type, reportUserInteraction); ++ } + if ("onscrollend" in target) { + target.removeEventListener("scrollend", emitScrollEnd); + } +@@ -6390,8 +6767,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3.useRef(0); const lastScrollAdjustUserOffsetRef = React3.useRef(0); @@ -7107,16 +8352,7 @@ index 95465f2..b028ddc 100644 const contentNodeRef = React3.useRef(null); const callback = React3.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6402,7 +6729,7 @@ function ScrollAdjust() { - const target = getScrollAdjustTarget(ctx, contentNodeRef.current); - if (target) { - const horizontal = !!ctx.state.props.horizontal; -- const axis = getScrollAdjustAxis(horizontal); -+ const axis = getScrollAdjustAxis(horizontal, isHorizontalRTL(ctx.state)); - const { contentNode, scrollElement: el } = target; - const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; - const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6417,29 +6744,10 @@ function ScrollAdjust() { +@@ -6417,29 +6792,10 @@ function ScrollAdjust() { const nextScroll = currentScroll + scrollDelta; const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; if (needsTemporaryPadding) { @@ -7148,7 +8384,7 @@ index 95465f2..b028ddc 100644 } else { scrollBy(); } -@@ -8267,6 +8575,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8267,6 +8623,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -7157,10 +8393,10 @@ index 95465f2..b028ddc 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js -index 914d2da..ce6a0d7 100644 +index 914d2da..c3ed202 100644 --- a/node_modules/@legendapp/list/react.js +++ b/node_modules/@legendapp/list/react.js -@@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; +@@ -434,178 +434,266 @@ var EDGE_POSITION_EPSILON = 1; var ENABLE_DEVMODE = IS_DEV && false; var ENABLE_DEBUG_VIEW = IS_DEV && false; @@ -7170,22 +8406,56 @@ index 914d2da..ce6a0d7 100644 - scheduledWork.cancel("checkFinishedScrollRetryFrame"); - scheduledWork.cancel("checkFinishedScrollFallback"); - scheduledWork.cancel("platformScrollCompletion"); --} ++// src/core/getStartOffsetAdjustment.ts ++function getStartOffsetAdjustment(ctx) { ++ const { state } = ctx; ++ const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; ++ return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); + } -function settlePendingImperativeScroll(state) { - var _a3, _b; - const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; - state.pendingScrollResolve = void 0; - state.pendingScrollToEnd = void 0; - resolvePendingScroll == null ? void 0 : resolvePendingScroll(); --} ++ ++// src/utils/getId.ts ++function getId(state, index) { ++ const { data, keyExtractor } = state.props; ++ if (!data) { ++ return ""; ++ } ++ const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; ++ const id = ret; ++ state.idCache[index] = id; ++ return id; + } -function cancelImperativeScroll(state) { - cancelScrollCompletionChecks(state); - state.scheduledWork.cancel("imperativeScrollReady"); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; - settlePendingImperativeScroll(state); --} -- ++ ++// src/core/updateContentMetricsState.ts ++function getRawContentLength(ctx) { ++ var _a3, _b, _c; ++ const { state, values } = ctx; ++ return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); ++} ++function getAlignItemsAtEndPadding(ctx) { ++ const { state } = ctx; ++ const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; ++ return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; ++} ++function updateContentMetricsState(ctx) { ++ const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; ++ const nextPadding = getAlignItemsAtEndPadding(ctx); ++ if (previousPadding !== nextPadding) { ++ set$(ctx, "alignItemsAtEndPadding", nextPadding); ++ } + } + -// src/core/deferredPublicOnScroll.ts -function withResolvedContentOffset(state, event, resolvedOffset) { - return { @@ -7193,12 +8463,40 @@ index 914d2da..ce6a0d7 100644 - nativeEvent: { - ...event.nativeEvent, - contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -- } ++// src/core/addTotalSize.ts ++function addTotalSize(ctx, key, add, notifyTotalSize = true) { ++ const state = ctx.state; ++ const prevTotalSize = state.totalSize; ++ let totalSize = state.totalSize; ++ if (key === null) { ++ totalSize = add; ++ if (state.timeoutSetPaddingTop) { ++ clearTimeout(state.timeoutSetPaddingTop); ++ state.timeoutSetPaddingTop = void 0; + } - }; --} ++ } else { ++ totalSize += add; ++ } ++ if (prevTotalSize !== totalSize) { ++ { ++ state.pendingTotalSize = void 0; ++ state.totalSize = totalSize; ++ if (notifyTotalSize) { ++ set$(ctx, "totalSize", totalSize); ++ } ++ updateContentMetricsState(ctx); ++ } ++ } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { ++ set$(ctx, "totalSize", totalSize); ++ } + } -function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { - var _a3, _b, _c, _d; -- const state = ctx.state; ++ ++// src/core/setSize.ts ++function setSize(ctx, itemKey, size, notifyTotalSize = true) { + const state = ctx.state; - const deferredEvent = state.deferredPublicOnScrollEvent; - state.deferredPublicOnScrollEvent = void 0; - if (deferredEvent) { @@ -7210,9 +8508,15 @@ index 914d2da..ce6a0d7 100644 - (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 - ) - ); -- } --} -- ++ const { sizes } = state; ++ const previousSize = sizes.get(itemKey); ++ const diff = previousSize !== void 0 ? size - previousSize : size; ++ if (diff !== 0) { ++ addTotalSize(ctx, itemKey, diff, notifyTotalSize); + } ++ sizes.set(itemKey, size); + } + -// src/core/initialScrollSession.ts -var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -function hasInitialScrollSessionCompletion(completion) { @@ -7221,7 +8525,10 @@ index 914d2da..ce6a0d7 100644 -function clearInitialScrollSession(state) { - state.initialScrollSession = void 0; - return void 0; --} ++// src/utils/helpers.ts ++function isFunction(obj) { ++ return typeof obj === "function"; + } -function createInitialScrollSession(options) { - const { bootstrap, completion, kind, previousDataLength } = options; - return kind === "offset" ? { @@ -7234,7 +8541,9 @@ index 914d2da..ce6a0d7 100644 - kind, - previousDataLength - }; --} ++function isArray(obj) { ++ return Array.isArray(obj); + } -function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { - var _a4, _b2; - if (!state.initialScrollSession) { @@ -7250,10 +8559,15 @@ index 914d2da..ce6a0d7 100644 - kind, - previousDataLength: state.initialScrollSession.previousDataLength - }); -- } ++var warned = /* @__PURE__ */ new Set(); ++function warnDevOnce(id, text) { ++ if (IS_DEV && !warned.has(id)) { ++ warned.add(id); ++ console.warn(`[legend-list] ${text}`); + } - (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; - return state.initialScrollSession.completion; --} + } -var initialScrollCompletion = { - didDispatchNativeScroll(state) { - var _a3, _b; @@ -7276,7 +8590,26 @@ index 914d2da..ce6a0d7 100644 - const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); - completion.didDispatchNativeScroll = void 0; - completion.didRetrySilentInitialScroll = void 0; -- } ++function roundSize(size) { ++ return Math.floor(size * 8) / 8; ++} ++function isNullOrUndefined(value) { ++ return value === null || value === void 0; ++} ++function getPadding(s, type) { ++ var _a3, _b, _c; ++ const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; ++ return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; ++} ++function extractPadding(style, contentContainerStyle, type) { ++ return getPadding(style, type) + getPadding(contentContainerStyle, type); ++} ++function findContainerId(ctx, key) { ++ var _a3, _b; ++ const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); ++ if (directMatch !== void 0) { ++ return directMatch; + } -}; -var initialScrollWatchdog = { - clear(state) { @@ -7300,7 +8633,12 @@ index 914d2da..ce6a0d7 100644 - var _a3, _b; - if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { - return; -- } ++ const numContainers = peek$(ctx, "numContainers"); ++ for (let i = 0; i < numContainers; i++) { ++ const itemKey = peek$(ctx, `containerItemKey${i}`); ++ if (itemKey === key) { ++ return i; + } - const completion = ensureInitialScrollSessionCompletion(state); - completion.watchdog = watchdog ? { - startScroll: watchdog.startScroll, @@ -7320,7 +8658,7 @@ index 914d2da..ce6a0d7 100644 - } - if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { - return clearInitialScrollSession(state); -- } + } - const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; - state.initialScrollSession = createInitialScrollSession({ - bootstrap, @@ -7329,256 +8667,196 @@ index 914d2da..ce6a0d7 100644 - previousDataLength - }); - return state.initialScrollSession; --} -- ++ return -1; + } + -// src/utils/checkThreshold.ts -var HYSTERESIS_MULTIPLIER = 1.3; -function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { - const absDistance = Math.abs(distance); - return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; ++// src/utils/getItemSize.ts ++function getKnownOrFixedSize(ctx, key, index, data, resolved) { ++ var _a3, _b; ++ const state = ctx.state; ++ const { getFixedItemSize, getItemType } = state.props; ++ let size = key ? state.sizesKnown.get(key) : void 0; ++ if (size === void 0 && key && getFixedItemSize) { ++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; ++ const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); ++ if (fixedSize !== void 0) { ++ size = fixedSize + ctx.scrollAxisGap; ++ state.sizesKnown.set(key, size); ++ } ++ } ++ return size; ++} ++function getKnownOrFixedItemSize(ctx, index) { ++ const key = getId(ctx.state, index); ++ return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); ++} ++function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { ++ for (let index = startIndex; index <= endIndex; index++) { ++ if (getKnownOrFixedItemSize(ctx, index) === void 0) { ++ return false; ++ } ++ } ++ return true; ++} ++function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { ++ var _a3, _b, _c, _d; ++ const state = ctx.state; ++ const { ++ sizes, ++ averageSizes, ++ props: { estimatedItemSize, getItemType }, ++ scrollingTo ++ } = state; ++ const sizeKnown = state.sizesKnown.get(key); ++ if (sizeKnown !== void 0) { ++ return sizeKnown; ++ } ++ let size; ++ const renderedSize = sizes.get(key); ++ if (preferCachedSize) { ++ if (renderedSize !== void 0) { ++ return renderedSize; ++ } ++ } ++ size = getKnownOrFixedSize(ctx, key, index, data, resolved); ++ if (size !== void 0) { ++ setSize(ctx, key, size, notifyTotalSize); ++ return size; ++ } ++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; ++ if (useAverageSize && !scrollingTo) { ++ const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; ++ if (averageSizeForType !== void 0) { ++ size = roundSize(averageSizeForType); ++ } ++ } ++ if (size === void 0 && renderedSize !== void 0) { ++ return renderedSize; ++ } ++ if (size === void 0 && useAverageSize && scrollingTo) { ++ const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; ++ if (averageSizeForType !== void 0) { ++ size = roundSize(averageSizeForType); ++ } ++ } ++ if (size === void 0) { ++ size = estimatedItemSize + ctx.scrollAxisGap; ++ } ++ setSize(ctx, key, size, notifyTotalSize); ++ return size; ++} ++function getItemSizeAtIndex(ctx, index) { ++ if (index === void 0 || index < 0) { ++ return void 0; ++ } ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; ++ } ++ if (index !== void 0) { ++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); ++ if (startOffsetAdjustment) { ++ offset += startOffsetAdjustment; ++ } ++ } ++ if (viewPosition !== void 0 && index !== void 0) { ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return offset; ++ } ++ const isOutOfBounds = index < 0 || index >= dataLength; ++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; ++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); ++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); ++ const trailingInset = getContentInsetEnd(ctx); ++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); ++ if (!isOutOfBounds && index === state.props.data.length - 1) { ++ const footerSize = peek$(ctx, "footerSize") || 0; ++ offset += footerSize; ++ } ++ } ++ return offset; ++} ++ ++// src/core/clampScrollOffset.ts ++function clampScrollOffset(ctx, offset, scrollTarget) { ++ const state = ctx.state; ++ const contentSize = getContentSize(ctx); ++ let clampedOffset = offset; ++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { ++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); ++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; ++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; ++ const maxOffset = baseMaxOffset + extraEndOffset; ++ clampedOffset = Math.min(offset, maxOffset); ++ } ++ clampedOffset = Math.max(0, clampedOffset); ++ return clampedOffset; ++} ++ ++// src/utils/checkThreshold.ts ++var HYSTERESIS_MULTIPLIER = 1.3; ++function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { ++ const absDistance = Math.abs(distance); ++ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; + } + var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { + const absDistance = Math.abs(distance); +@@ -815,642 +903,346 @@ function recalculateSettledScroll(ctx) { + } + checkThresholds(ctx); + } +-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; +-function clearAdaptiveRenderExitTimeout(ctx) { +- ctx.state.scheduledWork.cancel("adaptiveRender"); -} --var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { -- const absDistance = Math.abs(distance); -- const within = atThreshold || threshold > 0 && absDistance <= threshold; -- const updateSnapshot = () => { -- setSnapshot({ -- atThreshold, -- contentSize: context.contentSize, -- dataLength: context.dataLength, -- scrollPosition: context.scrollPosition -- }); -- }; -- if (!wasReached) { -- if (!within) { -- return false; -- } -- onReached(distance); -- updateSnapshot(); -- return true; -- } -- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); -- if (reset) { -- setSnapshot(void 0); -- return false; -- } -- if (within) { -- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; -- if (changed) { -- updateSnapshot(); -- } -- } -- return true; --}; -- --// src/utils/edgeReachedGate.ts --function resetEdgeLatch(ctx, edge) { +-function scheduleAdaptiveRenderExit(ctx, exitDelay) { - const state = ctx.state; -- if (edge === "start") { -- state.isStartReached = false; -- state.startReachedSnapshot = void 0; +- clearAdaptiveRenderExitTimeout(ctx); +- if (exitDelay <= 0) { +- setAdaptiveRender(ctx, "normal", "scroll"); - } else { -- state.isEndReached = false; -- state.endReachedSnapshot = void 0; +- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); - } -} --function resetSharedEdgeGateIfOutsideHysteresis(ctx) { -- const state = ctx.state; -- if (!state.edgeReachedGate) { -- return; +-function setAdaptiveRender(ctx, mode, reason) { +- var _a3, _b; +- const previousMode = peek$(ctx, "adaptiveRender"); +- if (previousMode !== mode) { +- set$(ctx, "adaptiveRender", mode); +- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); - } -- const contentSize = getContentSize(ctx); -- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); -- const isContentLess = contentSize < state.scrollLength; -- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; -- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; -- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); -- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); -- if (isOutsideStart && isOutsideEnd) { -- state.edgeReachedGate = void 0; -- } --} --function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { -- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; --} --function markReachedEdge(ctx) { -- ctx.state.edgeReachedGate = "closed"; --} --function prepareReachedEdgeForNextUserScroll(ctx) { -- if (ctx.state.edgeReachedGate) { -- ctx.state.edgeReachedGate = "prepared"; -- } --} --function beginReachedEdgeUserScroll(ctx, scrollDelta) { -- const state = ctx.state; -- if (state.edgeReachedGate !== "prepared") { -- return void 0; -- } -- const allowedEdge = scrollDelta < 0 ? "start" : "end"; -- state.edgeReachedGate = "closed"; -- resetEdgeLatch(ctx, allowedEdge); -- return allowedEdge; --} -- --// src/utils/hasActiveInitialScroll.ts --function hasActiveInitialScroll(state) { -- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; --} -- --// src/utils/checkAtBottom.ts --function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -- var _a3; -- const state = ctx.state; -- if (!state) { -- return; -- } -- const { -- queuedInitialLayout, -- scrollLength, -- scroll, -- maintainingScrollAtEnd, -- props: { maintainScrollAtEndThreshold, onEndReachedThreshold } -- } = state; -- const contentSize = getContentSize(ctx); -- resetSharedEdgeGateIfOutsideHysteresis(ctx); -- if (contentSize > 0 && queuedInitialLayout) { -- const insetEnd = getContentInsetEnd(ctx); -- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; -- const isContentLess = contentSize < scrollLength; -- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); -- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); -- set$( -- ctx, -- "isWithinMaintainScrollAtEndThreshold", -- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength -- ); -- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; -- if (!shouldSkipThresholdChecks) { -- state.isEndReached = checkThreshold( -- distanceFromEnd, -- isContentLess, -- onEndReachedThreshold * scrollLength, -- state.isEndReached, -- state.endReachedSnapshot, -- { -- contentSize, -- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, -- scrollPosition: scroll -- }, -- (distance) => { -- var _a4, _b; -- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { -- markReachedEdge(ctx); -- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); -- } -- }, -- (snapshot) => { -- state.endReachedSnapshot = snapshot; -- } -- ); -- } -- } --} -- --// src/utils/checkAtTop.ts --function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -- const state = ctx == null ? void 0 : ctx.state; -- if (!state) { -- return; -- } -- const { -- isStartReached, -- props: { data, onStartReachedThreshold }, -- scroll, -- scrollLength, -- startReachedSnapshot, -- totalSize -- } = state; -- const dataLength = data.length; -- const threshold = onStartReachedThreshold * scrollLength; -- resetSharedEdgeGateIfOutsideHysteresis(ctx); -- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { -- state.isStartReached = false; -- state.startReachedSnapshot = void 0; -- } -- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); -- set$(ctx, "isNearStart", scroll <= threshold); -- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; -- if (!shouldSkipThresholdChecks) { -- state.isStartReached = checkThreshold( -- scroll, -- false, -- threshold, -- state.isStartReached, -- startReachedSnapshot, -- { -- contentSize: totalSize, -- dataLength, -- scrollPosition: scroll -- }, -- (distance) => { -- var _a3, _b; -- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { -- markReachedEdge(ctx); -- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); -- } -- }, -- (snapshot) => { -- state.startReachedSnapshot = snapshot; -- } -- ); -- } --} -- --// src/utils/checkThresholds.ts --function checkThresholds(ctx, allowedEdge) { -- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; -- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); --} -- --// src/core/recalculateSettledScroll.ts --function recalculateSettledScroll(ctx) { -- var _a3, _b; -- const state = ctx.state; -- if ((_a3 = state.props) == null ? void 0 : _a3.data) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); -- } -- checkThresholds(ctx); --} --var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; --var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; --var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; --function clearAdaptiveRenderExitTimeout(ctx) { -- ctx.state.scheduledWork.cancel("adaptiveRender"); --} --function scheduleAdaptiveRenderExit(ctx, exitDelay) { -- const state = ctx.state; -- clearAdaptiveRenderExitTimeout(ctx); -- if (exitDelay <= 0) { -- setAdaptiveRender(ctx, "normal", "scroll"); -- } else { -- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); -- } --} --function setAdaptiveRender(ctx, mode, reason) { -- var _a3, _b; -- const previousMode = peek$(ctx, "adaptiveRender"); -- if (previousMode !== mode) { -- set$(ctx, "adaptiveRender", mode); -- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); -- } --} --function resetAdaptiveRender(ctx) { -- var _a3, _b; -- clearAdaptiveRenderExitTimeout(ctx); -- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; -- if (peek$(ctx, "adaptiveRender") !== mode) { -- setAdaptiveRender(ctx, mode, "initial"); +-} +-function resetAdaptiveRender(ctx) { ++ ++// src/core/finishScrollTo.ts ++function finishScrollTo(ctx) { + var _a3, _b; +- clearAdaptiveRenderExitTimeout(ctx); +- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; +- if (peek$(ctx, "adaptiveRender") !== mode) { +- setAdaptiveRender(ctx, mode, "initial"); - } -} -function updateAdaptiveRender(ctx, scrollVelocity, options) { - var _a3, _b, _c; -- const state = ctx.state; + const state = ctx.state; - const adaptiveRender = state.props.adaptiveRender; - const currentMode = peek$(ctx, "adaptiveRender"); - if (peek$(ctx, "readyToRender")) { @@ -7599,10 +8877,40 @@ index 914d2da..ce6a0d7 100644 - } - } else { - resetAdaptiveRender(ctx); -- } -- } --} -- ++ if (state == null ? void 0 : state.scrollingTo) { ++ cancelScrollCompletionChecks(state); ++ const resolvePendingScroll = state.pendingScrollResolve; ++ state.pendingScrollResolve = void 0; ++ const scrollingTo = state.scrollingTo; ++ state.scrollHistory.length = 0; ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ if (state.pendingTotalSize !== void 0) { ++ addTotalSize(ctx, null, state.pendingTotalSize); ++ } ++ { ++ state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); ++ } ++ if (scrollingTo.isInitialScroll || state.initialScroll) { ++ const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; ++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; ++ finishInitialScroll(ctx, { ++ onFinished: () => { ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); ++ }, ++ preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, ++ recalculateItems: true, ++ schedulePreservedTargetClear: shouldPreserveResizeTarget, ++ syncObservedOffset: isOffsetSession, ++ waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame ++ }); ++ return; + } ++ recalculateSettledScroll(ctx); ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); + } + } + -// src/utils/getEffectiveDrawDistance.ts -var INITIAL_DRAW_DISTANCE = 50; -function getEffectiveDrawDistance(ctx, mode) { @@ -7616,8 +8924,21 @@ index 914d2da..ce6a0d7 100644 -function scheduleFullDrawDistancePrewarm(ctx) { - const { state } = ctx; - if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { -- return; -- } ++// src/core/doScrollTo.ts ++var SCROLL_END_IDLE_MS = 80; ++var SCROLL_END_MAX_MS = 1500; ++var SMOOTH_SCROLL_DURATION_MS = 320; ++var SCROLL_END_TARGET_EPSILON = 1; ++function doScrollTo(ctx, params) { ++ var _a3, _b; ++ const state = ctx.state; ++ const { animated, horizontal, offset } = params; ++ state.scheduledWork.cancel("platformScrollCompletion"); ++ const scroller = state.refScroller.current; ++ const node = scroller == null ? void 0 : scroller.getScrollableNode(); ++ if (!scroller || !node) { + return; + } - state.scheduledWork.frame(() => { - var _a3; - return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); @@ -7636,10 +8957,35 @@ index 914d2da..ce6a0d7 100644 - } - if (resetInitialScroll) { - state.didFinishInitialScroll = false; -- } ++ const isAnimated = !!animated; ++ const isHorizontal = !!horizontal; ++ const contentSize = isHorizontal ? getContentSize(ctx) : void 0; ++ const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; ++ const top = isHorizontal ? 0 : offset; ++ scroller.scrollTo({ animated: isAnimated, x: left, y: top }); ++ if (isAnimated) { ++ const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; ++ listenForScrollEnd(ctx, { ++ readOffset: () => scroller.getCurrentScrollOffset(), ++ target, ++ targetOffset: offset ++ }); ++ } else { ++ state.scroll = offset; ++ const targetToken = state.scrollingTo; ++ state.scheduledWork.timeout( ++ () => { ++ if (targetToken === state.scrollingTo) { ++ finishScrollTo(ctx); ++ } ++ }, ++ 100, ++ "platformScrollCompletion" ++ ); + } - set$(ctx, "readyToRender", false); - resetAdaptiveRender(ctx); --} + } -function setInitialRenderState(ctx, { - didLayout, - didInitialScroll @@ -7668,7 +9014,12 @@ index 914d2da..ce6a0d7 100644 - onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); - } - } -- } ++function listenForScrollEnd(ctx, params) { ++ const { readOffset, target, targetOffset } = params; ++ if (!target) { ++ finishScrollTo(ctx); ++ return; + } -} - -// src/core/finishInitialScroll.ts @@ -7694,12 +9045,23 @@ index 914d2da..ce6a0d7 100644 - if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { - if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { - cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -- } ++ const supportsScrollEnd = "onscrollend" in target; ++ let idleTimeout; ++ let settled = false; ++ const { scheduledWork, scrollingTo: targetToken } = ctx.state; ++ const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); ++ const cleanup = () => { ++ target.removeEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.removeEventListener("scrollend", onScrollEnd); + } - if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { - cancelScrollCompletionChecks(state); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; -- } ++ if (idleTimeout !== void 0) { ++ clearTimeout(idleTimeout); + } - initialScrollCompletion.resetFlags(state); - setInitialScrollSession(state, { bootstrap: null }); - finishInitialScroll(ctx); @@ -7714,7 +9076,13 @@ index 914d2da..ce6a0d7 100644 - const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); - if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { - syncInitialScrollOffset(state, observedOffset); -- } ++ clearTimeout(maxTimeout); ++ }; ++ const cancel = () => { ++ if (!settled) { ++ settled = true; ++ cleanup(); + } - } - const complete = () => { - var _a4, _b2, _c2, _d, _e; @@ -7740,240 +9108,422 @@ index 914d2da..ce6a0d7 100644 - } - } else { - clearPreservedInitialScrollTarget(state); -- } ++ }; ++ const finish = (reason) => { ++ if (settled) return; ++ if (targetToken !== ctx.state.scrollingTo) { ++ scheduledWork.cancel("platformScrollCompletion"); ++ return; + } - if (options == null ? void 0 : options.recalculateItems) { - recalculateSettledScroll(ctx); -- } ++ const currentOffset = readOffset(); ++ const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; ++ if (reason === "scrollend" && !isNearTarget) { ++ return; + } - setInitialRenderState(ctx, { didInitialScroll: true }); - if (shouldReleaseDeferredPublicOnScroll) { - releaseDeferredPublicOnScroll(ctx, finalScrollOffset); -- } ++ scheduledWork.cancel("platformScrollCompletion"); ++ finishScrollTo(ctx); ++ }; ++ const onScroll2 = () => { ++ if (idleTimeout !== void 0) { ++ clearTimeout(idleTimeout); + } - (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); -- }; ++ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); + }; - if (options == null ? void 0 : options.waitForCompletionFrame) { - requestAnimationFrame(complete); - return; -- } ++ const onScrollEnd = () => finish("scrollend"); ++ target.addEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.addEventListener("scrollend", onScrollEnd); ++ } else { ++ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); + } - complete(); --} -- ++ scheduledWork.register("platformScrollCompletion", cancel); + } + -// src/core/calculateOffsetForIndex.ts -function calculateOffsetForIndex(ctx, index) { - const state = ctx.state; - return index !== void 0 ? state.positions[index] || 0 : 0; --} ++// src/core/initialScrollSession.ts ++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; ++function hasInitialScrollSessionCompletion(completion) { ++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); + } - - // src/core/getStartOffsetAdjustment.ts - function getStartOffsetAdjustment(ctx) { - const { state } = ctx; -@@ -1234,58 +635,273 @@ function getItemSizeAtIndex(ctx, index) { - if (index === void 0 || index < 0) { - return void 0; - } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+ const targetId = getId(ctx.state, index); -+ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+} -+ -+// src/core/calculateOffsetWithOffsetPosition.ts -+function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -+ var _a3; -+ const state = ctx.state; -+ const { index, viewOffset, viewPosition } = params; -+ let offset = offsetParam; -+ if (viewOffset) { -+ offset -= viewOffset; -+ } -+ if (index !== void 0) { -+ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -+ if (startOffsetAdjustment) { -+ offset += startOffsetAdjustment; -+ } -+ } -+ if (viewPosition !== void 0 && index !== void 0) { -+ const dataLength = state.props.data.length; -+ if (dataLength === 0) { -+ return offset; -+ } -+ const isOutOfBounds = index < 0 || index >= dataLength; -+ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -+ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -+ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -+ const trailingInset = getContentInsetEnd(ctx); -+ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -+ if (!isOutOfBounds && index === state.props.data.length - 1) { -+ const footerSize = peek$(ctx, "footerSize") || 0; -+ offset += footerSize; -+ } -+ } -+ return offset; -+} -+ -+// src/core/clampScrollOffset.ts -+function clampScrollOffset(ctx, offset, scrollTarget) { -+ const state = ctx.state; -+ const contentSize = getContentSize(ctx); -+ let clampedOffset = offset; -+ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { -+ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); -+ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; -+ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; -+ const maxOffset = baseMaxOffset + extraEndOffset; -+ clampedOffset = Math.min(offset, maxOffset); -+ } -+ clampedOffset = Math.max(0, clampedOffset); -+ return clampedOffset; -+} -+ -+// src/utils/checkThreshold.ts -+var HYSTERESIS_MULTIPLIER = 1.3; -+function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { -+ const absDistance = Math.abs(distance); -+ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; -+} -+var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { -+ const absDistance = Math.abs(distance); -+ const within = atThreshold || threshold > 0 && absDistance <= threshold; -+ const updateSnapshot = () => { -+ setSnapshot({ -+ atThreshold, -+ contentSize: context.contentSize, -+ dataLength: context.dataLength, -+ scrollPosition: context.scrollPosition -+ }); +-// src/core/getStartOffsetAdjustment.ts +-function getStartOffsetAdjustment(ctx) { +- const { state } = ctx; +- const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; +- return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); ++function clearInitialScrollSession(state) { ++ state.initialScrollSession = void 0; ++ return void 0; + } +- +-// src/utils/getId.ts +-function getId(state, index) { +- const { data, keyExtractor } = state.props; +- if (!data) { +- return ""; +- } +- const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; +- const id = ret; +- state.idCache[index] = id; +- return id; ++function createInitialScrollSession(options) { ++ const { bootstrap, completion, kind, previousDataLength } = options; ++ return kind === "offset" ? { ++ completion, ++ kind, ++ previousDataLength ++ } : { ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength + }; -+ if (!wasReached) { -+ if (!within) { -+ return false; -+ } -+ onReached(distance); -+ updateSnapshot(); -+ return true; -+ } -+ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); -+ if (reset) { -+ setSnapshot(void 0); -+ return false; -+ } -+ if (within) { -+ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; -+ if (changed) { -+ updateSnapshot(); -+ } + } +- +-// src/core/updateContentMetricsState.ts +-function getRawContentLength(ctx) { +- var _a3, _b, _c; +- const { state, values } = ctx; +- return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); ++function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { ++ var _a4, _b2; ++ if (!state.initialScrollSession) { ++ state.initialScrollSession = createInitialScrollSession({ ++ completion: {}, ++ kind, ++ previousDataLength: 0 ++ }); ++ } else if (state.initialScrollSession.kind !== kind) { ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, ++ completion: state.initialScrollSession.completion, ++ kind, ++ previousDataLength: state.initialScrollSession.previousDataLength ++ }); + } -+ return true; ++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; ++ return state.initialScrollSession.completion; + } +-function getAlignItemsAtEndPadding(ctx) { +- const { state } = ctx; +- const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; +- return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; +-} +-function updateContentMetricsState(ctx) { +- const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; +- const nextPadding = getAlignItemsAtEndPadding(ctx); +- if (previousPadding !== nextPadding) { +- set$(ctx, "alignItemsAtEndPadding", nextPadding); +- } +-} +- +-// src/core/addTotalSize.ts +-function addTotalSize(ctx, key, add, notifyTotalSize = true) { +- const state = ctx.state; +- const prevTotalSize = state.totalSize; +- let totalSize = state.totalSize; +- if (key === null) { +- totalSize = add; +- if (state.timeoutSetPaddingTop) { +- clearTimeout(state.timeoutSetPaddingTop); +- state.timeoutSetPaddingTop = void 0; ++var initialScrollCompletion = { ++ didDispatchNativeScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); ++ }, ++ didRetrySilentInitialScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); ++ }, ++ markInitialScrollNativeDispatch(state) { ++ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; ++ }, ++ markSilentInitialScrollRetry(state) { ++ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; ++ }, ++ resetFlags(state) { ++ if (!state.initialScrollSession) { ++ return; + } +- } else { +- totalSize += add; ++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); ++ completion.didDispatchNativeScroll = void 0; ++ completion.didRetrySilentInitialScroll = void 0; + } +- if (prevTotalSize !== totalSize) { +- { +- state.pendingTotalSize = void 0; +- state.totalSize = totalSize; +- if (notifyTotalSize) { +- set$(ctx, "totalSize", totalSize); +- } +- updateContentMetricsState(ctx); +}; -+ -+// src/utils/edgeReachedGate.ts -+function resetEdgeLatch(ctx, edge) { ++var initialScrollWatchdog = { ++ clear(state) { ++ initialScrollWatchdog.set(state, void 0); ++ }, ++ didReachTarget(newScroll, watchdog) { ++ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); ++ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ get(state) { ++ var _a3, _b; ++ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; ++ }, ++ hasNonZeroTargetOffset(targetOffset) { ++ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ isAtZeroTargetOffset(targetOffset) { ++ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ set(state, watchdog) { ++ var _a3, _b; ++ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { ++ return; + } +- } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { +- set$(ctx, "totalSize", totalSize); ++ const completion = ensureInitialScrollSessionCompletion(state); ++ completion.watchdog = watchdog ? { ++ startScroll: watchdog.startScroll, ++ targetOffset: watchdog.targetOffset ++ } : void 0; + } +-} +- +-// src/core/setSize.ts +-function setSize(ctx, itemKey, size, notifyTotalSize = true) { +- const state = ctx.state; +- const { sizes } = state; +- const previousSize = sizes.get(itemKey); +- const diff = previousSize !== void 0 ? size - previousSize : size; +- if (diff !== 0) { +- addTotalSize(ctx, itemKey, diff, notifyTotalSize); ++}; ++function setInitialScrollSession(state, options = {}) { ++ var _a3, _b, _c, _d; ++ const existingSession = state.initialScrollSession; ++ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; ++ const completion = existingSession == null ? void 0 : existingSession.completion; ++ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; ++ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; ++ if (!kind) { ++ return clearInitialScrollSession(state); + } +- sizes.set(itemKey, size); +-} +- +-// src/utils/helpers.ts +-function isFunction(obj) { +- return typeof obj === "function"; +-} +-function isArray(obj) { +- return Array.isArray(obj); +-} +-var warned = /* @__PURE__ */ new Set(); +-function warnDevOnce(id, text) { +- if (IS_DEV && !warned.has(id)) { +- warned.add(id); +- console.warn(`[legend-list] ${text}`); ++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { ++ return clearInitialScrollSession(state); + } ++ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }); ++ return state.initialScrollSession; + } +-function roundSize(size) { +- return Math.floor(size * 8) / 8; +-} +-function isNullOrUndefined(value) { +- return value === null || value === void 0; +-} +-function getPadding(s, type) { +- var _a3, _b, _c; +- const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; +- return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; ++var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; ++function clearAdaptiveRenderExitTimeout(ctx) { ++ ctx.state.scheduledWork.cancel("adaptiveRender"); + } +-function extractPadding(style, contentContainerStyle, type) { +- return getPadding(style, type) + getPadding(contentContainerStyle, type); ++function scheduleAdaptiveRenderExit(ctx, exitDelay) { + const state = ctx.state; -+ if (edge === "start") { -+ state.isStartReached = false; -+ state.startReachedSnapshot = void 0; ++ clearAdaptiveRenderExitTimeout(ctx); ++ if (exitDelay <= 0) { ++ setAdaptiveRender(ctx, "normal", "scroll"); + } else { -+ state.isEndReached = false; -+ state.endReachedSnapshot = void 0; -+ } -+} -+function resetSharedEdgeGateIfOutsideHysteresis(ctx) { -+ const state = ctx.state; -+ if (!state.edgeReachedGate) { -+ return; -+ } -+ const contentSize = getContentSize(ctx); -+ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); -+ const isContentLess = contentSize < state.scrollLength; -+ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; -+ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; -+ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); -+ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); -+ if (isOutsideStart && isOutsideEnd) { -+ state.edgeReachedGate = void 0; ++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); + } -+} -+function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { -+ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; -+} -+function markReachedEdge(ctx) { -+ ctx.state.edgeReachedGate = "closed"; -+} -+function prepareReachedEdgeForNextUserScroll(ctx) { -+ if (ctx.state.edgeReachedGate) { -+ ctx.state.edgeReachedGate = "prepared"; -+ } -+} -+function beginReachedEdgeUserScroll(ctx, scrollDelta) { -+ const state = ctx.state; -+ if (state.edgeReachedGate !== "prepared") { -+ return void 0; -+ } -+ const allowedEdge = scrollDelta < 0 ? "start" : "end"; -+ state.edgeReachedGate = "closed"; -+ resetEdgeLatch(ctx, allowedEdge); -+ return allowedEdge; -+} -+ -+// src/utils/hasActiveInitialScroll.ts -+function hasActiveInitialScroll(state) { -+ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; -+} -+ -+// src/utils/checkAtBottom.ts -+function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -+ var _a3; + } +-function findContainerId(ctx, key) { ++function setAdaptiveRender(ctx, mode, reason) { + var _a3, _b; +- const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); +- if (directMatch !== void 0) { +- return directMatch; +- } +- const numContainers = peek$(ctx, "numContainers"); +- for (let i = 0; i < numContainers; i++) { +- const itemKey = peek$(ctx, `containerItemKey${i}`); +- if (itemKey === key) { +- return i; +- } ++ const previousMode = peek$(ctx, "adaptiveRender"); ++ if (previousMode !== mode) { ++ set$(ctx, "adaptiveRender", mode); ++ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); + } +- return -1; + } +- +-// src/utils/getItemSize.ts +-function getKnownOrFixedSize(ctx, key, index, data, resolved) { ++function resetAdaptiveRender(ctx) { + var _a3, _b; +- const state = ctx.state; +- const { getFixedItemSize, getItemType } = state.props; +- let size = key ? state.sizesKnown.get(key) : void 0; +- if (size === void 0 && key && getFixedItemSize) { +- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; +- const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); +- if (fixedSize !== void 0) { +- size = fixedSize + ctx.scrollAxisGap; +- state.sizesKnown.set(key, size); +- } ++ clearAdaptiveRenderExitTimeout(ctx); ++ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; ++ if (peek$(ctx, "adaptiveRender") !== mode) { ++ setAdaptiveRender(ctx, mode, "initial"); + } +- return size; +-} +-function getKnownOrFixedItemSize(ctx, index) { +- const key = getId(ctx.state, index); +- return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); + } +-function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { +- for (let index = startIndex; index <= endIndex; index++) { +- if (getKnownOrFixedItemSize(ctx, index) === void 0) { +- return false; ++function updateAdaptiveRender(ctx, scrollVelocity, options) { ++ var _a3, _b, _c; + const state = ctx.state; -+ if (!state) { -+ return; -+ } -+ const { -+ queuedInitialLayout, -+ scrollLength, -+ scroll, -+ maintainingScrollAtEnd, -+ props: { maintainScrollAtEndThreshold, onEndReachedThreshold } -+ } = state; -+ const contentSize = getContentSize(ctx); -+ resetSharedEdgeGateIfOutsideHysteresis(ctx); -+ if (contentSize > 0 && queuedInitialLayout) { -+ const insetEnd = getContentInsetEnd(ctx); -+ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; -+ const isContentLess = contentSize < scrollLength; -+ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); -+ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); -+ set$( -+ ctx, -+ "isWithinMaintainScrollAtEndThreshold", -+ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength -+ ); -+ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; -+ if (!shouldSkipThresholdChecks) { -+ state.isEndReached = checkThreshold( -+ distanceFromEnd, -+ isContentLess, -+ onEndReachedThreshold * scrollLength, -+ state.isEndReached, -+ state.endReachedSnapshot, -+ { -+ contentSize, -+ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, -+ scrollPosition: scroll -+ }, -+ (distance) => { -+ var _a4, _b; -+ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { -+ markReachedEdge(ctx); -+ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); -+ } -+ }, -+ (snapshot) => { -+ state.endReachedSnapshot = snapshot; ++ const adaptiveRender = state.props.adaptiveRender; ++ const currentMode = peek$(ctx, "adaptiveRender"); ++ if (peek$(ctx, "readyToRender")) { ++ if (adaptiveRender) { ++ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; ++ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; ++ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; ++ const threshold = currentMode === "light" ? exitVelocity : enterVelocity; ++ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; ++ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; ++ if (nextMode !== previousMode) { ++ if (nextMode === "light") { ++ setAdaptiveRender(ctx, "light", "scroll"); ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } else if (currentMode === "light") { ++ scheduleAdaptiveRenderExit(ctx, exitDelay); + } -+ ); -+ } -+ } ++ } ++ } else { ++ resetAdaptiveRender(ctx); + } + } +- return true; } - +-function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { +- var _a3, _b, _c, _d; ++ ++// src/core/doMaintainScrollAtEnd.ts ++function doMaintainScrollAtEnd(ctx) { + const state = ctx.state; + const { +- sizes, +- averageSizes, +- props: { estimatedItemSize, getItemType }, +- scrollingTo ++ didContainersLayout, ++ pendingNativeMVCPAdjust, ++ refScroller, ++ props: { maintainScrollAtEnd } + } = state; +- const sizeKnown = state.sizesKnown.get(key); +- if (sizeKnown !== void 0) { +- return sizeKnown; ++ const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); ++ const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout); ++ if (pendingNativeMVCPAdjust) { ++ state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd; ++ return false; + } +- let size; +- const renderedSize = sizes.get(key); +- if (preferCachedSize) { +- if (renderedSize !== void 0) { +- return renderedSize; +- } +- } +- size = getKnownOrFixedSize(ctx, key, index, data, resolved); +- if (size !== void 0) { +- setSize(ctx, key, size, notifyTotalSize); +- return size; +- } +- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; +- if (useAverageSize && !scrollingTo) { +- const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; +- if (averageSizeForType !== void 0) { +- size = roundSize(averageSizeForType); +- } +- } +- if (size === void 0 && renderedSize !== void 0) { +- return renderedSize; +- } +- if (size === void 0 && useAverageSize && scrollingTo) { +- const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; +- if (averageSizeForType !== void 0) { +- size = roundSize(averageSizeForType); +- } +- } +- if (size === void 0) { +- size = estimatedItemSize + ctx.scrollAxisGap; +- } +- setSize(ctx, key, size, notifyTotalSize); +- return size; +-} +-function getItemSizeAtIndex(ctx, index) { +- if (index === void 0 || index < 0) { +- return void 0; +- } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); +-} +- -// src/core/calculateOffsetWithOffsetPosition.ts -function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { - var _a3; @@ -7982,32 +9532,13 @@ index 914d2da..ce6a0d7 100644 - let offset = offsetParam; - if (viewOffset) { - offset -= viewOffset; -+// src/utils/checkAtTop.ts -+function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -+ const state = ctx == null ? void 0 : ctx.state; -+ if (!state) { -+ return; - } +- } - if (index !== void 0) { - const startOffsetAdjustment = getStartOffsetAdjustment(ctx); - if (startOffsetAdjustment) { - offset += startOffsetAdjustment; - } -+ const { -+ isStartReached, -+ props: { data, onStartReachedThreshold }, -+ scroll, -+ scrollLength, -+ startReachedSnapshot, -+ totalSize -+ } = state; -+ const dataLength = data.length; -+ const threshold = onStartReachedThreshold * scrollLength; -+ resetSharedEdgeGateIfOutsideHysteresis(ctx); -+ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { -+ state.isStartReached = false; -+ state.startReachedSnapshot = void 0; - } +- } - if (viewPosition !== void 0 && index !== void 0) { - const dataLength = state.props.data.length; - if (dataLength === 0) { @@ -8023,49 +9554,13 @@ index 914d2da..ce6a0d7 100644 - const footerSize = peek$(ctx, "footerSize") || 0; - offset += footerSize; - } -+ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); -+ set$(ctx, "isNearStart", scroll <= threshold); -+ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; -+ if (!shouldSkipThresholdChecks) { -+ state.isStartReached = checkThreshold( -+ scroll, -+ false, -+ threshold, -+ state.isStartReached, -+ startReachedSnapshot, -+ { -+ contentSize: totalSize, -+ dataLength, -+ scrollPosition: scroll -+ }, -+ (distance) => { -+ var _a3, _b; -+ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { -+ markReachedEdge(ctx); -+ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); -+ } -+ }, -+ (snapshot) => { -+ state.startReachedSnapshot = snapshot; -+ } -+ ); - } +- } - return offset; - } - +-} +- -// src/core/clampScrollOffset.ts -function clampScrollOffset(ctx, offset, scrollTarget) { -+// src/utils/checkThresholds.ts -+function checkThresholds(ctx, allowedEdge) { -+ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; -+ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+} -+ -+// src/core/recalculateSettledScroll.ts -+function recalculateSettledScroll(ctx) { -+ var _a3, _b; - const state = ctx.state; +- const state = ctx.state; - const contentSize = getContentSize(ctx); - let clampedOffset = offset; - if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { @@ -8074,19 +9569,136 @@ index 914d2da..ce6a0d7 100644 - const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; - const maxOffset = baseMaxOffset + extraEndOffset; - clampedOffset = Math.min(offset, maxOffset); -+ if ((_a3 = state.props) == null ? void 0 : _a3.data) { -+ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); - } +- } - clampedOffset = Math.max(0, clampedOffset); - return clampedOffset; -+ checkThresholds(ctx); - } - - // src/core/finishScrollTo.ts -@@ -1413,16 +1029,191 @@ function listenForScrollEnd(ctx, params) { - if (idleTimeout !== void 0) { - clearTimeout(idleTimeout); - } +-} +- +-// src/core/finishScrollTo.ts +-function finishScrollTo(ctx) { +- var _a3, _b; +- const state = ctx.state; +- if (state == null ? void 0 : state.scrollingTo) { +- cancelScrollCompletionChecks(state); +- const resolvePendingScroll = state.pendingScrollResolve; +- state.pendingScrollResolve = void 0; +- const scrollingTo = state.scrollingTo; +- state.scrollHistory.length = 0; +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- if (state.pendingTotalSize !== void 0) { +- addTotalSize(ctx, null, state.pendingTotalSize); +- } +- { +- state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); +- } +- if (scrollingTo.isInitialScroll || state.initialScroll) { +- const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; +- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; +- finishInitialScroll(ctx, { +- onFinished: () => { +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +- }, +- preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, +- recalculateItems: true, +- schedulePreservedTargetClear: shouldPreserveResizeTarget, +- syncObservedOffset: isOffsetSession, +- waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame +- }); +- return; +- } +- recalculateSettledScroll(ctx); +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +- } +-} +- +-// src/core/doScrollTo.ts +-var SCROLL_END_IDLE_MS = 80; +-var SCROLL_END_MAX_MS = 1500; +-var SMOOTH_SCROLL_DURATION_MS = 320; +-var SCROLL_END_TARGET_EPSILON = 1; +-function doScrollTo(ctx, params) { +- var _a3, _b; +- const state = ctx.state; +- const { animated, horizontal, offset } = params; +- state.scheduledWork.cancel("platformScrollCompletion"); +- const scroller = state.refScroller.current; +- const node = scroller == null ? void 0 : scroller.getScrollableNode(); +- if (!scroller || !node) { +- return; +- } +- const isAnimated = !!animated; +- const isHorizontal = !!horizontal; +- const contentSize = isHorizontal ? getContentSize(ctx) : void 0; +- const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; +- const top = isHorizontal ? 0 : offset; +- scroller.scrollTo({ animated: isAnimated, x: left, y: top }); +- if (isAnimated) { +- const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; +- listenForScrollEnd(ctx, { +- readOffset: () => scroller.getCurrentScrollOffset(), +- target, +- targetOffset: offset +- }); +- } else { +- state.scroll = offset; +- const targetToken = state.scrollingTo; +- state.scheduledWork.timeout( +- () => { +- if (targetToken === state.scrollingTo) { +- finishScrollTo(ctx); +- } +- }, +- 100, +- "platformScrollCompletion" +- ); +- } +-} +-function listenForScrollEnd(ctx, params) { +- const { readOffset, target, targetOffset } = params; +- if (!target) { +- finishScrollTo(ctx); +- return; +- } +- const supportsScrollEnd = "onscrollend" in target; +- let idleTimeout; +- let settled = false; +- const { scheduledWork, scrollingTo: targetToken } = ctx.state; +- const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); +- const cleanup = () => { +- target.removeEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.removeEventListener("scrollend", onScrollEnd); +- } +- if (idleTimeout !== void 0) { +- clearTimeout(idleTimeout); +- } +- clearTimeout(maxTimeout); +- }; +- const cancel = () => { +- if (!settled) { +- settled = true; +- cleanup(); +- } +- }; +- const finish = (reason) => { +- if (settled) return; +- if (targetToken !== ctx.state.scrollingTo) { +- scheduledWork.cancel("platformScrollCompletion"); +- return; +- } +- const currentOffset = readOffset(); +- const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; +- if (reason === "scrollend" && !isNearTarget) { +- return; +- } +- scheduledWork.cancel("platformScrollCompletion"); +- finishScrollTo(ctx); +- }; +- const onScroll2 = () => { +- if (idleTimeout !== void 0) { +- clearTimeout(idleTimeout); +- } - idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); - }; - const onScrollEnd = () => finish("scrollend"); @@ -8095,196 +9707,38 @@ index 914d2da..ce6a0d7 100644 - target.addEventListener("scrollend", onScrollEnd); - } else { - idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); -+ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); -+ }; -+ const onScrollEnd = () => finish("scrollend"); -+ target.addEventListener("scroll", onScroll2); -+ if (supportsScrollEnd) { -+ target.addEventListener("scrollend", onScrollEnd); -+ } else { -+ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); -+ } -+ scheduledWork.register("platformScrollCompletion", cancel); -+} -+ -+// src/core/initialScrollSession.ts -+var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -+function hasInitialScrollSessionCompletion(completion) { -+ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); -+} -+function clearInitialScrollSession(state) { -+ state.initialScrollSession = void 0; -+ return void 0; -+} -+function createInitialScrollSession(options) { -+ const { bootstrap, completion, kind, previousDataLength } = options; -+ return kind === "offset" ? { -+ completion, -+ kind, -+ previousDataLength -+ } : { -+ bootstrap, -+ completion, -+ kind, -+ previousDataLength -+ }; -+} -+function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { -+ var _a4, _b2; -+ if (!state.initialScrollSession) { -+ state.initialScrollSession = createInitialScrollSession({ -+ completion: {}, -+ kind, -+ previousDataLength: 0 -+ }); -+ } else if (state.initialScrollSession.kind !== kind) { -+ state.initialScrollSession = createInitialScrollSession({ -+ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, -+ completion: state.initialScrollSession.completion, -+ kind, -+ previousDataLength: state.initialScrollSession.previousDataLength -+ }); -+ } -+ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; -+ return state.initialScrollSession.completion; -+} -+var initialScrollCompletion = { -+ didDispatchNativeScroll(state) { -+ var _a3, _b; -+ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); -+ }, -+ didRetrySilentInitialScroll(state) { -+ var _a3, _b; -+ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); -+ }, -+ markInitialScrollNativeDispatch(state) { -+ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; -+ }, -+ markSilentInitialScrollRetry(state) { -+ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; -+ }, -+ resetFlags(state) { -+ if (!state.initialScrollSession) { -+ return; -+ } -+ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); -+ completion.didDispatchNativeScroll = void 0; -+ completion.didRetrySilentInitialScroll = void 0; -+ } -+}; -+var initialScrollWatchdog = { -+ clear(state) { -+ initialScrollWatchdog.set(state, void 0); -+ }, -+ didReachTarget(newScroll, watchdog) { -+ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); -+ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ get(state) { -+ var _a3, _b; -+ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; -+ }, -+ hasNonZeroTargetOffset(targetOffset) { -+ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ isAtZeroTargetOffset(targetOffset) { -+ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ set(state, watchdog) { -+ var _a3, _b; -+ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { -+ return; -+ } -+ const completion = ensureInitialScrollSessionCompletion(state); -+ completion.watchdog = watchdog ? { -+ startScroll: watchdog.startScroll, -+ targetOffset: watchdog.targetOffset -+ } : void 0; -+ } -+}; -+function setInitialScrollSession(state, options = {}) { -+ var _a3, _b, _c, _d; -+ const existingSession = state.initialScrollSession; -+ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; -+ const completion = existingSession == null ? void 0 : existingSession.completion; -+ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; -+ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; -+ if (!kind) { -+ return clearInitialScrollSession(state); -+ } -+ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { -+ return clearInitialScrollSession(state); -+ } -+ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; -+ state.initialScrollSession = createInitialScrollSession({ -+ bootstrap, -+ completion, -+ kind, -+ previousDataLength -+ }); -+ return state.initialScrollSession; -+} -+var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; -+var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; -+var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; -+function clearAdaptiveRenderExitTimeout(ctx) { -+ ctx.state.scheduledWork.cancel("adaptiveRender"); -+} -+function scheduleAdaptiveRenderExit(ctx, exitDelay) { -+ const state = ctx.state; -+ clearAdaptiveRenderExitTimeout(ctx); -+ if (exitDelay <= 0) { -+ setAdaptiveRender(ctx, "normal", "scroll"); -+ } else { -+ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); -+ } -+} -+function setAdaptiveRender(ctx, mode, reason) { -+ var _a3, _b; -+ const previousMode = peek$(ctx, "adaptiveRender"); -+ if (previousMode !== mode) { -+ set$(ctx, "adaptiveRender", mode); -+ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); -+ } -+} -+function resetAdaptiveRender(ctx) { -+ var _a3, _b; -+ clearAdaptiveRenderExitTimeout(ctx); -+ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; -+ if (peek$(ctx, "adaptiveRender") !== mode) { -+ setAdaptiveRender(ctx, mode, "initial"); -+ } -+} -+function updateAdaptiveRender(ctx, scrollVelocity, options) { -+ var _a3, _b, _c; -+ const state = ctx.state; -+ const adaptiveRender = state.props.adaptiveRender; -+ const currentMode = peek$(ctx, "adaptiveRender"); -+ if (peek$(ctx, "readyToRender")) { -+ if (adaptiveRender) { -+ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; -+ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; -+ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; -+ const threshold = currentMode === "light" ? exitVelocity : enterVelocity; -+ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; -+ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; -+ if (nextMode !== previousMode) { -+ if (nextMode === "light") { -+ setAdaptiveRender(ctx, "light", "scroll"); -+ scheduleAdaptiveRenderExit(ctx, exitDelay); -+ } else if (currentMode === "light") { -+ scheduleAdaptiveRenderExit(ctx, exitDelay); -+ } -+ } -+ } else { -+ resetAdaptiveRender(ctx); -+ } - } +- } - scheduledWork.register("platformScrollCompletion", cancel); - } - - // src/core/doMaintainScrollAtEnd.ts -@@ -1451,6 +1242,7 @@ function doMaintainScrollAtEnd(ctx) { +-} +- +-// src/core/doMaintainScrollAtEnd.ts +-function doMaintainScrollAtEnd(ctx) { +- const state = ctx.state; +- const { +- didContainersLayout, +- pendingNativeMVCPAdjust, +- refScroller, +- props: { maintainScrollAtEnd } +- } = state; +- const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); +- const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout); +- if (pendingNativeMVCPAdjust) { +- state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd; +- return false; +- } +- if (shouldMaintainScrollAtEnd) { +- state.pendingMaintainScrollAtEnd = false; +- const contentSize = getContentSize(ctx); +- if (contentSize < state.scrollLength) { +- state.scroll = 0; ++ if (shouldMaintainScrollAtEnd) { ++ state.pendingMaintainScrollAtEnd = false; ++ const contentSize = getContentSize(ctx); ++ if (contentSize < state.scrollLength) { ++ state.scroll = 0; + } + if (!state.maintainingScrollAtEnd) { + const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; state.maintainingScrollAtEnd = pendingState; @@ -8292,10 +9746,27 @@ index 914d2da..ce6a0d7 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1809,6 +1601,27 @@ function prepareMVCP(ctx, dataChanged) { +@@ -1809,23 +1601,44 @@ function prepareMVCP(ctx, dataChanged) { } } +-// src/utils/getScrollVelocity.ts +-var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; +-var SCROLL_VELOCITY_HALF_LIFE_MS = 200; +-var getScrollVelocity = (state) => { +- const { scrollHistory } = state; +- const newestIndex = scrollHistory.length - 1; +- if (newestIndex < 1) { +- return 0; +- } +- const newest = scrollHistory[newestIndex]; +- if (Date.now() - newest.time > MAX_SCROLL_VELOCITY_WINDOW_MS) { +- return 0; +- } +- let direction = 0; +- let weightedVelocity = 0; +- let totalWeight = 0; +- for (let i = newestIndex; i > 0; i--) { +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { @@ -8317,69 +9788,269 @@ index 914d2da..ce6a0d7 100644 + }, "fullDrawDistancePrewarm"); +} + - // src/utils/getScrollVelocity.ts - var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; - var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -1906,6 +1719,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { - state.scrollPrevTime = state.scrollTime; - state.scroll = newScroll; - state.scrollTime = currentTime; -+ if ((options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust) { -+ releaseScrollTargetSettleIfMoved(ctx); -+ } - const scrollDelta = Math.abs(newScroll - prevScroll); - const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; - const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; -@@ -2026,99 +1842,417 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { - if (start === void 0) { - return void 0; - } -- if (targetIndex !== void 0 && state.positions[start] === void 0) { -- return { end: start, start }; -+ if (targetIndex !== void 0 && state.positions[start] === void 0) { -+ return { end: start, start }; ++// src/utils/getScrollVelocity.ts ++var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; ++var SCROLL_VELOCITY_HALF_LIFE_MS = 200; ++var getScrollVelocity = (state) => { ++ const { scrollHistory } = state; ++ const newestIndex = scrollHistory.length - 1; ++ if (newestIndex < 1) { ++ return 0; + } -+ if (targetIndex === void 0) { -+ const startBottom = getItemBottom(ctx, start); -+ if (startBottom === void 0 || startBottom <= viewportStart) { -+ return void 0; -+ } ++ const newest = scrollHistory[newestIndex]; ++ if (Date.now() - newest.time > MAX_SCROLL_VELOCITY_WINDOW_MS) { ++ return 0; + } -+ while (start > 0) { -+ const top = state.positions[start]; -+ if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { -+ break; -+ } -+ start--; ++ let direction = 0; ++ let weightedVelocity = 0; ++ let totalWeight = 0; ++ for (let i = newestIndex; i > 0; i--) { + const current = scrollHistory[i]; + const previous = scrollHistory[i - 1]; + const scrollDiff = current.scroll - previous.scroll; +@@ -1844,281 +1657,604 @@ var getScrollVelocity = (state) => { + if (scrollDiff === 0 || timeDiff <= 0) { + continue; + } +- const age = newest.time - current.time; +- const weight = Math.exp(-age / SCROLL_VELOCITY_HALF_LIFE_MS); +- weightedVelocity += scrollDiff / timeDiff * weight; +- totalWeight += weight; +- } +- return totalWeight > 0 ? weightedVelocity / totalWeight : 0; +-}; +- +-// src/utils/hasActiveMVCPAnchorLock.ts +-function hasActiveMVCPAnchorLock(state) { +- const lock = state.mvcpAnchorLock; +- if (!lock) { +- return false; ++ const age = newest.time - current.time; ++ const weight = Math.exp(-age / SCROLL_VELOCITY_HALF_LIFE_MS); ++ weightedVelocity += scrollDiff / timeDiff * weight; ++ totalWeight += weight; + } -+ while (start > 0) { -+ const previousBottom = getItemBottom(ctx, start - 1); -+ if (previousBottom === void 0 || previousBottom <= viewportStart) { -+ break; -+ } -+ start--; ++ return totalWeight > 0 ? weightedVelocity / totalWeight : 0; ++}; ++ ++// src/utils/hasActiveMVCPAnchorLock.ts ++function hasActiveMVCPAnchorLock(state) { ++ const lock = state.mvcpAnchorLock; ++ if (!lock) { ++ return false; + } -+ let end = start; -+ while (end + 1 < dataLength) { -+ const nextTop = state.positions[end + 1]; -+ if (nextTop === void 0 || nextTop > viewportEnd) { -+ break; -+ } -+ end++; ++ if (Date.now() > lock.expiresAt) { ++ state.mvcpAnchorLock = void 0; ++ return false; + } -+ return { end, start }; ++ return true; +} -+function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -+ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -+ if (range) { -+ ctx.state.scrollTargetPinnedRange = range; -+ ctx.state.scrollForNextCalculateItemsInView = void 0; -+ } else { -+ ctx.state.scrollTargetPinnedRange = void 0; -+ } ++ ++// src/utils/isInMVCPActiveMode.ts ++function isInMVCPActiveMode(state) { ++ return state.dataChangeNeedsScrollUpdate || hasActiveMVCPAnchorLock(state); +} -+function scrollTo(ctx, params) { -+ var _a3, _b, _c; ++ ++// src/core/updateScroll.ts ++function updateScroll(ctx, newScroll, forceUpdate, options) { ++ var _a3; ++ const state = ctx.state; ++ const { ignoreScrollFromMVCP, lastScrollAdjustForHistory, scrollAdjustHandler, scrollHistory, scrollingTo } = state; ++ const prevScroll = state.scroll; ++ if ((options == null ? void 0 : options.markHasScrolled) !== false) { ++ state.hasScrolled = true; ++ } ++ const currentTime = Date.now(); ++ state.lastBatchingAction = currentTime; ++ const adjust = scrollAdjustHandler.getAdjust(); ++ const adjustChanged = lastScrollAdjustForHistory !== void 0 && Math.abs(adjust - lastScrollAdjustForHistory) > 0.1; ++ if (adjustChanged) { ++ scrollHistory.length = 0; ++ } ++ state.lastScrollAdjustForHistory = adjust; ++ if (scrollingTo === void 0 && !(scrollHistory.length === 0 && newScroll === state.scroll)) { ++ if (!adjustChanged) { ++ scrollHistory.push({ scroll: newScroll, time: currentTime }); ++ } ++ } ++ if (scrollHistory.length > 5) { ++ scrollHistory.shift(); ++ } ++ if (ignoreScrollFromMVCP && !scrollingTo) { ++ const { lt, gt } = ignoreScrollFromMVCP; ++ if (lt && newScroll < lt || gt && newScroll > gt) { ++ state.ignoreScrollFromMVCPIgnored = true; ++ return; ++ } ++ } ++ state.scrollPrev = prevScroll; ++ state.scrollPrevTime = state.scrollTime; ++ state.scroll = newScroll; ++ state.scrollTime = currentTime; ++ const scrollDelta = Math.abs(newScroll - prevScroll); ++ const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; ++ const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; ++ const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); ++ const scrollLength = state.scrollLength; ++ const isLargeUserScrollJump = scrollLength > 0 && scrollingTo === void 0 && scrollDelta > scrollLength && !state.pendingNativeMVCPAdjust; ++ const scrollVelocity = getScrollVelocity(state); ++ updateAdaptiveRender(ctx, scrollVelocity, { forceLight: isLargeUserScrollJump }); ++ const lastCalculated = state.scrollLastCalculate; ++ const useAggressiveItemRecalculation = isInMVCPActiveMode(state); ++ const shouldUpdate = useAggressiveItemRecalculation || didResolvePendingNativeMVCPAdjust || allowedEdge !== void 0 || forceUpdate || lastCalculated === void 0 || Math.abs(state.scroll - lastCalculated) > 2; ++ if (shouldUpdate) { ++ state.scrollLastCalculate = state.scroll; ++ state.ignoreScrollFromMVCPIgnored = false; ++ state.lastScrollDelta = scrollDelta; ++ const runCalculateItems = () => { ++ var _a4; ++ const calculateItemsParams = { ++ doMVCP: scrollingTo !== void 0, ++ scrollVelocity ++ }; ++ if (isLargeUserScrollJump) { ++ calculateItemsParams.drawDistanceMode = "visible-first"; ++ } ++ (_a4 = state.triggerCalculateItemsInView) == null ? void 0 : _a4.call(state, calculateItemsParams); ++ checkThresholds(ctx, allowedEdge); ++ }; ++ if (isLargeUserScrollJump) { ++ state.mvcpAnchorLock = void 0; ++ state.pendingNativeMVCPAdjust = void 0; ++ state.userScrollAnchorReset = { keys: /* @__PURE__ */ new Set() }; ++ state.scheduledWork.cancel("mvcpRecalculate"); ++ ReactDOM.flushSync(runCalculateItems); ++ scheduleFullDrawDistancePrewarm(ctx); ++ } else { ++ runCalculateItems(); ++ } ++ const shouldMaintainScrollAtEndAfterPendingSettle = !!state.pendingMaintainScrollAtEnd || !!((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onDataChange); ++ if (didResolvePendingNativeMVCPAdjust && shouldMaintainScrollAtEndAfterPendingSettle) { ++ state.pendingMaintainScrollAtEnd = false; ++ doMaintainScrollAtEnd(ctx); ++ } ++ state.dataChangeNeedsScrollUpdate = false; ++ state.lastScrollDelta = 0; ++ } ++} ++ ++// src/core/scrollTo.ts ++function getAverageSizeSnapshot(state) { ++ if (Object.keys(state.averageSizes).length === 0) { ++ return void 0; ++ } ++ const snapshot = {}; ++ for (const itemType in state.averageSizes) { ++ const averages = state.averageSizes[itemType]; ++ snapshot[itemType] = averages.avg; ++ } ++ return snapshot; ++} ++function syncInitialScrollNativeWatchdog(state, options) { ++ var _a3; ++ const { isInitialScroll, requestedOffset, targetOffset } = options; ++ const existingWatchdog = initialScrollWatchdog.get(state); ++ const shouldWatchInitialNativeScroll = !state.didFinishInitialScroll && (isInitialScroll || !!existingWatchdog) && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); ++ const shouldClearInitialNativeScrollWatchdog = !state.didFinishInitialScroll && !!existingWatchdog && initialScrollWatchdog.isAtZeroTargetOffset(requestedOffset); ++ if (shouldWatchInitialNativeScroll) { ++ state.hasScrolled = false; ++ initialScrollWatchdog.set(state, { ++ startScroll: (_a3 = existingWatchdog == null ? void 0 : existingWatchdog.startScroll) != null ? _a3 : state.scroll, ++ targetOffset ++ }); ++ return; ++ } ++ if (shouldClearInitialNativeScrollWatchdog) { ++ initialScrollWatchdog.clear(state); ++ } ++} ++function findPositionIndexAtOrBeforeOffset(ctx, offset) { ++ const state = ctx.state; ++ const dataLength = state.props.data.length; ++ let low = 0; ++ let high = dataLength - 1; ++ let match; ++ while (low <= high) { ++ const mid = Math.floor((low + high) / 2); ++ const top = state.positions[mid]; ++ if (top === void 0) { ++ high = mid - 1; ++ } else { ++ if (top <= offset) { ++ match = mid; ++ low = mid + 1; ++ } else { ++ high = mid - 1; ++ } ++ } ++ } ++ return match; ++} ++function getItemBottom(ctx, index) { ++ var _a3; ++ const top = ctx.state.positions[index]; ++ if (top === void 0) { ++ return void 0; ++ } ++ const itemSize = (_a3 = getItemSizeAtIndex(ctx, index)) != null ? _a3 : 0; ++ return top + (Number.isFinite(itemSize) ? itemSize : 0); ++} ++function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { ++ const state = ctx.state; ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return void 0; ++ } ++ const viewportStart = Math.max(0, targetOffset); ++ const viewportEnd = Math.max(viewportStart, targetOffset + state.scrollLength); ++ let start = targetIndex !== void 0 ? Math.max(0, Math.min(dataLength - 1, targetIndex)) : findPositionIndexAtOrBeforeOffset(ctx, viewportStart); ++ if (start === void 0) { ++ return void 0; ++ } ++ if (targetIndex !== void 0 && state.positions[start] === void 0) { ++ return { end: start, start }; ++ } ++ if (targetIndex === void 0) { ++ const startBottom = getItemBottom(ctx, start); ++ if (startBottom === void 0 || startBottom <= viewportStart) { ++ return void 0; ++ } ++ } ++ while (start > 0) { ++ const top = state.positions[start]; ++ if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { ++ break; ++ } ++ start--; ++ } ++ while (start > 0) { ++ const previousBottom = getItemBottom(ctx, start - 1); ++ if (previousBottom === void 0 || previousBottom <= viewportStart) { ++ break; ++ } ++ start--; ++ } ++ let end = start; ++ while (end + 1 < dataLength) { ++ const nextTop = state.positions[end + 1]; ++ if (nextTop === void 0 || nextTop > viewportEnd) { ++ break; ++ } ++ end++; ++ } ++ return { end, start }; ++} ++function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { ++ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); ++ if (range) { ++ ctx.state.scrollTargetPinnedRange = range; ++ ctx.state.scrollForNextCalculateItemsInView = void 0; ++ } else { ++ ctx.state.scrollTargetPinnedRange = void 0; ++ } ++} ++function scrollTo(ctx, params) { ++ var _a3, _b, _c; + const state = ctx.state; + const { noScrollingTo, forceScroll, ...scrollTarget } = params; + const { @@ -8432,23 +10103,36 @@ index 914d2da..ce6a0d7 100644 + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); + } -+ } + } +- if (Date.now() > lock.expiresAt) { +- state.mvcpAnchorLock = void 0; +- return false; + if (forceScroll || !isInitialScroll || Platform.OS === "android") { + doScrollTo(ctx, { animated, horizontal, offset }); + } else { + state.scroll = offset; -+ } -+} -+ + } +- return true; + } + +-// src/utils/isInMVCPActiveMode.ts +-function isInMVCPActiveMode(state) { +- return state.dataChangeNeedsScrollUpdate || hasActiveMVCPAnchorLock(state); +// src/core/scrollTargetSettle.ts +var SETTLE_POSITION_EPSILON = 0.5; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; +var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; ++function releaseScrollTargetForUserInteraction(ctx) { ++ if (ctx.state.scrollTargetSettle) { ++ clearScrollTargetSettle(ctx.state); ++ } ++} +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; + state.scheduledWork.cancel("scrollTargetSettle"); ++ state.scheduledWork.cancel("scrollTargetSettleDeadline"); +} +function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; @@ -8465,35 +10149,56 @@ index 914d2da..ce6a0d7 100644 + deadline: now + SETTLE_MAX_MS, + expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), ++ measuredIndex: void 0, + quietPasses: 0, -+ requestedAt: now, + viewOffset, + viewPosition + }; -+} ++ state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline"); + } +- +-// src/core/updateScroll.ts +-function updateScroll(ctx, newScroll, forceUpdate, options) { +function getSettleTargetOffset(ctx, settle, index, position) { + const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; + return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} +function applyScrollTargetCorrection(ctx, id) { -+ var _a3; -+ const state = ctx.state; + var _a3; + const state = ctx.state; +- const { ignoreScrollFromMVCP, lastScrollAdjustForHistory, scrollAdjustHandler, scrollHistory, scrollingTo } = state; +- const prevScroll = state.scroll; +- if ((options == null ? void 0 : options.markHasScrolled) !== false) { +- state.hasScrolled = true; + const settle = state.scrollTargetSettle; + if (!settle || settle.id !== id) { + return; -+ } + } +- const currentTime = Date.now(); +- state.lastBatchingAction = currentTime; +- const adjust = scrollAdjustHandler.getAdjust(); +- const adjustChanged = lastScrollAdjustForHistory !== void 0 && Math.abs(adjust - lastScrollAdjustForHistory) > 0.1; +- if (adjustChanged) { +- scrollHistory.length = 0; + const index = state.indexByKey.get(id); + const position = index === void 0 ? void 0 : state.positions[index]; + if (index === void 0 || position === void 0 || Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return; -+ } + } +- state.lastScrollAdjustForHistory = adjust; +- if (scrollingTo === void 0 && !(scrollHistory.length === 0 && newScroll === state.scroll)) { +- if (!adjustChanged) { +- scrollHistory.push({ scroll: newScroll, time: currentTime }); +- } + if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { + clearScrollTargetSettle(state); + return; -+ } + } +- if (scrollHistory.length > 5) { +- scrollHistory.shift(); + settle.corrections++; -+ settle.requestedAt = Date.now(); ++ settle.measuredIndex = void 0; + scrollTo(ctx, { + animated: false, + index, @@ -8508,21 +10213,71 @@ index 914d2da..ce6a0d7 100644 + }); + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +} -+function settleScrollTarget(ctx, isCompensating) { ++function settleScrollTarget(ctx, options) { ++ var _a3; + const state = ctx.state; + const settle = state.scrollTargetSettle; + if (!settle) { + return false; + } +- if (ignoreScrollFromMVCP && !scrollingTo) { +- const { lt, gt } = ignoreScrollFromMVCP; +- if (lt && newScroll < lt || gt && newScroll > gt) { +- state.ignoreScrollFromMVCPIgnored = true; +- return; ++ const measured = options == null ? void 0 : options.minIndexSizeChanged; ++ if (measured !== void 0) { ++ settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); + } -+ if (isCompensating) { ++ if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return false; -+ } + } + settle.quietPasses = 0; + settle.expiresAt = Date.now() + SETTLE_TTL_MS; + return false; -+ } + } +- state.scrollPrev = prevScroll; +- state.scrollPrevTime = state.scrollTime; +- state.scroll = newScroll; +- state.scrollTime = currentTime; +- const scrollDelta = Math.abs(newScroll - prevScroll); +- const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; +- const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; +- const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); +- const scrollLength = state.scrollLength; +- const isLargeUserScrollJump = scrollLength > 0 && scrollingTo === void 0 && scrollDelta > scrollLength && !state.pendingNativeMVCPAdjust; +- const scrollVelocity = getScrollVelocity(state); +- updateAdaptiveRender(ctx, scrollVelocity, { forceLight: isLargeUserScrollJump }); +- const lastCalculated = state.scrollLastCalculate; +- const useAggressiveItemRecalculation = isInMVCPActiveMode(state); +- const shouldUpdate = useAggressiveItemRecalculation || didResolvePendingNativeMVCPAdjust || allowedEdge !== void 0 || forceUpdate || lastCalculated === void 0 || Math.abs(state.scroll - lastCalculated) > 2; +- if (shouldUpdate) { +- state.scrollLastCalculate = state.scroll; +- state.ignoreScrollFromMVCPIgnored = false; +- state.lastScrollDelta = scrollDelta; +- const runCalculateItems = () => { +- var _a4; +- const calculateItemsParams = { +- doMVCP: scrollingTo !== void 0, +- scrollVelocity +- }; +- if (isLargeUserScrollJump) { +- calculateItemsParams.drawDistanceMode = "visible-first"; +- } +- (_a4 = state.triggerCalculateItemsInView) == null ? void 0 : _a4.call(state, calculateItemsParams); +- checkThresholds(ctx, allowedEdge); +- }; +- if (isLargeUserScrollJump) { +- state.mvcpAnchorLock = void 0; +- state.pendingNativeMVCPAdjust = void 0; +- state.userScrollAnchorReset = { keys: /* @__PURE__ */ new Set() }; +- state.scheduledWork.cancel("mvcpRecalculate"); +- ReactDOM.flushSync(runCalculateItems); +- scheduleFullDrawDistancePrewarm(ctx); +- } else { +- runCalculateItems(); + const index = state.indexByKey.get(settle.id); + const position = index === void 0 ? void 0 : state.positions[index]; + if (index === void 0 || position === void 0) { @@ -8534,13 +10289,21 @@ index 914d2da..ce6a0d7 100644 + clearScrollTargetSettle(state); + return false; + } ++ if (settle.measuredIndex === void 0 || settle.measuredIndex > index) { ++ return false; ++ } + const targetOffset = getSettleTargetOffset(ctx, settle, index, position); + const diff = targetOffset - state.scroll; + if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ settle.measuredIndex = void 0; + settle.quietPasses++; + if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { + clearScrollTargetSettle(state); -+ } + } +- const shouldMaintainScrollAtEndAfterPendingSettle = !!state.pendingMaintainScrollAtEnd || !!((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onDataChange); +- if (didResolvePendingNativeMVCPAdjust && shouldMaintainScrollAtEndAfterPendingSettle) { +- state.pendingMaintainScrollAtEnd = false; +- doMaintainScrollAtEnd(ctx); + return false; + } + settle.quietPasses = 0; @@ -8548,17 +10311,6 @@ index 914d2da..ce6a0d7 100644 + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} -+var SETTLE_ECHO_WINDOW_MS = 150; -+function releaseScrollTargetSettleIfMoved(ctx, _scrollOffset) { -+ const state = ctx.state; -+ const settle = state.scrollTargetSettle; -+ if (!settle) { -+ return; -+ } -+ if (Date.now() - settle.requestedAt > SETTLE_ECHO_WINDOW_MS) { -+ clearScrollTargetSettle(state); -+ } -+} + +// src/core/cancelImperativeScroll.ts +function cancelScrollCompletionChecks({ scheduledWork }) { @@ -8590,7 +10342,9 @@ index 914d2da..ce6a0d7 100644 + nativeEvent: { + ...event.nativeEvent, + contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -+ } + } +- state.dataChangeNeedsScrollUpdate = false; +- state.lastScrollDelta = 0; + }; +} +function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { @@ -8607,9 +10361,13 @@ index 914d2da..ce6a0d7 100644 + (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 + ) + ); -+ } -+} -+ + } + } + +-// src/core/scrollTo.ts +-function getAverageSizeSnapshot(state) { +- if (Object.keys(state.averageSizes).length === 0) { +- return void 0; +// src/utils/setInitialRenderState.ts +function resetInitialRenderState(ctx, { + resetLayout, @@ -8619,18 +10377,31 @@ index 914d2da..ce6a0d7 100644 + if (resetLayout) { + state.didContainersLayout = false; + state.queuedInitialLayout = false; -+ } + } +- const snapshot = {}; +- for (const itemType in state.averageSizes) { +- const averages = state.averageSizes[itemType]; +- snapshot[itemType] = averages.avg; + if (resetInitialScroll) { + state.didFinishInitialScroll = false; } -- if (targetIndex === void 0) { -- const startBottom = getItemBottom(ctx, start); -- if (startBottom === void 0 || startBottom <= viewportStart) { -- return void 0; -- } +- return snapshot; + set$(ctx, "readyToRender", false); + resetAdaptiveRender(ctx); -+} + } +-function syncInitialScrollNativeWatchdog(state, options) { +- var _a3; +- const { isInitialScroll, requestedOffset, targetOffset } = options; +- const existingWatchdog = initialScrollWatchdog.get(state); +- const shouldWatchInitialNativeScroll = !state.didFinishInitialScroll && (isInitialScroll || !!existingWatchdog) && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); +- const shouldClearInitialNativeScrollWatchdog = !state.didFinishInitialScroll && !!existingWatchdog && initialScrollWatchdog.isAtZeroTargetOffset(requestedOffset); +- if (shouldWatchInitialNativeScroll) { +- state.hasScrolled = false; +- initialScrollWatchdog.set(state, { +- startScroll: (_a3 = existingWatchdog == null ? void 0 : existingWatchdog.startScroll) != null ? _a3 : state.scroll, +- targetOffset +- }); +- return; +function setInitialRenderState(ctx, { + didLayout, + didInitialScroll @@ -8643,51 +10414,53 @@ index 914d2da..ce6a0d7 100644 + if (didLayout) { + state.didContainersLayout = true; } -- while (start > 0) { -- const top = state.positions[start]; -- if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { -- break; -- } -- start--; +- if (shouldClearInitialNativeScrollWatchdog) { +- initialScrollWatchdog.clear(state); + if (didInitialScroll) { + state.didFinishInitialScroll = true; } -- while (start > 0) { -- const previousBottom = getItemBottom(ctx, start - 1); -- if (previousBottom === void 0 || previousBottom <= viewportStart) { -- break; +-} +-function findPositionIndexAtOrBeforeOffset(ctx, offset) { +- const state = ctx.state; +- const dataLength = state.props.data.length; +- let low = 0; +- let high = dataLength - 1; +- let match; +- while (low <= high) { +- const mid = Math.floor((low + high) / 2); +- const top = state.positions[mid]; +- if (top === void 0) { +- high = mid - 1; +- } else { +- if (top <= offset) { +- match = mid; +- low = mid + 1; +- } else { +- high = mid - 1; + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); + if (isReadyToRender && !peek$(ctx, "readyToRender")) { + set$(ctx, "readyToRender", true); + setAdaptiveRender(ctx, "normal", "ready"); + if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { + scheduleFullDrawDistancePrewarm(ctx); - } -- start--; -- } -- let end = start; -- while (end + 1 < dataLength) { -- const nextTop = state.positions[end + 1]; -- if (nextTop === void 0 || nextTop > viewportEnd) { -- break; ++ } + if (!state.didLoad) { + state.didLoad = true; + if (onLoad) { + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); -+ } + } } -- end++; } -- return { end, start }; +- return match; } --function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -- if (range) { -- ctx.state.scrollTargetPinnedRange = range; -- ctx.state.scrollForNextCalculateItemsInView = void 0; -- } else { -- ctx.state.scrollTargetPinnedRange = void 0; +-function getItemBottom(ctx, index) { +- var _a3; +- const top = ctx.state.positions[index]; +- if (top === void 0) { +- return void 0; - } +- const itemSize = (_a3 = getItemSizeAtIndex(ctx, index)) != null ? _a3 : 0; +- return top + (Number.isFinite(itemSize) ? itemSize : 0); + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -8696,8 +10469,7 @@ index 914d2da..ce6a0d7 100644 + state.scrollPending = offset; + state.scrollPrev = offset; } --function scrollTo(ctx, params) { -- var _a3, _b; +-function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -8708,6 +10480,74 @@ index 914d2da..ce6a0d7 100644 + setInitialScrollSession(state); +} +function supersedeInitialScroll(ctx) { ++ var _a3, _b, _c; + const state = ctx.state; +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return void 0; +- } +- const viewportStart = Math.max(0, targetOffset); +- const viewportEnd = Math.max(viewportStart, targetOffset + state.scrollLength); +- let start = targetIndex !== void 0 ? Math.max(0, Math.min(dataLength - 1, targetIndex)) : findPositionIndexAtOrBeforeOffset(ctx, viewportStart); +- if (start === void 0) { +- return void 0; +- } +- if (targetIndex !== void 0 && state.positions[start] === void 0) { +- return { end: start, start }; +- } +- if (targetIndex === void 0) { +- const startBottom = getItemBottom(ctx, start); +- if (startBottom === void 0 || startBottom <= viewportStart) { +- return void 0; +- } +- } +- while (start > 0) { +- const top = state.positions[start]; +- if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { +- break; +- } +- start--; +- } +- while (start > 0) { +- const previousBottom = getItemBottom(ctx, start - 1); +- if (previousBottom === void 0 || previousBottom <= viewportStart) { +- break; ++ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; ++ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { ++ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { ++ cancelAnimationFrame(bootstrapInitialScroll.frameHandle); + } +- start--; +- } +- let end = start; +- while (end + 1 < dataLength) { +- const nextTop = state.positions[end + 1]; +- if (nextTop === void 0 || nextTop > viewportEnd) { +- break; ++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { ++ cancelScrollCompletionChecks(state); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; + } +- end++; +- } +- return { end, start }; +-} +-function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); +- if (range) { +- ctx.state.scrollTargetPinnedRange = range; +- ctx.state.scrollForNextCalculateItemsInView = void 0; +- } else { +- ctx.state.scrollTargetPinnedRange = void 0; ++ initialScrollCompletion.resetFlags(state); ++ setInitialScrollSession(state, { bootstrap: null }); ++ finishInitialScroll(ctx); + } + } +-function scrollTo(ctx, params) { +- var _a3, _b; ++function finishInitialScroll(ctx, options) { + var _a3, _b, _c; const state = ctx.state; - const { noScrollingTo, forceScroll, ...scrollTarget } = params; @@ -8730,11 +10570,7 @@ index 914d2da..ce6a0d7 100644 - if (!noScrollingTo) { - if (isInitialScroll) { - initialScrollCompletion.resetFlags(state); -+ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; -+ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { -+ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { -+ cancelAnimationFrame(bootstrapInitialScroll.frameHandle); - } +- } - const averageSizeSnapshot = getAverageSizeSnapshot(state); - state.scrollingTo = { - ...scrollTarget, @@ -8744,19 +10580,6 @@ index 914d2da..ce6a0d7 100644 - }; - if (!isInitialScroll) { - pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); -+ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { -+ cancelScrollCompletionChecks(state); -+ state.scrollingTo = void 0; -+ state.scrollTargetPinnedRange = void 0; -+ } -+ initialScrollCompletion.resetFlags(state); -+ setInitialScrollSession(state, { bootstrap: null }); -+ finishInitialScroll(ctx); -+ } -+} -+function finishInitialScroll(ctx, options) { -+ var _a3, _b, _c; -+ const state = ctx.state; + if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { + syncInitialScrollOffset(state, options.resolvedOffset); + } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { @@ -8825,7 +10648,15 @@ index 914d2da..ce6a0d7 100644 } // src/core/scrollToIndex.ts -@@ -4367,8 +4501,17 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4350,6 +4486,7 @@ function calculateItemsInView(ctx, params = {}) { + startIndex + }); + totalSize = getContentSize(ctx); ++ const minIndexSizeChangedThisPass = dataChanged ? 0 : minIndexSizeChanged; + if (minIndexSizeChanged !== void 0) { + state.minIndexSizeChanged = void 0; + } +@@ -4367,8 +4504,17 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -8839,13 +10670,13 @@ index 914d2da..ce6a0d7 100644 + } + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { -+ settleScrollTarget(ctx, isCompensating); ++ settleScrollTarget(ctx, { isCompensating, minIndexSizeChanged: minIndexSizeChangedThisPass }); + } + if (didMVCPAdjust) { updateScroll2(state.scroll); updateScrollRange(); } -@@ -5915,6 +6058,119 @@ function useRafCoalescer(callback) { +@@ -5915,6 +6061,119 @@ function useRafCoalescer(callback) { return coalescer; } @@ -8965,16 +10796,18 @@ index 914d2da..ce6a0d7 100644 // src/components/webConstants.ts var LEGEND_LIST_CONTENT_CONTAINER_CLASS = "legend-list-content-container"; var LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS = "legend-list-scrollbar-x-hidden"; -@@ -6006,6 +6262,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -6006,6 +6265,10 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; +var SMOOTH_SCROLL_MAX_MS = 2e3; ++var BORROW_MEASURE_ATTEMPTS = 2; ++var USER_INTERACTION_EVENTS = ["wheel", "pointerdown", "touchstart", "keydown"]; var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6074,6 +6332,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6074,6 +6337,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -8986,33 +10819,47 @@ index 914d2da..ce6a0d7 100644 const getMaxScrollOffset = React3.useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6094,6 +6357,59 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,6 +6362,95 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); + const paddedNodeRef = React3.useRef(null); ++ const borrowedExtentsRef = React3.useRef(/* @__PURE__ */ new Map()); ++ const borrowIdRef = React3.useRef(0); ++ const borrowWatchRef = React3.useRef(0); + const animatedPaddingReleaseRef = React3.useRef(void 0); + const withReachableExtent = React3.useCallback( + (offset, maxOffset, animated, run) => { + var _a4; + const contentNode = contentRef.current; -+ const committedMaxOffset = getCommittedMaxScrollOffset(maxOffset); ++ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); ++ const committedMaxOffset = getCommittedMaxScrollOffset(getMaxScrollOffset()); + if (!contentNode || offset <= committedMaxOffset + SCROLL_EXTENT_EPSILON) { + run(clampOffset(offset, maxOffset)); + return; + } -+ const release = addTemporaryEndPadding( -+ contentNode, -+ paddingEndProp, -+ offset - committedMaxOffset + SCROLL_EXTENT_EPSILON -+ ); ++ const releases = []; ++ for (let attempt = 0; attempt < BORROW_MEASURE_ATTEMPTS; attempt++) { ++ const shortfall = offset - getMaxScrollOffset(); ++ if (shortfall <= 0) { ++ break; ++ } ++ releases.push(addTemporaryEndPadding(contentNode, paddingEndProp, shortfall + SCROLL_EXTENT_EPSILON)); ++ } ++ const borrowId = ++borrowIdRef.current; ++ const release = () => { ++ for (const releaseOne of releases) { ++ releaseOne(); ++ } ++ borrowedExtentsRef.current.delete(borrowId); ++ }; ++ borrowedExtentsRef.current.set(borrowId, Math.max(0, getMaxScrollOffset() - committedMaxOffset)); + paddedNodeRef.current = contentNode; + run(offset); + if (!animated) { + scheduleTemporaryEndPaddingRelease(contentNode, paddingEndProp, release); + return; + } -+ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); + const scrollTarget = getScrollTarget(); + const supportsScrollEnd = !!scrollTarget && "onscrollend" in scrollTarget; + const finish = () => { @@ -9021,21 +10868,40 @@ index 914d2da..ce6a0d7 100644 + } + animatedPaddingReleaseRef.current = void 0; + clearTimeout(settleTimeout); -+ scrollTarget == null ? void 0 : scrollTarget.removeEventListener("scrollend", finish); ++ scrollTarget == null ? void 0 : scrollTarget.removeEventListener("scrollend", finishIfArrived); ++ cancelAnimationFrame(borrowWatchRef.current); + release(); + }; ++ const finishIfArrived = () => { ++ if (Math.abs(getCurrentScrollOffset() - offset) <= SCROLL_EXTENT_EPSILON) { ++ finish(); ++ } ++ }; ++ const releaseWhenContentCommits = () => { ++ if (animatedPaddingReleaseRef.current !== finish) { ++ return; ++ } ++ if (getCommittedMaxScrollOffset(getMaxScrollOffset()) + SCROLL_EXTENT_EPSILON >= offset) { ++ finish(); ++ return; ++ } ++ borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); ++ }; + animatedPaddingReleaseRef.current = finish; ++ cancelAnimationFrame(borrowWatchRef.current); ++ borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); + const settleTimeout = setTimeout(finish, SMOOTH_SCROLL_MAX_MS); + if (supportsScrollEnd) { -+ scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finish); ++ scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finishIfArrived); + } + }, -+ [getCommittedMaxScrollOffset, getScrollTarget, paddingEndProp] ++ [getCommittedMaxScrollOffset, getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, paddingEndProp] + ); + React3.useEffect( + () => () => { + var _a4; + (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); ++ cancelAnimationFrame(borrowWatchRef.current); + const paddedNode = paddedNodeRef.current; + if (paddedNode) { + releaseAllTemporaryEndPadding(paddedNode); @@ -9043,10 +10909,13 @@ index 914d2da..ce6a0d7 100644 + }, + [] + ); ++ const reportUserInteraction = React3.useCallback(() => { ++ releaseScrollTargetForUserInteraction(ctx); ++ }, [ctx]); const scrollToLocalOffset = React3.useCallback( (offset, animated) => { const scrollElement = scrollRef.current; -@@ -6116,14 +6432,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6116,14 +6473,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); options.left = left; options.top = top; @@ -9073,7 +10942,7 @@ index 914d2da..ce6a0d7 100644 ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6472,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6149,8 +6513,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -9083,13 +10952,16 @@ index 914d2da..ce6a0d7 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6163,7 +6485,12 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6163,7 +6526,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView if (!onScroll2 || !scrollRef.current) { return; } - const contentSize = getContentSize2(contentRef.current); + const rawContentSize = getContentSize2(contentRef.current); -+ const temporaryPadding = getTemporaryEndPadding(contentRef.current, paddingEndProp); ++ let temporaryPadding = 0; ++ for (const bought of borrowedExtentsRef.current.values()) { ++ temporaryPadding = Math.max(temporaryPadding, bought); ++ } + const contentSize = temporaryPadding ? { + height: horizontal ? rawContentSize.height : rawContentSize.height - temporaryPadding, + width: horizontal ? rawContentSize.width - temporaryPadding : rawContentSize.width @@ -9097,22 +10969,25 @@ index 914d2da..ce6a0d7 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6379,10 +6706,12 @@ function useValueListener$(key, callback) { - } - - // src/components/ScrollAdjust.tsx --function getScrollAdjustAxis(horizontal) { -+function getScrollAdjustAxis(horizontal, rtl = false) { - return horizontal ? { - contentSizeKey: "scrollWidth", -- paddingEndProp: "paddingRight", -+ // The end side under RTL is the left, matching how the list pads its own content and -+ // which property the scroll view borrows room on. -+ paddingEndProp: rtl ? "paddingLeft" : "paddingRight", - viewportSizeKey: "clientWidth", - x: 1, - y: 0 -@@ -6411,8 +6740,6 @@ function ScrollAdjust() { +@@ -6221,11 +6592,17 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + const target = getScrollTarget(); + if (!target) return; + target.addEventListener("scroll", handleScroll, { passive: true }); ++ for (const type of USER_INTERACTION_EVENTS) { ++ target.addEventListener(type, reportUserInteraction, { passive: true }); ++ } + if ("onscrollend" in target) { + target.addEventListener("scrollend", emitScrollEnd); + } + return () => { + target.removeEventListener("scroll", handleScroll); ++ for (const type of USER_INTERACTION_EVENTS) { ++ target.removeEventListener(type, reportUserInteraction); ++ } + if ("onscrollend" in target) { + target.removeEventListener("scrollend", emitScrollEnd); + } +@@ -6411,8 +6788,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3__namespace.useRef(0); const lastScrollAdjustUserOffsetRef = React3__namespace.useRef(0); @@ -9121,16 +10996,7 @@ index 914d2da..ce6a0d7 100644 const contentNodeRef = React3__namespace.useRef(null); const callback = React3__namespace.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6423,7 +6750,7 @@ function ScrollAdjust() { - const target = getScrollAdjustTarget(ctx, contentNodeRef.current); - if (target) { - const horizontal = !!ctx.state.props.horizontal; -- const axis = getScrollAdjustAxis(horizontal); -+ const axis = getScrollAdjustAxis(horizontal, isHorizontalRTL(ctx.state)); - const { contentNode, scrollElement: el } = target; - const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; - const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6438,29 +6765,10 @@ function ScrollAdjust() { +@@ -6438,29 +6813,10 @@ function ScrollAdjust() { const nextScroll = currentScroll + scrollDelta; const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; if (needsTemporaryPadding) { @@ -9162,7 +11028,7 @@ index 914d2da..ce6a0d7 100644 } else { scrollBy(); } -@@ -8288,6 +8596,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8288,6 +8644,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -9171,10 +11037,10 @@ index 914d2da..ce6a0d7 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs -index 95465f2..b028ddc 100644 +index 95465f2..9ac6d3f 100644 --- a/node_modules/@legendapp/list/react.mjs +++ b/node_modules/@legendapp/list/react.mjs -@@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; +@@ -413,178 +413,266 @@ var EDGE_POSITION_EPSILON = 1; var ENABLE_DEVMODE = IS_DEV && false; var ENABLE_DEBUG_VIEW = IS_DEV && false; @@ -9184,22 +11050,56 @@ index 95465f2..b028ddc 100644 - scheduledWork.cancel("checkFinishedScrollRetryFrame"); - scheduledWork.cancel("checkFinishedScrollFallback"); - scheduledWork.cancel("platformScrollCompletion"); --} ++// src/core/getStartOffsetAdjustment.ts ++function getStartOffsetAdjustment(ctx) { ++ const { state } = ctx; ++ const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; ++ return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); + } -function settlePendingImperativeScroll(state) { - var _a3, _b; - const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; - state.pendingScrollResolve = void 0; - state.pendingScrollToEnd = void 0; - resolvePendingScroll == null ? void 0 : resolvePendingScroll(); --} ++ ++// src/utils/getId.ts ++function getId(state, index) { ++ const { data, keyExtractor } = state.props; ++ if (!data) { ++ return ""; ++ } ++ const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; ++ const id = ret; ++ state.idCache[index] = id; ++ return id; + } -function cancelImperativeScroll(state) { - cancelScrollCompletionChecks(state); - state.scheduledWork.cancel("imperativeScrollReady"); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; - settlePendingImperativeScroll(state); --} -- ++ ++// src/core/updateContentMetricsState.ts ++function getRawContentLength(ctx) { ++ var _a3, _b, _c; ++ const { state, values } = ctx; ++ return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); ++} ++function getAlignItemsAtEndPadding(ctx) { ++ const { state } = ctx; ++ const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; ++ return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; ++} ++function updateContentMetricsState(ctx) { ++ const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; ++ const nextPadding = getAlignItemsAtEndPadding(ctx); ++ if (previousPadding !== nextPadding) { ++ set$(ctx, "alignItemsAtEndPadding", nextPadding); ++ } + } + -// src/core/deferredPublicOnScroll.ts -function withResolvedContentOffset(state, event, resolvedOffset) { - return { @@ -9207,12 +11107,40 @@ index 95465f2..b028ddc 100644 - nativeEvent: { - ...event.nativeEvent, - contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -- } ++// src/core/addTotalSize.ts ++function addTotalSize(ctx, key, add, notifyTotalSize = true) { ++ const state = ctx.state; ++ const prevTotalSize = state.totalSize; ++ let totalSize = state.totalSize; ++ if (key === null) { ++ totalSize = add; ++ if (state.timeoutSetPaddingTop) { ++ clearTimeout(state.timeoutSetPaddingTop); ++ state.timeoutSetPaddingTop = void 0; + } - }; --} ++ } else { ++ totalSize += add; ++ } ++ if (prevTotalSize !== totalSize) { ++ { ++ state.pendingTotalSize = void 0; ++ state.totalSize = totalSize; ++ if (notifyTotalSize) { ++ set$(ctx, "totalSize", totalSize); ++ } ++ updateContentMetricsState(ctx); ++ } ++ } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { ++ set$(ctx, "totalSize", totalSize); ++ } + } -function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { - var _a3, _b, _c, _d; -- const state = ctx.state; ++ ++// src/core/setSize.ts ++function setSize(ctx, itemKey, size, notifyTotalSize = true) { + const state = ctx.state; - const deferredEvent = state.deferredPublicOnScrollEvent; - state.deferredPublicOnScrollEvent = void 0; - if (deferredEvent) { @@ -9224,9 +11152,15 @@ index 95465f2..b028ddc 100644 - (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 - ) - ); -- } --} -- ++ const { sizes } = state; ++ const previousSize = sizes.get(itemKey); ++ const diff = previousSize !== void 0 ? size - previousSize : size; ++ if (diff !== 0) { ++ addTotalSize(ctx, itemKey, diff, notifyTotalSize); + } ++ sizes.set(itemKey, size); + } + -// src/core/initialScrollSession.ts -var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -function hasInitialScrollSessionCompletion(completion) { @@ -9235,7 +11169,10 @@ index 95465f2..b028ddc 100644 -function clearInitialScrollSession(state) { - state.initialScrollSession = void 0; - return void 0; --} ++// src/utils/helpers.ts ++function isFunction(obj) { ++ return typeof obj === "function"; + } -function createInitialScrollSession(options) { - const { bootstrap, completion, kind, previousDataLength } = options; - return kind === "offset" ? { @@ -9248,7 +11185,9 @@ index 95465f2..b028ddc 100644 - kind, - previousDataLength - }; --} ++function isArray(obj) { ++ return Array.isArray(obj); + } -function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { - var _a4, _b2; - if (!state.initialScrollSession) { @@ -9264,10 +11203,15 @@ index 95465f2..b028ddc 100644 - kind, - previousDataLength: state.initialScrollSession.previousDataLength - }); -- } ++var warned = /* @__PURE__ */ new Set(); ++function warnDevOnce(id, text) { ++ if (IS_DEV && !warned.has(id)) { ++ warned.add(id); ++ console.warn(`[legend-list] ${text}`); + } - (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; - return state.initialScrollSession.completion; --} + } -var initialScrollCompletion = { - didDispatchNativeScroll(state) { - var _a3, _b; @@ -9290,7 +11234,26 @@ index 95465f2..b028ddc 100644 - const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); - completion.didDispatchNativeScroll = void 0; - completion.didRetrySilentInitialScroll = void 0; -- } ++function roundSize(size) { ++ return Math.floor(size * 8) / 8; ++} ++function isNullOrUndefined(value) { ++ return value === null || value === void 0; ++} ++function getPadding(s, type) { ++ var _a3, _b, _c; ++ const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; ++ return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; ++} ++function extractPadding(style, contentContainerStyle, type) { ++ return getPadding(style, type) + getPadding(contentContainerStyle, type); ++} ++function findContainerId(ctx, key) { ++ var _a3, _b; ++ const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); ++ if (directMatch !== void 0) { ++ return directMatch; + } -}; -var initialScrollWatchdog = { - clear(state) { @@ -9314,7 +11277,12 @@ index 95465f2..b028ddc 100644 - var _a3, _b; - if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { - return; -- } ++ const numContainers = peek$(ctx, "numContainers"); ++ for (let i = 0; i < numContainers; i++) { ++ const itemKey = peek$(ctx, `containerItemKey${i}`); ++ if (itemKey === key) { ++ return i; + } - const completion = ensureInitialScrollSessionCompletion(state); - completion.watchdog = watchdog ? { - startScroll: watchdog.startScroll, @@ -9332,460 +11300,104 @@ index 95465f2..b028ddc 100644 - if (!kind) { - return clearInitialScrollSession(state); - } -- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { -- return clearInitialScrollSession(state); -- } -- const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; -- state.initialScrollSession = createInitialScrollSession({ -- bootstrap, -- completion, -- kind, -- previousDataLength -- }); -- return state.initialScrollSession; --} -- --// src/utils/checkThreshold.ts --var HYSTERESIS_MULTIPLIER = 1.3; --function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { -- const absDistance = Math.abs(distance); -- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; --} --var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { -- const absDistance = Math.abs(distance); -- const within = atThreshold || threshold > 0 && absDistance <= threshold; -- const updateSnapshot = () => { -- setSnapshot({ -- atThreshold, -- contentSize: context.contentSize, -- dataLength: context.dataLength, -- scrollPosition: context.scrollPosition -- }); -- }; -- if (!wasReached) { -- if (!within) { -- return false; -- } -- onReached(distance); -- updateSnapshot(); -- return true; -- } -- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); -- if (reset) { -- setSnapshot(void 0); -- return false; -- } -- if (within) { -- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; -- if (changed) { -- updateSnapshot(); -- } -- } -- return true; --}; -- --// src/utils/edgeReachedGate.ts --function resetEdgeLatch(ctx, edge) { -- const state = ctx.state; -- if (edge === "start") { -- state.isStartReached = false; -- state.startReachedSnapshot = void 0; -- } else { -- state.isEndReached = false; -- state.endReachedSnapshot = void 0; -- } --} --function resetSharedEdgeGateIfOutsideHysteresis(ctx) { -- const state = ctx.state; -- if (!state.edgeReachedGate) { -- return; -- } -- const contentSize = getContentSize(ctx); -- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); -- const isContentLess = contentSize < state.scrollLength; -- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; -- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; -- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); -- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); -- if (isOutsideStart && isOutsideEnd) { -- state.edgeReachedGate = void 0; -- } --} --function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { -- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; --} --function markReachedEdge(ctx) { -- ctx.state.edgeReachedGate = "closed"; --} --function prepareReachedEdgeForNextUserScroll(ctx) { -- if (ctx.state.edgeReachedGate) { -- ctx.state.edgeReachedGate = "prepared"; -- } --} --function beginReachedEdgeUserScroll(ctx, scrollDelta) { -- const state = ctx.state; -- if (state.edgeReachedGate !== "prepared") { -- return void 0; -- } -- const allowedEdge = scrollDelta < 0 ? "start" : "end"; -- state.edgeReachedGate = "closed"; -- resetEdgeLatch(ctx, allowedEdge); -- return allowedEdge; --} -- --// src/utils/hasActiveInitialScroll.ts --function hasActiveInitialScroll(state) { -- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; --} -- --// src/utils/checkAtBottom.ts --function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -- var _a3; -- const state = ctx.state; -- if (!state) { -- return; -- } -- const { -- queuedInitialLayout, -- scrollLength, -- scroll, -- maintainingScrollAtEnd, -- props: { maintainScrollAtEndThreshold, onEndReachedThreshold } -- } = state; -- const contentSize = getContentSize(ctx); -- resetSharedEdgeGateIfOutsideHysteresis(ctx); -- if (contentSize > 0 && queuedInitialLayout) { -- const insetEnd = getContentInsetEnd(ctx); -- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; -- const isContentLess = contentSize < scrollLength; -- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); -- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); -- set$( -- ctx, -- "isWithinMaintainScrollAtEndThreshold", -- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength -- ); -- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; -- if (!shouldSkipThresholdChecks) { -- state.isEndReached = checkThreshold( -- distanceFromEnd, -- isContentLess, -- onEndReachedThreshold * scrollLength, -- state.isEndReached, -- state.endReachedSnapshot, -- { -- contentSize, -- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, -- scrollPosition: scroll -- }, -- (distance) => { -- var _a4, _b; -- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { -- markReachedEdge(ctx); -- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); -- } -- }, -- (snapshot) => { -- state.endReachedSnapshot = snapshot; -- } -- ); -- } -- } --} -- --// src/utils/checkAtTop.ts --function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -- const state = ctx == null ? void 0 : ctx.state; -- if (!state) { -- return; -- } -- const { -- isStartReached, -- props: { data, onStartReachedThreshold }, -- scroll, -- scrollLength, -- startReachedSnapshot, -- totalSize -- } = state; -- const dataLength = data.length; -- const threshold = onStartReachedThreshold * scrollLength; -- resetSharedEdgeGateIfOutsideHysteresis(ctx); -- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { -- state.isStartReached = false; -- state.startReachedSnapshot = void 0; -- } -- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); -- set$(ctx, "isNearStart", scroll <= threshold); -- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; -- if (!shouldSkipThresholdChecks) { -- state.isStartReached = checkThreshold( -- scroll, -- false, -- threshold, -- state.isStartReached, -- startReachedSnapshot, -- { -- contentSize: totalSize, -- dataLength, -- scrollPosition: scroll -- }, -- (distance) => { -- var _a3, _b; -- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { -- markReachedEdge(ctx); -- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); -- } -- }, -- (snapshot) => { -- state.startReachedSnapshot = snapshot; -- } -- ); -- } --} -- --// src/utils/checkThresholds.ts --function checkThresholds(ctx, allowedEdge) { -- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; -- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); --} -- --// src/core/recalculateSettledScroll.ts --function recalculateSettledScroll(ctx) { -- var _a3, _b; -- const state = ctx.state; -- if ((_a3 = state.props) == null ? void 0 : _a3.data) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); -- } -- checkThresholds(ctx); --} --var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; --var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; --var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; --function clearAdaptiveRenderExitTimeout(ctx) { -- ctx.state.scheduledWork.cancel("adaptiveRender"); --} --function scheduleAdaptiveRenderExit(ctx, exitDelay) { -- const state = ctx.state; -- clearAdaptiveRenderExitTimeout(ctx); -- if (exitDelay <= 0) { -- setAdaptiveRender(ctx, "normal", "scroll"); -- } else { -- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); -- } --} --function setAdaptiveRender(ctx, mode, reason) { -- var _a3, _b; -- const previousMode = peek$(ctx, "adaptiveRender"); -- if (previousMode !== mode) { -- set$(ctx, "adaptiveRender", mode); -- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); -- } --} --function resetAdaptiveRender(ctx) { -- var _a3, _b; -- clearAdaptiveRenderExitTimeout(ctx); -- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; -- if (peek$(ctx, "adaptiveRender") !== mode) { -- setAdaptiveRender(ctx, mode, "initial"); -- } --} --function updateAdaptiveRender(ctx, scrollVelocity, options) { -- var _a3, _b, _c; -- const state = ctx.state; -- const adaptiveRender = state.props.adaptiveRender; -- const currentMode = peek$(ctx, "adaptiveRender"); -- if (peek$(ctx, "readyToRender")) { -- if (adaptiveRender) { -- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; -- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; -- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; -- const threshold = currentMode === "light" ? exitVelocity : enterVelocity; -- const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; -- const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; -- if (nextMode !== previousMode) { -- if (nextMode === "light") { -- setAdaptiveRender(ctx, "light", "scroll"); -- scheduleAdaptiveRenderExit(ctx, exitDelay); -- } else if (currentMode === "light") { -- scheduleAdaptiveRenderExit(ctx, exitDelay); -- } -- } -- } else { -- resetAdaptiveRender(ctx); -- } -- } --} -- --// src/utils/getEffectiveDrawDistance.ts --var INITIAL_DRAW_DISTANCE = 50; --function getEffectiveDrawDistance(ctx, mode) { -- var _a3; -- const drawDistance = ctx.state.props.drawDistance; -- const initialScroll = ctx.state.initialScroll; -- const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; -- const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; -- return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; --} --function scheduleFullDrawDistancePrewarm(ctx) { -- const { state } = ctx; -- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { -- return; -- } -- state.scheduledWork.frame(() => { -- var _a3; -- return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); -- }, "fullDrawDistancePrewarm"); --} -- --// src/utils/setInitialRenderState.ts --function resetInitialRenderState(ctx, { -- resetLayout, -- resetInitialScroll --}) { -- const { state } = ctx; -- if (resetLayout) { -- state.didContainersLayout = false; -- state.queuedInitialLayout = false; -- } -- if (resetInitialScroll) { -- state.didFinishInitialScroll = false; -- } -- set$(ctx, "readyToRender", false); -- resetAdaptiveRender(ctx); --} --function setInitialRenderState(ctx, { -- didLayout, -- didInitialScroll --}) { -- const { state } = ctx; -- const { -- loadStartTime, -- props: { onLoad } -- } = state; -- if (didLayout) { -- state.didContainersLayout = true; -- } -- if (didInitialScroll) { -- state.didFinishInitialScroll = true; -- } -- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); -- if (isReadyToRender && !peek$(ctx, "readyToRender")) { -- set$(ctx, "readyToRender", true); -- setAdaptiveRender(ctx, "normal", "ready"); -- if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { -- scheduleFullDrawDistancePrewarm(ctx); -- } -- if (!state.didLoad) { -- state.didLoad = true; -- if (onLoad) { -- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); -- } -- } -- } --} -- --// src/core/finishInitialScroll.ts --var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; --function syncInitialScrollOffset(state, offset) { -- state.scroll = offset; -- state.scrollPending = offset; -- state.scrollPrev = offset; --} --function clearPreservedInitialScrollTargetTimeout(state) { -- state.scheduledWork.cancel("preservedInitialScroll"); --} --function clearPreservedInitialScrollTarget(state) { -- clearPreservedInitialScrollTargetTimeout(state); -- state.clearPreservedInitialScrollOnNextFinish = void 0; -- state.initialScroll = void 0; -- setInitialScrollSession(state); --} --function supersedeInitialScroll(ctx) { -- var _a3, _b, _c; -- const state = ctx.state; -- const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; -- if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { -- if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { -- cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -- } -- if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { -- cancelScrollCompletionChecks(state); -- state.scrollingTo = void 0; -- state.scrollTargetPinnedRange = void 0; -- } -- initialScrollCompletion.resetFlags(state); -- setInitialScrollSession(state, { bootstrap: null }); -- finishInitialScroll(ctx); -- } --} --function finishInitialScroll(ctx, options) { -- var _a3, _b, _c; -- const state = ctx.state; -- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { -- syncInitialScrollOffset(state, options.resolvedOffset); -- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { -- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); -- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { -- syncInitialScrollOffset(state, observedOffset); -- } -- } -- const complete = () => { -- var _a4, _b2, _c2, _d, _e; -- const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; -- const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0; -- initialScrollWatchdog.clear(state); -- if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) { -- state.clearPreservedInitialScrollOnNextFinish = void 0; -- setInitialScrollSession(state); -- clearPreservedInitialScrollTargetTimeout(state); -- if (options == null ? void 0 : options.schedulePreservedTargetClear) { -- state.scheduledWork.timeout( -- () => { -- var _a5; -- if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) { -- return; -- } -- clearPreservedInitialScrollTarget(state); -- }, -- PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, -- "preservedInitialScroll" -- ); -- } -- } else { -- clearPreservedInitialScrollTarget(state); -- } -- if (options == null ? void 0 : options.recalculateItems) { -- recalculateSettledScroll(ctx); -- } -- setInitialRenderState(ctx, { didInitialScroll: true }); -- if (shouldReleaseDeferredPublicOnScroll) { -- releaseDeferredPublicOnScroll(ctx, finalScrollOffset); -- } -- (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); -- }; -- if (options == null ? void 0 : options.waitForCompletionFrame) { -- requestAnimationFrame(complete); -- return; -- } -- complete(); --} -- --// src/core/calculateOffsetForIndex.ts --function calculateOffsetForIndex(ctx, index) { -- const state = ctx.state; -- return index !== void 0 ? state.positions[index] || 0 : 0; --} -- - // src/core/getStartOffsetAdjustment.ts - function getStartOffsetAdjustment(ctx) { - const { state } = ctx; -@@ -1213,58 +614,273 @@ function getItemSizeAtIndex(ctx, index) { - if (index === void 0 || index < 0) { - return void 0; +- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { +- return clearInitialScrollSession(state); } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); +- const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; +- state.initialScrollSession = createInitialScrollSession({ +- bootstrap, +- completion, +- kind, +- previousDataLength +- }); +- return state.initialScrollSession; ++ return -1; + } + +-// src/utils/checkThreshold.ts +-var HYSTERESIS_MULTIPLIER = 1.3; +-function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { +- const absDistance = Math.abs(distance); +- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; ++// src/utils/getItemSize.ts ++function getKnownOrFixedSize(ctx, key, index, data, resolved) { ++ var _a3, _b; ++ const state = ctx.state; ++ const { getFixedItemSize, getItemType } = state.props; ++ let size = key ? state.sizesKnown.get(key) : void 0; ++ if (size === void 0 && key && getFixedItemSize) { ++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; ++ const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); ++ if (fixedSize !== void 0) { ++ size = fixedSize + ctx.scrollAxisGap; ++ state.sizesKnown.set(key, size); ++ } ++ } ++ return size; ++} ++function getKnownOrFixedItemSize(ctx, index) { ++ const key = getId(ctx.state, index); ++ return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); ++} ++function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { ++ for (let index = startIndex; index <= endIndex; index++) { ++ if (getKnownOrFixedItemSize(ctx, index) === void 0) { ++ return false; ++ } ++ } ++ return true; ++} ++function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { ++ var _a3, _b, _c, _d; ++ const state = ctx.state; ++ const { ++ sizes, ++ averageSizes, ++ props: { estimatedItemSize, getItemType }, ++ scrollingTo ++ } = state; ++ const sizeKnown = state.sizesKnown.get(key); ++ if (sizeKnown !== void 0) { ++ return sizeKnown; ++ } ++ let size; ++ const renderedSize = sizes.get(key); ++ if (preferCachedSize) { ++ if (renderedSize !== void 0) { ++ return renderedSize; ++ } ++ } ++ size = getKnownOrFixedSize(ctx, key, index, data, resolved); ++ if (size !== void 0) { ++ setSize(ctx, key, size, notifyTotalSize); ++ return size; ++ } ++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; ++ if (useAverageSize && !scrollingTo) { ++ const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; ++ if (averageSizeForType !== void 0) { ++ size = roundSize(averageSizeForType); ++ } ++ } ++ if (size === void 0 && renderedSize !== void 0) { ++ return renderedSize; ++ } ++ if (size === void 0 && useAverageSize && scrollingTo) { ++ const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; ++ if (averageSizeForType !== void 0) { ++ size = roundSize(averageSizeForType); ++ } ++ } ++ if (size === void 0) { ++ size = estimatedItemSize + ctx.scrollAxisGap; ++ } ++ setSize(ctx, key, size, notifyTotalSize); ++ return size; ++} ++function getItemSizeAtIndex(ctx, index) { ++ if (index === void 0 || index < 0) { ++ return void 0; ++ } + const targetId = getId(ctx.state, index); + return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); +} @@ -9845,291 +11457,372 @@ index 95465f2..b028ddc 100644 +function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { + const absDistance = Math.abs(distance); + return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; -+} -+var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { -+ const absDistance = Math.abs(distance); -+ const within = atThreshold || threshold > 0 && absDistance <= threshold; -+ const updateSnapshot = () => { -+ setSnapshot({ -+ atThreshold, -+ contentSize: context.contentSize, -+ dataLength: context.dataLength, -+ scrollPosition: context.scrollPosition -+ }); -+ }; -+ if (!wasReached) { -+ if (!within) { -+ return false; -+ } -+ onReached(distance); -+ updateSnapshot(); -+ return true; -+ } -+ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); -+ if (reset) { -+ setSnapshot(void 0); -+ return false; -+ } -+ if (within) { -+ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; -+ if (changed) { -+ updateSnapshot(); -+ } -+ } -+ return true; -+}; -+ -+// src/utils/edgeReachedGate.ts -+function resetEdgeLatch(ctx, edge) { -+ const state = ctx.state; -+ if (edge === "start") { -+ state.isStartReached = false; -+ state.startReachedSnapshot = void 0; -+ } else { -+ state.isEndReached = false; -+ state.endReachedSnapshot = void 0; -+ } -+} -+function resetSharedEdgeGateIfOutsideHysteresis(ctx) { -+ const state = ctx.state; -+ if (!state.edgeReachedGate) { -+ return; -+ } -+ const contentSize = getContentSize(ctx); -+ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); -+ const isContentLess = contentSize < state.scrollLength; -+ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; -+ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; -+ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); -+ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); -+ if (isOutsideStart && isOutsideEnd) { -+ state.edgeReachedGate = void 0; -+ } -+} -+function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { -+ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; -+} -+function markReachedEdge(ctx) { -+ ctx.state.edgeReachedGate = "closed"; -+} -+function prepareReachedEdgeForNextUserScroll(ctx) { -+ if (ctx.state.edgeReachedGate) { -+ ctx.state.edgeReachedGate = "prepared"; -+ } -+} -+function beginReachedEdgeUserScroll(ctx, scrollDelta) { -+ const state = ctx.state; -+ if (state.edgeReachedGate !== "prepared") { -+ return void 0; -+ } -+ const allowedEdge = scrollDelta < 0 ? "start" : "end"; -+ state.edgeReachedGate = "closed"; -+ resetEdgeLatch(ctx, allowedEdge); -+ return allowedEdge; -+} -+ -+// src/utils/hasActiveInitialScroll.ts -+function hasActiveInitialScroll(state) { -+ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; -+} + } + var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { + const absDistance = Math.abs(distance); +@@ -794,642 +882,346 @@ function recalculateSettledScroll(ctx) { + } + checkThresholds(ctx); + } +-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; +-function clearAdaptiveRenderExitTimeout(ctx) { +- ctx.state.scheduledWork.cancel("adaptiveRender"); +-} +-function scheduleAdaptiveRenderExit(ctx, exitDelay) { +- const state = ctx.state; +- clearAdaptiveRenderExitTimeout(ctx); +- if (exitDelay <= 0) { +- setAdaptiveRender(ctx, "normal", "scroll"); +- } else { +- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); +- } +-} +-function setAdaptiveRender(ctx, mode, reason) { +- var _a3, _b; +- const previousMode = peek$(ctx, "adaptiveRender"); +- if (previousMode !== mode) { +- set$(ctx, "adaptiveRender", mode); +- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); +- } +-} +-function resetAdaptiveRender(ctx) { + -+// src/utils/checkAtBottom.ts -+function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -+ var _a3; -+ const state = ctx.state; -+ if (!state) { -+ return; -+ } -+ const { -+ queuedInitialLayout, -+ scrollLength, -+ scroll, -+ maintainingScrollAtEnd, -+ props: { maintainScrollAtEndThreshold, onEndReachedThreshold } -+ } = state; -+ const contentSize = getContentSize(ctx); -+ resetSharedEdgeGateIfOutsideHysteresis(ctx); -+ if (contentSize > 0 && queuedInitialLayout) { -+ const insetEnd = getContentInsetEnd(ctx); -+ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; -+ const isContentLess = contentSize < scrollLength; -+ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); -+ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); -+ set$( -+ ctx, -+ "isWithinMaintainScrollAtEndThreshold", -+ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength -+ ); -+ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; -+ if (!shouldSkipThresholdChecks) { -+ state.isEndReached = checkThreshold( -+ distanceFromEnd, -+ isContentLess, -+ onEndReachedThreshold * scrollLength, -+ state.isEndReached, -+ state.endReachedSnapshot, -+ { -+ contentSize, -+ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, -+ scrollPosition: scroll -+ }, -+ (distance) => { -+ var _a4, _b; -+ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { -+ markReachedEdge(ctx); -+ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); -+ } ++// src/core/finishScrollTo.ts ++function finishScrollTo(ctx) { + var _a3, _b; +- clearAdaptiveRenderExitTimeout(ctx); +- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; +- if (peek$(ctx, "adaptiveRender") !== mode) { +- setAdaptiveRender(ctx, mode, "initial"); +- } +-} +-function updateAdaptiveRender(ctx, scrollVelocity, options) { +- var _a3, _b, _c; + const state = ctx.state; +- const adaptiveRender = state.props.adaptiveRender; +- const currentMode = peek$(ctx, "adaptiveRender"); +- if (peek$(ctx, "readyToRender")) { +- if (adaptiveRender) { +- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; +- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; +- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; +- const threshold = currentMode === "light" ? exitVelocity : enterVelocity; +- const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; +- const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; +- if (nextMode !== previousMode) { +- if (nextMode === "light") { +- setAdaptiveRender(ctx, "light", "scroll"); +- scheduleAdaptiveRenderExit(ctx, exitDelay); +- } else if (currentMode === "light") { +- scheduleAdaptiveRenderExit(ctx, exitDelay); +- } +- } +- } else { +- resetAdaptiveRender(ctx); ++ if (state == null ? void 0 : state.scrollingTo) { ++ cancelScrollCompletionChecks(state); ++ const resolvePendingScroll = state.pendingScrollResolve; ++ state.pendingScrollResolve = void 0; ++ const scrollingTo = state.scrollingTo; ++ state.scrollHistory.length = 0; ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ if (state.pendingTotalSize !== void 0) { ++ addTotalSize(ctx, null, state.pendingTotalSize); ++ } ++ { ++ state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); ++ } ++ if (scrollingTo.isInitialScroll || state.initialScroll) { ++ const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; ++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; ++ finishInitialScroll(ctx, { ++ onFinished: () => { ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); + }, -+ (snapshot) => { -+ state.endReachedSnapshot = snapshot; -+ } -+ ); -+ } -+ } ++ preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, ++ recalculateItems: true, ++ schedulePreservedTargetClear: shouldPreserveResizeTarget, ++ syncObservedOffset: isOffsetSession, ++ waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame ++ }); ++ return; + } ++ recalculateSettledScroll(ctx); ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); + } } --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +-// src/utils/getEffectiveDrawDistance.ts +-var INITIAL_DRAW_DISTANCE = 50; +-function getEffectiveDrawDistance(ctx, mode) { - var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; -+// src/utils/checkAtTop.ts -+function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -+ const state = ctx == null ? void 0 : ctx.state; -+ if (!state) { -+ return; - } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } -+ const { -+ isStartReached, -+ props: { data, onStartReachedThreshold }, -+ scroll, -+ scrollLength, -+ startReachedSnapshot, -+ totalSize -+ } = state; -+ const dataLength = data.length; -+ const threshold = onStartReachedThreshold * scrollLength; -+ resetSharedEdgeGateIfOutsideHysteresis(ctx); -+ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { -+ state.isStartReached = false; -+ state.startReachedSnapshot = void 0; +- const drawDistance = ctx.state.props.drawDistance; +- const initialScroll = ctx.state.initialScroll; +- const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; +- const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; +- return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; +-} +-function scheduleFullDrawDistancePrewarm(ctx) { +- const { state } = ctx; +- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { ++// src/core/doScrollTo.ts ++var SCROLL_END_IDLE_MS = 80; ++var SCROLL_END_MAX_MS = 1500; ++var SMOOTH_SCROLL_DURATION_MS = 320; ++var SCROLL_END_TARGET_EPSILON = 1; ++function doScrollTo(ctx, params) { ++ var _a3, _b; ++ const state = ctx.state; ++ const { animated, horizontal, offset } = params; ++ state.scheduledWork.cancel("platformScrollCompletion"); ++ const scroller = state.refScroller.current; ++ const node = scroller == null ? void 0 : scroller.getScrollableNode(); ++ if (!scroller || !node) { + return; } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } -+ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); -+ set$(ctx, "isNearStart", scroll <= threshold); -+ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; -+ if (!shouldSkipThresholdChecks) { -+ state.isStartReached = checkThreshold( -+ scroll, -+ false, -+ threshold, -+ state.isStartReached, -+ startReachedSnapshot, -+ { -+ contentSize: totalSize, -+ dataLength, -+ scrollPosition: scroll -+ }, -+ (distance) => { -+ var _a3, _b; -+ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { -+ markReachedEdge(ctx); -+ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); +- state.scheduledWork.frame(() => { +- var _a3; +- return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +- }, "fullDrawDistancePrewarm"); +-} +- +-// src/utils/setInitialRenderState.ts +-function resetInitialRenderState(ctx, { +- resetLayout, +- resetInitialScroll +-}) { +- const { state } = ctx; +- if (resetLayout) { +- state.didContainersLayout = false; +- state.queuedInitialLayout = false; +- } +- if (resetInitialScroll) { +- state.didFinishInitialScroll = false; ++ const isAnimated = !!animated; ++ const isHorizontal = !!horizontal; ++ const contentSize = isHorizontal ? getContentSize(ctx) : void 0; ++ const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; ++ const top = isHorizontal ? 0 : offset; ++ scroller.scrollTo({ animated: isAnimated, x: left, y: top }); ++ if (isAnimated) { ++ const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; ++ listenForScrollEnd(ctx, { ++ readOffset: () => scroller.getCurrentScrollOffset(), ++ target, ++ targetOffset: offset ++ }); ++ } else { ++ state.scroll = offset; ++ const targetToken = state.scrollingTo; ++ state.scheduledWork.timeout( ++ () => { ++ if (targetToken === state.scrollingTo) { ++ finishScrollTo(ctx); + } + }, -+ (snapshot) => { -+ state.startReachedSnapshot = snapshot; -+ } ++ 100, ++ "platformScrollCompletion" + ); } -- return offset; +- set$(ctx, "readyToRender", false); +- resetAdaptiveRender(ctx); } - --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { -+// src/utils/checkThresholds.ts -+function checkThresholds(ctx, allowedEdge) { -+ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; -+ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+} -+ -+// src/core/recalculateSettledScroll.ts -+function recalculateSettledScroll(ctx) { -+ var _a3, _b; - const state = ctx.state; -- const contentSize = getContentSize(ctx); -- let clampedOffset = offset; -- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { -- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); -- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; -- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; -- const maxOffset = baseMaxOffset + extraEndOffset; -- clampedOffset = Math.min(offset, maxOffset); -+ if ((_a3 = state.props) == null ? void 0 : _a3.data) { -+ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); +-function setInitialRenderState(ctx, { +- didLayout, +- didInitialScroll +-}) { +- const { state } = ctx; +- const { +- loadStartTime, +- props: { onLoad } +- } = state; +- if (didLayout) { +- state.didContainersLayout = true; +- } +- if (didInitialScroll) { +- state.didFinishInitialScroll = true; +- } +- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); +- if (isReadyToRender && !peek$(ctx, "readyToRender")) { +- set$(ctx, "readyToRender", true); +- setAdaptiveRender(ctx, "normal", "ready"); +- if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { +- scheduleFullDrawDistancePrewarm(ctx); +- } +- if (!state.didLoad) { +- state.didLoad = true; +- if (onLoad) { +- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); +- } +- } ++function listenForScrollEnd(ctx, params) { ++ const { readOffset, target, targetOffset } = params; ++ if (!target) { ++ finishScrollTo(ctx); ++ return; } -- clampedOffset = Math.max(0, clampedOffset); -- return clampedOffset; -+ checkThresholds(ctx); - } - - // src/core/finishScrollTo.ts -@@ -1392,16 +1008,191 @@ function listenForScrollEnd(ctx, params) { - if (idleTimeout !== void 0) { - clearTimeout(idleTimeout); +-} +- +-// src/core/finishInitialScroll.ts +-var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; +-function syncInitialScrollOffset(state, offset) { +- state.scroll = offset; +- state.scrollPending = offset; +- state.scrollPrev = offset; +-} +-function clearPreservedInitialScrollTargetTimeout(state) { +- state.scheduledWork.cancel("preservedInitialScroll"); +-} +-function clearPreservedInitialScrollTarget(state) { +- clearPreservedInitialScrollTargetTimeout(state); +- state.clearPreservedInitialScrollOnNextFinish = void 0; +- state.initialScroll = void 0; +- setInitialScrollSession(state); +-} +-function supersedeInitialScroll(ctx) { +- var _a3, _b, _c; +- const state = ctx.state; +- const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; +- if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { +- if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { +- cancelAnimationFrame(bootstrapInitialScroll.frameHandle); ++ const supportsScrollEnd = "onscrollend" in target; ++ let idleTimeout; ++ let settled = false; ++ const { scheduledWork, scrollingTo: targetToken } = ctx.state; ++ const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); ++ const cleanup = () => { ++ target.removeEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.removeEventListener("scrollend", onScrollEnd); } -- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); -- }; -- const onScrollEnd = () => finish("scrollend"); -- target.addEventListener("scroll", onScroll2); -- if (supportsScrollEnd) { -- target.addEventListener("scrollend", onScrollEnd); -- } else { -- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); -+ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); +- if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { +- cancelScrollCompletionChecks(state); +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; ++ if (idleTimeout !== void 0) { ++ clearTimeout(idleTimeout); + } +- initialScrollCompletion.resetFlags(state); +- setInitialScrollSession(state, { bootstrap: null }); +- finishInitialScroll(ctx); +- } +-} +-function finishInitialScroll(ctx, options) { +- var _a3, _b, _c; +- const state = ctx.state; +- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { +- syncInitialScrollOffset(state, options.resolvedOffset); +- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { +- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); +- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { +- syncInitialScrollOffset(state, observedOffset); ++ clearTimeout(maxTimeout); ++ }; ++ const cancel = () => { ++ if (!settled) { ++ settled = true; ++ cleanup(); + } +- } +- const complete = () => { +- var _a4, _b2, _c2, _d, _e; +- const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; +- const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0; +- initialScrollWatchdog.clear(state); +- if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) { +- state.clearPreservedInitialScrollOnNextFinish = void 0; +- setInitialScrollSession(state); +- clearPreservedInitialScrollTargetTimeout(state); +- if (options == null ? void 0 : options.schedulePreservedTargetClear) { +- state.scheduledWork.timeout( +- () => { +- var _a5; +- if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) { +- return; +- } +- clearPreservedInitialScrollTarget(state); +- }, +- PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, +- "preservedInitialScroll" +- ); +- } +- } else { +- clearPreservedInitialScrollTarget(state); ++ }; ++ const finish = (reason) => { ++ if (settled) return; ++ if (targetToken !== ctx.state.scrollingTo) { ++ scheduledWork.cancel("platformScrollCompletion"); ++ return; + } +- if (options == null ? void 0 : options.recalculateItems) { +- recalculateSettledScroll(ctx); ++ const currentOffset = readOffset(); ++ const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; ++ if (reason === "scrollend" && !isNearTarget) { ++ return; + } +- setInitialRenderState(ctx, { didInitialScroll: true }); +- if (shouldReleaseDeferredPublicOnScroll) { +- releaseDeferredPublicOnScroll(ctx, finalScrollOffset); ++ scheduledWork.cancel("platformScrollCompletion"); ++ finishScrollTo(ctx); + }; ++ const onScroll2 = () => { ++ if (idleTimeout !== void 0) { ++ clearTimeout(idleTimeout); + } +- (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); ++ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); + }; +- if (options == null ? void 0 : options.waitForCompletionFrame) { +- requestAnimationFrame(complete); +- return; + const onScrollEnd = () => finish("scrollend"); + target.addEventListener("scroll", onScroll2); + if (supportsScrollEnd) { + target.addEventListener("scrollend", onScrollEnd); + } else { + idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); -+ } + } +- complete(); + scheduledWork.register("platformScrollCompletion", cancel); -+} -+ + } + +-// src/core/calculateOffsetForIndex.ts +-function calculateOffsetForIndex(ctx, index) { +- const state = ctx.state; +- return index !== void 0 ? state.positions[index] || 0 : 0; +// src/core/initialScrollSession.ts +var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +function hasInitialScrollSessionCompletion(completion) { + return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); -+} + } +- +-// src/core/getStartOffsetAdjustment.ts +-function getStartOffsetAdjustment(ctx) { +- const { state } = ctx; +- const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; +- return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); +function clearInitialScrollSession(state) { + state.initialScrollSession = void 0; + return void 0; -+} + } +- +-// src/utils/getId.ts +-function getId(state, index) { +- const { data, keyExtractor } = state.props; +- if (!data) { +- return ""; +- } +- const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; +- const id = ret; +- state.idCache[index] = id; +- return id; +function createInitialScrollSession(options) { + const { bootstrap, completion, kind, previousDataLength } = options; + return kind === "offset" ? { @@ -10142,7 +11835,13 @@ index 95465f2..b028ddc 100644 + kind, + previousDataLength + }; -+} + } +- +-// src/core/updateContentMetricsState.ts +-function getRawContentLength(ctx) { +- var _a3, _b, _c; +- const { state, values } = ctx; +- return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); +function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { + var _a4, _b2; + if (!state.initialScrollSession) { @@ -10161,7 +11860,30 @@ index 95465f2..b028ddc 100644 + } + (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; + return state.initialScrollSession.completion; -+} + } +-function getAlignItemsAtEndPadding(ctx) { +- const { state } = ctx; +- const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; +- return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; +-} +-function updateContentMetricsState(ctx) { +- const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; +- const nextPadding = getAlignItemsAtEndPadding(ctx); +- if (previousPadding !== nextPadding) { +- set$(ctx, "alignItemsAtEndPadding", nextPadding); +- } +-} +- +-// src/core/addTotalSize.ts +-function addTotalSize(ctx, key, add, notifyTotalSize = true) { +- const state = ctx.state; +- const prevTotalSize = state.totalSize; +- let totalSize = state.totalSize; +- if (key === null) { +- totalSize = add; +- if (state.timeoutSetPaddingTop) { +- clearTimeout(state.timeoutSetPaddingTop); +- state.timeoutSetPaddingTop = void 0; +var initialScrollCompletion = { + didDispatchNativeScroll(state) { + var _a3, _b; @@ -10180,11 +11902,21 @@ index 95465f2..b028ddc 100644 + resetFlags(state) { + if (!state.initialScrollSession) { + return; -+ } + } +- } else { +- totalSize += add; + const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); + completion.didDispatchNativeScroll = void 0; + completion.didRetrySilentInitialScroll = void 0; -+ } + } +- if (prevTotalSize !== totalSize) { +- { +- state.pendingTotalSize = void 0; +- state.totalSize = totalSize; +- if (notifyTotalSize) { +- set$(ctx, "totalSize", totalSize); +- } +- updateContentMetricsState(ctx); +}; +var initialScrollWatchdog = { + clear(state) { @@ -10208,13 +11940,25 @@ index 95465f2..b028ddc 100644 + var _a3, _b; + if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { + return; -+ } + } +- } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { +- set$(ctx, "totalSize", totalSize); + const completion = ensureInitialScrollSessionCompletion(state); + completion.watchdog = watchdog ? { + startScroll: watchdog.startScroll, + targetOffset: watchdog.targetOffset + } : void 0; -+ } + } +-} +- +-// src/core/setSize.ts +-function setSize(ctx, itemKey, size, notifyTotalSize = true) { +- const state = ctx.state; +- const { sizes } = state; +- const previousSize = sizes.get(itemKey); +- const diff = previousSize !== void 0 ? size - previousSize : size; +- if (diff !== 0) { +- addTotalSize(ctx, itemKey, diff, notifyTotalSize); +}; +function setInitialScrollSession(state, options = {}) { + var _a3, _b, _c, _d; @@ -10225,10 +11969,25 @@ index 95465f2..b028ddc 100644 + const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; + if (!kind) { + return clearInitialScrollSession(state); -+ } + } +- sizes.set(itemKey, size); +-} +- +-// src/utils/helpers.ts +-function isFunction(obj) { +- return typeof obj === "function"; +-} +-function isArray(obj) { +- return Array.isArray(obj); +-} +-var warned = /* @__PURE__ */ new Set(); +-function warnDevOnce(id, text) { +- if (IS_DEV && !warned.has(id)) { +- warned.add(id); +- console.warn(`[legend-list] ${text}`); + if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { + return clearInitialScrollSession(state); -+ } + } + const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; + state.initialScrollSession = createInitialScrollSession({ + bootstrap, @@ -10237,13 +11996,25 @@ index 95465f2..b028ddc 100644 + previousDataLength + }); + return state.initialScrollSession; -+} + } +-function roundSize(size) { +- return Math.floor(size * 8) / 8; +-} +-function isNullOrUndefined(value) { +- return value === null || value === void 0; +-} +-function getPadding(s, type) { +- var _a3, _b, _c; +- const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; +- return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; +var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; +var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; +var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; +function clearAdaptiveRenderExitTimeout(ctx) { + ctx.state.scheduledWork.cancel("adaptiveRender"); -+} + } +-function extractPadding(style, contentContainerStyle, type) { +- return getPadding(style, type) + getPadding(contentContainerStyle, type); +function scheduleAdaptiveRenderExit(ctx, exitDelay) { + const state = ctx.state; + clearAdaptiveRenderExitTimeout(ctx); @@ -10252,23 +12023,57 @@ index 95465f2..b028ddc 100644 + } else { + state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); + } -+} + } +-function findContainerId(ctx, key) { +function setAdaptiveRender(ctx, mode, reason) { -+ var _a3, _b; + var _a3, _b; +- const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); +- if (directMatch !== void 0) { +- return directMatch; +- } +- const numContainers = peek$(ctx, "numContainers"); +- for (let i = 0; i < numContainers; i++) { +- const itemKey = peek$(ctx, `containerItemKey${i}`); +- if (itemKey === key) { +- return i; +- } + const previousMode = peek$(ctx, "adaptiveRender"); + if (previousMode !== mode) { + set$(ctx, "adaptiveRender", mode); + (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); -+ } -+} + } +- return -1; + } +- +-// src/utils/getItemSize.ts +-function getKnownOrFixedSize(ctx, key, index, data, resolved) { +function resetAdaptiveRender(ctx) { -+ var _a3, _b; + var _a3, _b; +- const state = ctx.state; +- const { getFixedItemSize, getItemType } = state.props; +- let size = key ? state.sizesKnown.get(key) : void 0; +- if (size === void 0 && key && getFixedItemSize) { +- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; +- const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); +- if (fixedSize !== void 0) { +- size = fixedSize + ctx.scrollAxisGap; +- state.sizesKnown.set(key, size); +- } + clearAdaptiveRenderExitTimeout(ctx); + const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; + if (peek$(ctx, "adaptiveRender") !== mode) { + setAdaptiveRender(ctx, mode, "initial"); -+ } -+} + } +- return size; +-} +-function getKnownOrFixedItemSize(ctx, index) { +- const key = getId(ctx.state, index); +- return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); + } +-function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { +- for (let index = startIndex; index <= endIndex; index++) { +- if (getKnownOrFixedItemSize(ctx, index) === void 0) { +- return false; +function updateAdaptiveRender(ctx, scrollVelocity, options) { + var _a3, _b, _c; + const state = ctx.state; @@ -10292,13 +12097,292 @@ index 95465f2..b028ddc 100644 + } + } else { + resetAdaptiveRender(ctx); -+ } + } } -- scheduledWork.register("platformScrollCompletion", cancel); +- return true; } - - // src/core/doMaintainScrollAtEnd.ts -@@ -1430,6 +1221,7 @@ function doMaintainScrollAtEnd(ctx) { +-function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { +- var _a3, _b, _c, _d; ++ ++// src/core/doMaintainScrollAtEnd.ts ++function doMaintainScrollAtEnd(ctx) { + const state = ctx.state; + const { +- sizes, +- averageSizes, +- props: { estimatedItemSize, getItemType }, +- scrollingTo ++ didContainersLayout, ++ pendingNativeMVCPAdjust, ++ refScroller, ++ props: { maintainScrollAtEnd } + } = state; +- const sizeKnown = state.sizesKnown.get(key); +- if (sizeKnown !== void 0) { +- return sizeKnown; ++ const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); ++ const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout); ++ if (pendingNativeMVCPAdjust) { ++ state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd; ++ return false; + } +- let size; +- const renderedSize = sizes.get(key); +- if (preferCachedSize) { +- if (renderedSize !== void 0) { +- return renderedSize; +- } +- } +- size = getKnownOrFixedSize(ctx, key, index, data, resolved); +- if (size !== void 0) { +- setSize(ctx, key, size, notifyTotalSize); +- return size; +- } +- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; +- if (useAverageSize && !scrollingTo) { +- const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; +- if (averageSizeForType !== void 0) { +- size = roundSize(averageSizeForType); +- } +- } +- if (size === void 0 && renderedSize !== void 0) { +- return renderedSize; +- } +- if (size === void 0 && useAverageSize && scrollingTo) { +- const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; +- if (averageSizeForType !== void 0) { +- size = roundSize(averageSizeForType); +- } +- } +- if (size === void 0) { +- size = estimatedItemSize + ctx.scrollAxisGap; +- } +- setSize(ctx, key, size, notifyTotalSize); +- return size; +-} +-function getItemSizeAtIndex(ctx, index) { +- if (index === void 0 || index < 0) { +- return void 0; +- } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); +-} +- +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +- } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } +- } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } +- } +- return offset; +-} +- +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +- const state = ctx.state; +- const contentSize = getContentSize(ctx); +- let clampedOffset = offset; +- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { +- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); +- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; +- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; +- const maxOffset = baseMaxOffset + extraEndOffset; +- clampedOffset = Math.min(offset, maxOffset); +- } +- clampedOffset = Math.max(0, clampedOffset); +- return clampedOffset; +-} +- +-// src/core/finishScrollTo.ts +-function finishScrollTo(ctx) { +- var _a3, _b; +- const state = ctx.state; +- if (state == null ? void 0 : state.scrollingTo) { +- cancelScrollCompletionChecks(state); +- const resolvePendingScroll = state.pendingScrollResolve; +- state.pendingScrollResolve = void 0; +- const scrollingTo = state.scrollingTo; +- state.scrollHistory.length = 0; +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- if (state.pendingTotalSize !== void 0) { +- addTotalSize(ctx, null, state.pendingTotalSize); +- } +- { +- state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); +- } +- if (scrollingTo.isInitialScroll || state.initialScroll) { +- const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; +- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; +- finishInitialScroll(ctx, { +- onFinished: () => { +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +- }, +- preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, +- recalculateItems: true, +- schedulePreservedTargetClear: shouldPreserveResizeTarget, +- syncObservedOffset: isOffsetSession, +- waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame +- }); +- return; +- } +- recalculateSettledScroll(ctx); +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +- } +-} +- +-// src/core/doScrollTo.ts +-var SCROLL_END_IDLE_MS = 80; +-var SCROLL_END_MAX_MS = 1500; +-var SMOOTH_SCROLL_DURATION_MS = 320; +-var SCROLL_END_TARGET_EPSILON = 1; +-function doScrollTo(ctx, params) { +- var _a3, _b; +- const state = ctx.state; +- const { animated, horizontal, offset } = params; +- state.scheduledWork.cancel("platformScrollCompletion"); +- const scroller = state.refScroller.current; +- const node = scroller == null ? void 0 : scroller.getScrollableNode(); +- if (!scroller || !node) { +- return; +- } +- const isAnimated = !!animated; +- const isHorizontal = !!horizontal; +- const contentSize = isHorizontal ? getContentSize(ctx) : void 0; +- const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; +- const top = isHorizontal ? 0 : offset; +- scroller.scrollTo({ animated: isAnimated, x: left, y: top }); +- if (isAnimated) { +- const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; +- listenForScrollEnd(ctx, { +- readOffset: () => scroller.getCurrentScrollOffset(), +- target, +- targetOffset: offset +- }); +- } else { +- state.scroll = offset; +- const targetToken = state.scrollingTo; +- state.scheduledWork.timeout( +- () => { +- if (targetToken === state.scrollingTo) { +- finishScrollTo(ctx); +- } +- }, +- 100, +- "platformScrollCompletion" +- ); +- } +-} +-function listenForScrollEnd(ctx, params) { +- const { readOffset, target, targetOffset } = params; +- if (!target) { +- finishScrollTo(ctx); +- return; +- } +- const supportsScrollEnd = "onscrollend" in target; +- let idleTimeout; +- let settled = false; +- const { scheduledWork, scrollingTo: targetToken } = ctx.state; +- const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); +- const cleanup = () => { +- target.removeEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.removeEventListener("scrollend", onScrollEnd); +- } +- if (idleTimeout !== void 0) { +- clearTimeout(idleTimeout); +- } +- clearTimeout(maxTimeout); +- }; +- const cancel = () => { +- if (!settled) { +- settled = true; +- cleanup(); +- } +- }; +- const finish = (reason) => { +- if (settled) return; +- if (targetToken !== ctx.state.scrollingTo) { +- scheduledWork.cancel("platformScrollCompletion"); +- return; +- } +- const currentOffset = readOffset(); +- const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; +- if (reason === "scrollend" && !isNearTarget) { +- return; +- } +- scheduledWork.cancel("platformScrollCompletion"); +- finishScrollTo(ctx); +- }; +- const onScroll2 = () => { +- if (idleTimeout !== void 0) { +- clearTimeout(idleTimeout); +- } +- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); +- }; +- const onScrollEnd = () => finish("scrollend"); +- target.addEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.addEventListener("scrollend", onScrollEnd); +- } else { +- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); +- } +- scheduledWork.register("platformScrollCompletion", cancel); +-} +- +-// src/core/doMaintainScrollAtEnd.ts +-function doMaintainScrollAtEnd(ctx) { +- const state = ctx.state; +- const { +- didContainersLayout, +- pendingNativeMVCPAdjust, +- refScroller, +- props: { maintainScrollAtEnd } +- } = state; +- const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); +- const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout); +- if (pendingNativeMVCPAdjust) { +- state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd; +- return false; +- } +- if (shouldMaintainScrollAtEnd) { +- state.pendingMaintainScrollAtEnd = false; +- const contentSize = getContentSize(ctx); +- if (contentSize < state.scrollLength) { +- state.scroll = 0; ++ if (shouldMaintainScrollAtEnd) { ++ state.pendingMaintainScrollAtEnd = false; ++ const contentSize = getContentSize(ctx); ++ if (contentSize < state.scrollLength) { ++ state.scroll = 0; + } + if (!state.maintainingScrollAtEnd) { + const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; state.maintainingScrollAtEnd = pendingState; @@ -10306,10 +12390,27 @@ index 95465f2..b028ddc 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1788,6 +1580,27 @@ function prepareMVCP(ctx, dataChanged) { +@@ -1788,23 +1580,44 @@ function prepareMVCP(ctx, dataChanged) { } } +-// src/utils/getScrollVelocity.ts +-var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; +-var SCROLL_VELOCITY_HALF_LIFE_MS = 200; +-var getScrollVelocity = (state) => { +- const { scrollHistory } = state; +- const newestIndex = scrollHistory.length - 1; +- if (newestIndex < 1) { +- return 0; +- } +- const newest = scrollHistory[newestIndex]; +- if (Date.now() - newest.time > MAX_SCROLL_VELOCITY_WINDOW_MS) { +- return 0; +- } +- let direction = 0; +- let weightedVelocity = 0; +- let totalWeight = 0; +- for (let i = newestIndex; i > 0; i--) { +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { @@ -10320,36 +12421,236 @@ index 95465f2..b028ddc 100644 + const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; + return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; +} -+function scheduleFullDrawDistancePrewarm(ctx) { -+ const { state } = ctx; -+ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { ++function scheduleFullDrawDistancePrewarm(ctx) { ++ const { state } = ctx; ++ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { ++ return; ++ } ++ state.scheduledWork.frame(() => { ++ var _a3; ++ return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); ++ }, "fullDrawDistancePrewarm"); ++} ++ ++// src/utils/getScrollVelocity.ts ++var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; ++var SCROLL_VELOCITY_HALF_LIFE_MS = 200; ++var getScrollVelocity = (state) => { ++ const { scrollHistory } = state; ++ const newestIndex = scrollHistory.length - 1; ++ if (newestIndex < 1) { ++ return 0; ++ } ++ const newest = scrollHistory[newestIndex]; ++ if (Date.now() - newest.time > MAX_SCROLL_VELOCITY_WINDOW_MS) { ++ return 0; ++ } ++ let direction = 0; ++ let weightedVelocity = 0; ++ let totalWeight = 0; ++ for (let i = newestIndex; i > 0; i--) { + const current = scrollHistory[i]; + const previous = scrollHistory[i - 1]; + const scrollDiff = current.scroll - previous.scroll; +@@ -1823,281 +1636,604 @@ var getScrollVelocity = (state) => { + if (scrollDiff === 0 || timeDiff <= 0) { + continue; + } +- const age = newest.time - current.time; +- const weight = Math.exp(-age / SCROLL_VELOCITY_HALF_LIFE_MS); +- weightedVelocity += scrollDiff / timeDiff * weight; +- totalWeight += weight; +- } +- return totalWeight > 0 ? weightedVelocity / totalWeight : 0; +-}; +- +-// src/utils/hasActiveMVCPAnchorLock.ts +-function hasActiveMVCPAnchorLock(state) { +- const lock = state.mvcpAnchorLock; +- if (!lock) { +- return false; ++ const age = newest.time - current.time; ++ const weight = Math.exp(-age / SCROLL_VELOCITY_HALF_LIFE_MS); ++ weightedVelocity += scrollDiff / timeDiff * weight; ++ totalWeight += weight; ++ } ++ return totalWeight > 0 ? weightedVelocity / totalWeight : 0; ++}; ++ ++// src/utils/hasActiveMVCPAnchorLock.ts ++function hasActiveMVCPAnchorLock(state) { ++ const lock = state.mvcpAnchorLock; ++ if (!lock) { ++ return false; ++ } ++ if (Date.now() > lock.expiresAt) { ++ state.mvcpAnchorLock = void 0; ++ return false; ++ } ++ return true; ++} ++ ++// src/utils/isInMVCPActiveMode.ts ++function isInMVCPActiveMode(state) { ++ return state.dataChangeNeedsScrollUpdate || hasActiveMVCPAnchorLock(state); ++} ++ ++// src/core/updateScroll.ts ++function updateScroll(ctx, newScroll, forceUpdate, options) { ++ var _a3; ++ const state = ctx.state; ++ const { ignoreScrollFromMVCP, lastScrollAdjustForHistory, scrollAdjustHandler, scrollHistory, scrollingTo } = state; ++ const prevScroll = state.scroll; ++ if ((options == null ? void 0 : options.markHasScrolled) !== false) { ++ state.hasScrolled = true; ++ } ++ const currentTime = Date.now(); ++ state.lastBatchingAction = currentTime; ++ const adjust = scrollAdjustHandler.getAdjust(); ++ const adjustChanged = lastScrollAdjustForHistory !== void 0 && Math.abs(adjust - lastScrollAdjustForHistory) > 0.1; ++ if (adjustChanged) { ++ scrollHistory.length = 0; ++ } ++ state.lastScrollAdjustForHistory = adjust; ++ if (scrollingTo === void 0 && !(scrollHistory.length === 0 && newScroll === state.scroll)) { ++ if (!adjustChanged) { ++ scrollHistory.push({ scroll: newScroll, time: currentTime }); ++ } ++ } ++ if (scrollHistory.length > 5) { ++ scrollHistory.shift(); ++ } ++ if (ignoreScrollFromMVCP && !scrollingTo) { ++ const { lt, gt } = ignoreScrollFromMVCP; ++ if (lt && newScroll < lt || gt && newScroll > gt) { ++ state.ignoreScrollFromMVCPIgnored = true; ++ return; ++ } ++ } ++ state.scrollPrev = prevScroll; ++ state.scrollPrevTime = state.scrollTime; ++ state.scroll = newScroll; ++ state.scrollTime = currentTime; ++ const scrollDelta = Math.abs(newScroll - prevScroll); ++ const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; ++ const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; ++ const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); ++ const scrollLength = state.scrollLength; ++ const isLargeUserScrollJump = scrollLength > 0 && scrollingTo === void 0 && scrollDelta > scrollLength && !state.pendingNativeMVCPAdjust; ++ const scrollVelocity = getScrollVelocity(state); ++ updateAdaptiveRender(ctx, scrollVelocity, { forceLight: isLargeUserScrollJump }); ++ const lastCalculated = state.scrollLastCalculate; ++ const useAggressiveItemRecalculation = isInMVCPActiveMode(state); ++ const shouldUpdate = useAggressiveItemRecalculation || didResolvePendingNativeMVCPAdjust || allowedEdge !== void 0 || forceUpdate || lastCalculated === void 0 || Math.abs(state.scroll - lastCalculated) > 2; ++ if (shouldUpdate) { ++ state.scrollLastCalculate = state.scroll; ++ state.ignoreScrollFromMVCPIgnored = false; ++ state.lastScrollDelta = scrollDelta; ++ const runCalculateItems = () => { ++ var _a4; ++ const calculateItemsParams = { ++ doMVCP: scrollingTo !== void 0, ++ scrollVelocity ++ }; ++ if (isLargeUserScrollJump) { ++ calculateItemsParams.drawDistanceMode = "visible-first"; ++ } ++ (_a4 = state.triggerCalculateItemsInView) == null ? void 0 : _a4.call(state, calculateItemsParams); ++ checkThresholds(ctx, allowedEdge); ++ }; ++ if (isLargeUserScrollJump) { ++ state.mvcpAnchorLock = void 0; ++ state.pendingNativeMVCPAdjust = void 0; ++ state.userScrollAnchorReset = { keys: /* @__PURE__ */ new Set() }; ++ state.scheduledWork.cancel("mvcpRecalculate"); ++ flushSync(runCalculateItems); ++ scheduleFullDrawDistancePrewarm(ctx); ++ } else { ++ runCalculateItems(); ++ } ++ const shouldMaintainScrollAtEndAfterPendingSettle = !!state.pendingMaintainScrollAtEnd || !!((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onDataChange); ++ if (didResolvePendingNativeMVCPAdjust && shouldMaintainScrollAtEndAfterPendingSettle) { ++ state.pendingMaintainScrollAtEnd = false; ++ doMaintainScrollAtEnd(ctx); ++ } ++ state.dataChangeNeedsScrollUpdate = false; ++ state.lastScrollDelta = 0; ++ } ++} ++ ++// src/core/scrollTo.ts ++function getAverageSizeSnapshot(state) { ++ if (Object.keys(state.averageSizes).length === 0) { ++ return void 0; ++ } ++ const snapshot = {}; ++ for (const itemType in state.averageSizes) { ++ const averages = state.averageSizes[itemType]; ++ snapshot[itemType] = averages.avg; ++ } ++ return snapshot; ++} ++function syncInitialScrollNativeWatchdog(state, options) { ++ var _a3; ++ const { isInitialScroll, requestedOffset, targetOffset } = options; ++ const existingWatchdog = initialScrollWatchdog.get(state); ++ const shouldWatchInitialNativeScroll = !state.didFinishInitialScroll && (isInitialScroll || !!existingWatchdog) && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); ++ const shouldClearInitialNativeScrollWatchdog = !state.didFinishInitialScroll && !!existingWatchdog && initialScrollWatchdog.isAtZeroTargetOffset(requestedOffset); ++ if (shouldWatchInitialNativeScroll) { ++ state.hasScrolled = false; ++ initialScrollWatchdog.set(state, { ++ startScroll: (_a3 = existingWatchdog == null ? void 0 : existingWatchdog.startScroll) != null ? _a3 : state.scroll, ++ targetOffset ++ }); + return; + } -+ state.scheduledWork.frame(() => { -+ var _a3; -+ return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); -+ }, "fullDrawDistancePrewarm"); ++ if (shouldClearInitialNativeScrollWatchdog) { ++ initialScrollWatchdog.clear(state); ++ } +} -+ - // src/utils/getScrollVelocity.ts - var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; - var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -1885,6 +1698,9 @@ function updateScroll(ctx, newScroll, forceUpdate, options) { - state.scrollPrevTime = state.scrollTime; - state.scroll = newScroll; - state.scrollTime = currentTime; -+ if ((options == null ? void 0 : options.fromNativeScrollEvent) && !adjustChanged && !state.pendingNativeMVCPAdjust) { -+ releaseScrollTargetSettleIfMoved(ctx); -+ } - const scrollDelta = Math.abs(newScroll - prevScroll); - const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; - const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; -@@ -2005,99 +1821,417 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { - if (start === void 0) { - return void 0; - } -- if (targetIndex !== void 0 && state.positions[start] === void 0) { -- return { end: start, start }; ++function findPositionIndexAtOrBeforeOffset(ctx, offset) { ++ const state = ctx.state; ++ const dataLength = state.props.data.length; ++ let low = 0; ++ let high = dataLength - 1; ++ let match; ++ while (low <= high) { ++ const mid = Math.floor((low + high) / 2); ++ const top = state.positions[mid]; ++ if (top === void 0) { ++ high = mid - 1; ++ } else { ++ if (top <= offset) { ++ match = mid; ++ low = mid + 1; ++ } else { ++ high = mid - 1; ++ } ++ } ++ } ++ return match; ++} ++function getItemBottom(ctx, index) { ++ var _a3; ++ const top = ctx.state.positions[index]; ++ if (top === void 0) { ++ return void 0; ++ } ++ const itemSize = (_a3 = getItemSizeAtIndex(ctx, index)) != null ? _a3 : 0; ++ return top + (Number.isFinite(itemSize) ? itemSize : 0); ++} ++function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { ++ const state = ctx.state; ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return void 0; ++ } ++ const viewportStart = Math.max(0, targetOffset); ++ const viewportEnd = Math.max(viewportStart, targetOffset + state.scrollLength); ++ let start = targetIndex !== void 0 ? Math.max(0, Math.min(dataLength - 1, targetIndex)) : findPositionIndexAtOrBeforeOffset(ctx, viewportStart); ++ if (start === void 0) { ++ return void 0; ++ } + if (targetIndex !== void 0 && state.positions[start] === void 0) { + return { end: start, start }; + } @@ -10446,23 +12747,36 @@ index 95465f2..b028ddc 100644 + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); + } -+ } + } +- if (Date.now() > lock.expiresAt) { +- state.mvcpAnchorLock = void 0; +- return false; + if (forceScroll || !isInitialScroll || Platform.OS === "android") { + doScrollTo(ctx, { animated, horizontal, offset }); + } else { + state.scroll = offset; -+ } -+} -+ + } +- return true; + } + +-// src/utils/isInMVCPActiveMode.ts +-function isInMVCPActiveMode(state) { +- return state.dataChangeNeedsScrollUpdate || hasActiveMVCPAnchorLock(state); +// src/core/scrollTargetSettle.ts +var SETTLE_POSITION_EPSILON = 0.5; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; +var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; ++function releaseScrollTargetForUserInteraction(ctx) { ++ if (ctx.state.scrollTargetSettle) { ++ clearScrollTargetSettle(ctx.state); ++ } ++} +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; + state.scheduledWork.cancel("scrollTargetSettle"); ++ state.scheduledWork.cancel("scrollTargetSettleDeadline"); +} +function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; @@ -10479,35 +12793,56 @@ index 95465f2..b028ddc 100644 + deadline: now + SETTLE_MAX_MS, + expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), ++ measuredIndex: void 0, + quietPasses: 0, -+ requestedAt: now, + viewOffset, + viewPosition + }; -+} ++ state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline"); + } +- +-// src/core/updateScroll.ts +-function updateScroll(ctx, newScroll, forceUpdate, options) { +function getSettleTargetOffset(ctx, settle, index, position) { + const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; + return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} +function applyScrollTargetCorrection(ctx, id) { -+ var _a3; -+ const state = ctx.state; + var _a3; + const state = ctx.state; +- const { ignoreScrollFromMVCP, lastScrollAdjustForHistory, scrollAdjustHandler, scrollHistory, scrollingTo } = state; +- const prevScroll = state.scroll; +- if ((options == null ? void 0 : options.markHasScrolled) !== false) { +- state.hasScrolled = true; + const settle = state.scrollTargetSettle; + if (!settle || settle.id !== id) { + return; -+ } + } +- const currentTime = Date.now(); +- state.lastBatchingAction = currentTime; +- const adjust = scrollAdjustHandler.getAdjust(); +- const adjustChanged = lastScrollAdjustForHistory !== void 0 && Math.abs(adjust - lastScrollAdjustForHistory) > 0.1; +- if (adjustChanged) { +- scrollHistory.length = 0; + const index = state.indexByKey.get(id); + const position = index === void 0 ? void 0 : state.positions[index]; + if (index === void 0 || position === void 0 || Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return; -+ } + } +- state.lastScrollAdjustForHistory = adjust; +- if (scrollingTo === void 0 && !(scrollHistory.length === 0 && newScroll === state.scroll)) { +- if (!adjustChanged) { +- scrollHistory.push({ scroll: newScroll, time: currentTime }); +- } + if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { + clearScrollTargetSettle(state); + return; -+ } + } +- if (scrollHistory.length > 5) { +- scrollHistory.shift(); + settle.corrections++; -+ settle.requestedAt = Date.now(); ++ settle.measuredIndex = void 0; + scrollTo(ctx, { + animated: false, + index, @@ -10522,21 +12857,71 @@ index 95465f2..b028ddc 100644 + }); + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +} -+function settleScrollTarget(ctx, isCompensating) { ++function settleScrollTarget(ctx, options) { ++ var _a3; + const state = ctx.state; + const settle = state.scrollTargetSettle; + if (!settle) { + return false; + } +- if (ignoreScrollFromMVCP && !scrollingTo) { +- const { lt, gt } = ignoreScrollFromMVCP; +- if (lt && newScroll < lt || gt && newScroll > gt) { +- state.ignoreScrollFromMVCPIgnored = true; +- return; ++ const measured = options == null ? void 0 : options.minIndexSizeChanged; ++ if (measured !== void 0) { ++ settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); + } -+ if (isCompensating) { ++ if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return false; -+ } + } + settle.quietPasses = 0; + settle.expiresAt = Date.now() + SETTLE_TTL_MS; + return false; -+ } + } +- state.scrollPrev = prevScroll; +- state.scrollPrevTime = state.scrollTime; +- state.scroll = newScroll; +- state.scrollTime = currentTime; +- const scrollDelta = Math.abs(newScroll - prevScroll); +- const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; +- const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; +- const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); +- const scrollLength = state.scrollLength; +- const isLargeUserScrollJump = scrollLength > 0 && scrollingTo === void 0 && scrollDelta > scrollLength && !state.pendingNativeMVCPAdjust; +- const scrollVelocity = getScrollVelocity(state); +- updateAdaptiveRender(ctx, scrollVelocity, { forceLight: isLargeUserScrollJump }); +- const lastCalculated = state.scrollLastCalculate; +- const useAggressiveItemRecalculation = isInMVCPActiveMode(state); +- const shouldUpdate = useAggressiveItemRecalculation || didResolvePendingNativeMVCPAdjust || allowedEdge !== void 0 || forceUpdate || lastCalculated === void 0 || Math.abs(state.scroll - lastCalculated) > 2; +- if (shouldUpdate) { +- state.scrollLastCalculate = state.scroll; +- state.ignoreScrollFromMVCPIgnored = false; +- state.lastScrollDelta = scrollDelta; +- const runCalculateItems = () => { +- var _a4; +- const calculateItemsParams = { +- doMVCP: scrollingTo !== void 0, +- scrollVelocity +- }; +- if (isLargeUserScrollJump) { +- calculateItemsParams.drawDistanceMode = "visible-first"; +- } +- (_a4 = state.triggerCalculateItemsInView) == null ? void 0 : _a4.call(state, calculateItemsParams); +- checkThresholds(ctx, allowedEdge); +- }; +- if (isLargeUserScrollJump) { +- state.mvcpAnchorLock = void 0; +- state.pendingNativeMVCPAdjust = void 0; +- state.userScrollAnchorReset = { keys: /* @__PURE__ */ new Set() }; +- state.scheduledWork.cancel("mvcpRecalculate"); +- flushSync(runCalculateItems); +- scheduleFullDrawDistancePrewarm(ctx); +- } else { +- runCalculateItems(); + const index = state.indexByKey.get(settle.id); + const position = index === void 0 ? void 0 : state.positions[index]; + if (index === void 0 || position === void 0) { @@ -10548,13 +12933,21 @@ index 95465f2..b028ddc 100644 + clearScrollTargetSettle(state); + return false; + } ++ if (settle.measuredIndex === void 0 || settle.measuredIndex > index) { ++ return false; ++ } + const targetOffset = getSettleTargetOffset(ctx, settle, index, position); + const diff = targetOffset - state.scroll; + if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ settle.measuredIndex = void 0; + settle.quietPasses++; + if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { + clearScrollTargetSettle(state); -+ } + } +- const shouldMaintainScrollAtEndAfterPendingSettle = !!state.pendingMaintainScrollAtEnd || !!((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onDataChange); +- if (didResolvePendingNativeMVCPAdjust && shouldMaintainScrollAtEndAfterPendingSettle) { +- state.pendingMaintainScrollAtEnd = false; +- doMaintainScrollAtEnd(ctx); + return false; + } + settle.quietPasses = 0; @@ -10562,17 +12955,6 @@ index 95465f2..b028ddc 100644 + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} -+var SETTLE_ECHO_WINDOW_MS = 150; -+function releaseScrollTargetSettleIfMoved(ctx, _scrollOffset) { -+ const state = ctx.state; -+ const settle = state.scrollTargetSettle; -+ if (!settle) { -+ return; -+ } -+ if (Date.now() - settle.requestedAt > SETTLE_ECHO_WINDOW_MS) { -+ clearScrollTargetSettle(state); -+ } -+} + +// src/core/cancelImperativeScroll.ts +function cancelScrollCompletionChecks({ scheduledWork }) { @@ -10604,7 +12986,9 @@ index 95465f2..b028ddc 100644 + nativeEvent: { + ...event.nativeEvent, + contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -+ } + } +- state.dataChangeNeedsScrollUpdate = false; +- state.lastScrollDelta = 0; + }; +} +function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { @@ -10621,9 +13005,13 @@ index 95465f2..b028ddc 100644 + (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 + ) + ); -+ } -+} -+ + } + } + +-// src/core/scrollTo.ts +-function getAverageSizeSnapshot(state) { +- if (Object.keys(state.averageSizes).length === 0) { +- return void 0; +// src/utils/setInitialRenderState.ts +function resetInitialRenderState(ctx, { + resetLayout, @@ -10633,18 +13021,31 @@ index 95465f2..b028ddc 100644 + if (resetLayout) { + state.didContainersLayout = false; + state.queuedInitialLayout = false; -+ } + } +- const snapshot = {}; +- for (const itemType in state.averageSizes) { +- const averages = state.averageSizes[itemType]; +- snapshot[itemType] = averages.avg; + if (resetInitialScroll) { + state.didFinishInitialScroll = false; } -- if (targetIndex === void 0) { -- const startBottom = getItemBottom(ctx, start); -- if (startBottom === void 0 || startBottom <= viewportStart) { -- return void 0; -- } +- return snapshot; + set$(ctx, "readyToRender", false); + resetAdaptiveRender(ctx); -+} + } +-function syncInitialScrollNativeWatchdog(state, options) { +- var _a3; +- const { isInitialScroll, requestedOffset, targetOffset } = options; +- const existingWatchdog = initialScrollWatchdog.get(state); +- const shouldWatchInitialNativeScroll = !state.didFinishInitialScroll && (isInitialScroll || !!existingWatchdog) && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); +- const shouldClearInitialNativeScrollWatchdog = !state.didFinishInitialScroll && !!existingWatchdog && initialScrollWatchdog.isAtZeroTargetOffset(requestedOffset); +- if (shouldWatchInitialNativeScroll) { +- state.hasScrolled = false; +- initialScrollWatchdog.set(state, { +- startScroll: (_a3 = existingWatchdog == null ? void 0 : existingWatchdog.startScroll) != null ? _a3 : state.scroll, +- targetOffset +- }); +- return; +function setInitialRenderState(ctx, { + didLayout, + didInitialScroll @@ -10657,51 +13058,53 @@ index 95465f2..b028ddc 100644 + if (didLayout) { + state.didContainersLayout = true; } -- while (start > 0) { -- const top = state.positions[start]; -- if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { -- break; -- } -- start--; +- if (shouldClearInitialNativeScrollWatchdog) { +- initialScrollWatchdog.clear(state); + if (didInitialScroll) { + state.didFinishInitialScroll = true; } -- while (start > 0) { -- const previousBottom = getItemBottom(ctx, start - 1); -- if (previousBottom === void 0 || previousBottom <= viewportStart) { -- break; +-} +-function findPositionIndexAtOrBeforeOffset(ctx, offset) { +- const state = ctx.state; +- const dataLength = state.props.data.length; +- let low = 0; +- let high = dataLength - 1; +- let match; +- while (low <= high) { +- const mid = Math.floor((low + high) / 2); +- const top = state.positions[mid]; +- if (top === void 0) { +- high = mid - 1; +- } else { +- if (top <= offset) { +- match = mid; +- low = mid + 1; +- } else { +- high = mid - 1; + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); + if (isReadyToRender && !peek$(ctx, "readyToRender")) { + set$(ctx, "readyToRender", true); + setAdaptiveRender(ctx, "normal", "ready"); + if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { + scheduleFullDrawDistancePrewarm(ctx); - } -- start--; -- } -- let end = start; -- while (end + 1 < dataLength) { -- const nextTop = state.positions[end + 1]; -- if (nextTop === void 0 || nextTop > viewportEnd) { -- break; ++ } + if (!state.didLoad) { + state.didLoad = true; + if (onLoad) { + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); -+ } + } } -- end++; } -- return { end, start }; +- return match; } --function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -- if (range) { -- ctx.state.scrollTargetPinnedRange = range; -- ctx.state.scrollForNextCalculateItemsInView = void 0; -- } else { -- ctx.state.scrollTargetPinnedRange = void 0; +-function getItemBottom(ctx, index) { +- var _a3; +- const top = ctx.state.positions[index]; +- if (top === void 0) { +- return void 0; - } +- const itemSize = (_a3 = getItemSizeAtIndex(ctx, index)) != null ? _a3 : 0; +- return top + (Number.isFinite(itemSize) ? itemSize : 0); + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -10710,8 +13113,7 @@ index 95465f2..b028ddc 100644 + state.scrollPending = offset; + state.scrollPrev = offset; } --function scrollTo(ctx, params) { -- var _a3, _b; +-function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -10722,6 +13124,74 @@ index 95465f2..b028ddc 100644 + setInitialScrollSession(state); +} +function supersedeInitialScroll(ctx) { ++ var _a3, _b, _c; + const state = ctx.state; +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return void 0; +- } +- const viewportStart = Math.max(0, targetOffset); +- const viewportEnd = Math.max(viewportStart, targetOffset + state.scrollLength); +- let start = targetIndex !== void 0 ? Math.max(0, Math.min(dataLength - 1, targetIndex)) : findPositionIndexAtOrBeforeOffset(ctx, viewportStart); +- if (start === void 0) { +- return void 0; +- } +- if (targetIndex !== void 0 && state.positions[start] === void 0) { +- return { end: start, start }; +- } +- if (targetIndex === void 0) { +- const startBottom = getItemBottom(ctx, start); +- if (startBottom === void 0 || startBottom <= viewportStart) { +- return void 0; +- } +- } +- while (start > 0) { +- const top = state.positions[start]; +- if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { +- break; +- } +- start--; +- } +- while (start > 0) { +- const previousBottom = getItemBottom(ctx, start - 1); +- if (previousBottom === void 0 || previousBottom <= viewportStart) { +- break; ++ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; ++ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { ++ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { ++ cancelAnimationFrame(bootstrapInitialScroll.frameHandle); + } +- start--; +- } +- let end = start; +- while (end + 1 < dataLength) { +- const nextTop = state.positions[end + 1]; +- if (nextTop === void 0 || nextTop > viewportEnd) { +- break; ++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { ++ cancelScrollCompletionChecks(state); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; + } +- end++; +- } +- return { end, start }; +-} +-function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); +- if (range) { +- ctx.state.scrollTargetPinnedRange = range; +- ctx.state.scrollForNextCalculateItemsInView = void 0; +- } else { +- ctx.state.scrollTargetPinnedRange = void 0; ++ initialScrollCompletion.resetFlags(state); ++ setInitialScrollSession(state, { bootstrap: null }); ++ finishInitialScroll(ctx); + } + } +-function scrollTo(ctx, params) { +- var _a3, _b; ++function finishInitialScroll(ctx, options) { + var _a3, _b, _c; const state = ctx.state; - const { noScrollingTo, forceScroll, ...scrollTarget } = params; @@ -10744,11 +13214,7 @@ index 95465f2..b028ddc 100644 - if (!noScrollingTo) { - if (isInitialScroll) { - initialScrollCompletion.resetFlags(state); -+ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; -+ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { -+ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { -+ cancelAnimationFrame(bootstrapInitialScroll.frameHandle); - } +- } - const averageSizeSnapshot = getAverageSizeSnapshot(state); - state.scrollingTo = { - ...scrollTarget, @@ -10758,19 +13224,6 @@ index 95465f2..b028ddc 100644 - }; - if (!isInitialScroll) { - pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); -+ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { -+ cancelScrollCompletionChecks(state); -+ state.scrollingTo = void 0; -+ state.scrollTargetPinnedRange = void 0; -+ } -+ initialScrollCompletion.resetFlags(state); -+ setInitialScrollSession(state, { bootstrap: null }); -+ finishInitialScroll(ctx); -+ } -+} -+function finishInitialScroll(ctx, options) { -+ var _a3, _b, _c; -+ const state = ctx.state; + if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { + syncInitialScrollOffset(state, options.resolvedOffset); + } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { @@ -10839,7 +13292,15 @@ index 95465f2..b028ddc 100644 } // src/core/scrollToIndex.ts -@@ -4346,8 +4480,17 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4329,6 +4465,7 @@ function calculateItemsInView(ctx, params = {}) { + startIndex + }); + totalSize = getContentSize(ctx); ++ const minIndexSizeChangedThisPass = dataChanged ? 0 : minIndexSizeChanged; + if (minIndexSizeChanged !== void 0) { + state.minIndexSizeChanged = void 0; + } +@@ -4346,8 +4483,17 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -10853,13 +13314,13 @@ index 95465f2..b028ddc 100644 + } + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { -+ settleScrollTarget(ctx, isCompensating); ++ settleScrollTarget(ctx, { isCompensating, minIndexSizeChanged: minIndexSizeChangedThisPass }); + } + if (didMVCPAdjust) { updateScroll2(state.scroll); updateScrollRange(); } -@@ -5894,6 +6037,119 @@ function useRafCoalescer(callback) { +@@ -5894,6 +6040,119 @@ function useRafCoalescer(callback) { return coalescer; } @@ -10979,16 +13440,18 @@ index 95465f2..b028ddc 100644 // src/components/webConstants.ts var LEGEND_LIST_CONTENT_CONTAINER_CLASS = "legend-list-content-container"; var LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS = "legend-list-scrollbar-x-hidden"; -@@ -5985,6 +6241,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -5985,6 +6244,10 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; +var SMOOTH_SCROLL_MAX_MS = 2e3; ++var BORROW_MEASURE_ATTEMPTS = 2; ++var USER_INTERACTION_EVENTS = ["wheel", "pointerdown", "touchstart", "keydown"]; var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6053,6 +6311,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6053,6 +6316,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -11000,33 +13463,47 @@ index 95465f2..b028ddc 100644 const getMaxScrollOffset = useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6073,6 +6336,59 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,6 +6341,95 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); + const paddedNodeRef = useRef(null); ++ const borrowedExtentsRef = useRef(/* @__PURE__ */ new Map()); ++ const borrowIdRef = useRef(0); ++ const borrowWatchRef = useRef(0); + const animatedPaddingReleaseRef = useRef(void 0); + const withReachableExtent = useCallback( + (offset, maxOffset, animated, run) => { + var _a4; + const contentNode = contentRef.current; -+ const committedMaxOffset = getCommittedMaxScrollOffset(maxOffset); ++ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); ++ const committedMaxOffset = getCommittedMaxScrollOffset(getMaxScrollOffset()); + if (!contentNode || offset <= committedMaxOffset + SCROLL_EXTENT_EPSILON) { + run(clampOffset(offset, maxOffset)); + return; + } -+ const release = addTemporaryEndPadding( -+ contentNode, -+ paddingEndProp, -+ offset - committedMaxOffset + SCROLL_EXTENT_EPSILON -+ ); ++ const releases = []; ++ for (let attempt = 0; attempt < BORROW_MEASURE_ATTEMPTS; attempt++) { ++ const shortfall = offset - getMaxScrollOffset(); ++ if (shortfall <= 0) { ++ break; ++ } ++ releases.push(addTemporaryEndPadding(contentNode, paddingEndProp, shortfall + SCROLL_EXTENT_EPSILON)); ++ } ++ const borrowId = ++borrowIdRef.current; ++ const release = () => { ++ for (const releaseOne of releases) { ++ releaseOne(); ++ } ++ borrowedExtentsRef.current.delete(borrowId); ++ }; ++ borrowedExtentsRef.current.set(borrowId, Math.max(0, getMaxScrollOffset() - committedMaxOffset)); + paddedNodeRef.current = contentNode; + run(offset); + if (!animated) { + scheduleTemporaryEndPaddingRelease(contentNode, paddingEndProp, release); + return; + } -+ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); + const scrollTarget = getScrollTarget(); + const supportsScrollEnd = !!scrollTarget && "onscrollend" in scrollTarget; + const finish = () => { @@ -11035,21 +13512,40 @@ index 95465f2..b028ddc 100644 + } + animatedPaddingReleaseRef.current = void 0; + clearTimeout(settleTimeout); -+ scrollTarget == null ? void 0 : scrollTarget.removeEventListener("scrollend", finish); ++ scrollTarget == null ? void 0 : scrollTarget.removeEventListener("scrollend", finishIfArrived); ++ cancelAnimationFrame(borrowWatchRef.current); + release(); + }; ++ const finishIfArrived = () => { ++ if (Math.abs(getCurrentScrollOffset() - offset) <= SCROLL_EXTENT_EPSILON) { ++ finish(); ++ } ++ }; ++ const releaseWhenContentCommits = () => { ++ if (animatedPaddingReleaseRef.current !== finish) { ++ return; ++ } ++ if (getCommittedMaxScrollOffset(getMaxScrollOffset()) + SCROLL_EXTENT_EPSILON >= offset) { ++ finish(); ++ return; ++ } ++ borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); ++ }; + animatedPaddingReleaseRef.current = finish; ++ cancelAnimationFrame(borrowWatchRef.current); ++ borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); + const settleTimeout = setTimeout(finish, SMOOTH_SCROLL_MAX_MS); + if (supportsScrollEnd) { -+ scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finish); ++ scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finishIfArrived); + } + }, -+ [getCommittedMaxScrollOffset, getScrollTarget, paddingEndProp] ++ [getCommittedMaxScrollOffset, getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, paddingEndProp] + ); + useEffect( + () => () => { + var _a4; + (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); ++ cancelAnimationFrame(borrowWatchRef.current); + const paddedNode = paddedNodeRef.current; + if (paddedNode) { + releaseAllTemporaryEndPadding(paddedNode); @@ -11057,10 +13553,13 @@ index 95465f2..b028ddc 100644 + }, + [] + ); ++ const reportUserInteraction = useCallback(() => { ++ releaseScrollTargetForUserInteraction(ctx); ++ }, [ctx]); const scrollToLocalOffset = useCallback( (offset, animated) => { const scrollElement = scrollRef.current; -@@ -6095,14 +6411,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6095,14 +6452,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); options.left = left; options.top = top; @@ -11087,7 +13586,7 @@ index 95465f2..b028ddc 100644 ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6451,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6128,8 +6492,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -11097,13 +13596,16 @@ index 95465f2..b028ddc 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6142,7 +6464,12 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6142,7 +6505,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ if (!onScroll2 || !scrollRef.current) { return; } - const contentSize = getContentSize2(contentRef.current); + const rawContentSize = getContentSize2(contentRef.current); -+ const temporaryPadding = getTemporaryEndPadding(contentRef.current, paddingEndProp); ++ let temporaryPadding = 0; ++ for (const bought of borrowedExtentsRef.current.values()) { ++ temporaryPadding = Math.max(temporaryPadding, bought); ++ } + const contentSize = temporaryPadding ? { + height: horizontal ? rawContentSize.height : rawContentSize.height - temporaryPadding, + width: horizontal ? rawContentSize.width - temporaryPadding : rawContentSize.width @@ -11111,22 +13613,25 @@ index 95465f2..b028ddc 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6358,10 +6685,12 @@ function useValueListener$(key, callback) { - } - - // src/components/ScrollAdjust.tsx --function getScrollAdjustAxis(horizontal) { -+function getScrollAdjustAxis(horizontal, rtl = false) { - return horizontal ? { - contentSizeKey: "scrollWidth", -- paddingEndProp: "paddingRight", -+ // The end side under RTL is the left, matching how the list pads its own content and -+ // which property the scroll view borrows room on. -+ paddingEndProp: rtl ? "paddingLeft" : "paddingRight", - viewportSizeKey: "clientWidth", - x: 1, - y: 0 -@@ -6390,8 +6719,6 @@ function ScrollAdjust() { +@@ -6200,11 +6571,17 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + const target = getScrollTarget(); + if (!target) return; + target.addEventListener("scroll", handleScroll, { passive: true }); ++ for (const type of USER_INTERACTION_EVENTS) { ++ target.addEventListener(type, reportUserInteraction, { passive: true }); ++ } + if ("onscrollend" in target) { + target.addEventListener("scrollend", emitScrollEnd); + } + return () => { + target.removeEventListener("scroll", handleScroll); ++ for (const type of USER_INTERACTION_EVENTS) { ++ target.removeEventListener(type, reportUserInteraction); ++ } + if ("onscrollend" in target) { + target.removeEventListener("scrollend", emitScrollEnd); + } +@@ -6390,8 +6767,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3.useRef(0); const lastScrollAdjustUserOffsetRef = React3.useRef(0); @@ -11135,16 +13640,7 @@ index 95465f2..b028ddc 100644 const contentNodeRef = React3.useRef(null); const callback = React3.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6402,7 +6729,7 @@ function ScrollAdjust() { - const target = getScrollAdjustTarget(ctx, contentNodeRef.current); - if (target) { - const horizontal = !!ctx.state.props.horizontal; -- const axis = getScrollAdjustAxis(horizontal); -+ const axis = getScrollAdjustAxis(horizontal, isHorizontalRTL(ctx.state)); - const { contentNode, scrollElement: el } = target; - const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; - const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6417,29 +6744,10 @@ function ScrollAdjust() { +@@ -6417,29 +6792,10 @@ function ScrollAdjust() { const nextScroll = currentScroll + scrollDelta; const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; if (needsTemporaryPadding) { @@ -11176,7 +13672,7 @@ index 95465f2..b028ddc 100644 } else { scrollBy(); } -@@ -8267,6 +8575,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8267,6 +8623,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); diff --git a/shared/tests/e2e/ios-appium/all.test.ts b/shared/tests/e2e/ios-appium/all.test.ts index d5895c7e9be7..a6af9d957d56 100644 --- a/shared/tests/e2e/ios-appium/all.test.ts +++ b/shared/tests/e2e/ios-appium/all.test.ts @@ -4,6 +4,7 @@ // each) and the app stays warm between flows. import './flows/android-activity-restart.test' import './flows/chat-conversation.test' +import './flows/chat-search-hit.test' import './flows/chat-send-message.test' import './flows/crypto-outputs.test' import './flows/crypto-subtabs.test' diff --git a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts new file mode 100644 index 000000000000..00ca773a5542 --- /dev/null +++ b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts @@ -0,0 +1,150 @@ +import {expect} from '@wdio/globals' +import {anyExist, el, els, tab, waitForTestID} from '../helpers/elements' +import {escapeToTabs} from '../helpers/navigate' +import * as T from '../../shared/test-ids' + +// More steps than the thread has hits, so the search wraps around and lands on hits it has already +// visited from a different scroll position - a case that used to leave the hit off screen. +const WRAPPING_STEPS = 20 +// A word common enough to match throughout the thread, so hits span messages of different heights. +const QUERY = 'one' +// A word whose hit sits among the messages already on screen: jumping a few rows is the case where +// the list has nothing to load and the scroll lands against a content size that has not caught up. +const SAME_SCREEN_QUERY = 'working' + +// iOS 26 puts the conversation's header actions in a native overflow menu (one glass pill), so +// there is no React view to carry a testID - the bar button and its menu item are addressed by the +// accessibility labels the platform exposes. Other platforms render the search control directly. +const openThreadSearch = async () => { + if (browser.isIOS) { + await browser.$('~More').waitForExist({timeout: 5000, timeoutMsg: 'header overflow menu never appeared'}) + await browser.$('~More').click() + await browser.$('~Search').waitForExist({timeout: 5000, timeoutMsg: 'search menu item never appeared'}) + await browser.$('~Search').click() + return + } + await waitForTestID(T.CHAT_HEADER_SEARCH_BUTTON, 5000) + await el(T.CHAT_HEADER_SEARCH_BUTTON).click() +} + +const boundsOf = async (id: string) => { + const element = el(id) + const [location, size] = await Promise.all([element.getLocation(), element.getSize()]) + return {height: size.height, y: location.y} +} + +// The row keeps its marker while it is the selected hit, but a virtualised list renders rows +// outside the viewport too - so the marker existing says nothing about whether it can be seen. +// Compare where the row is against where the list is. +const hitOverlapsViewport = async (): Promise => { + const [hit, list] = await Promise.all([boundsOf(T.CHAT_SEARCH_HIT), boundsOf(T.CHAT_MESSAGE_LIST)]) + const overlap = Math.min(hit.y + hit.height, list.y + list.height) - Math.max(hit.y, list.y) + if (overlap <= 0) { + console.log(`hit off screen: row ${hit.y}..${hit.y + hit.height}, list ${list.y}..${list.y + list.height}`) + } + return overlap > 0 +} + +const runSearch = async (query: string, steps: number) => { + await openThreadSearch() + await waitForTestID(T.CHAT_THREAD_SEARCH_NEXT, 5000) + // The search bar focuses itself on mount, so the query goes straight to the keyboard. + await browser.keys(query.split('')) + await browser.keys(['\n']) + + // Results stream in from the server; the first hit is selected once they arrive. + await waitForTestID(T.CHAT_SEARCH_HIT, 15000) + await browser.pause(1200) + expect(await hitOverlapsViewport()).toBe(true) + + for (let step = 0; step < steps; step++) { + await el(T.CHAT_THREAD_SEARCH_PREV).click() + + // Give the jump, and the measurements that follow it, time to settle before looking. A hit that + // lands and then drifts off screen is exactly the failure this is watching for. + await browser.pause(1200) + + await expect(el(T.CHAT_SEARCH_HIT)).toExist() + expect(await hitOverlapsViewport()).toBe(true) + } +} + +const closeThreadSearch = async () => { + await el(T.CHAT_THREAD_SEARCH_CANCEL).click() + await browser.pause(500) +} + +// Drag the thread without lifting into a fling, so it ends where it was left rather than coasting. +const dragThread = async (distance: number) => { + const list = await boundsOf(T.CHAT_MESSAGE_LIST) + const midY = Math.round(list.y + list.height / 2) + await browser + .action('pointer') + .move({x: 200, y: midY}) + .down() + .pause(100) + .move({duration: 400, x: 200, y: midY + distance}) + .pause(100) + .up() + .perform() +} + +// Each test starts from the tab root: the suite returns there between tests, so a flow cannot +// assume the conversation another one left open. +const openFirstConversation = async (): Promise => { + await escapeToTabs() + await tab('Teams').click() + await tab('Chat').click() + await waitForTestID(T.CHAT_INBOX_LIST, 5000) + + if (!(await anyExist(T.CHAT_INBOX_ROW))) return false + await els(T.CHAT_INBOX_ROW)[0]!.click() + await waitForTestID(T.CHAT_MESSAGE_LIST, 5000) + return true +} + +describe('chat thread search', () => { + it('keeps every hit it lands on visible, including wrapping around', async () => { + if (!(await openFirstConversation())) return + + await runSearch(QUERY, WRAPPING_STEPS) + await closeThreadSearch() + }) + + it('lands on a hit that is already on screen', async () => { + if (!(await openFirstConversation())) return + await runSearch(SAME_SCREEN_QUERY, 1) + await closeThreadSearch() + }) + + it('leaves the thread where the user drags it after a hit', async () => { + if (!(await openFirstConversation())) return + await runSearch(SAME_SCREEN_QUERY, 0) + + // A moment after landing is when the list is still measuring, and where anything holding the + // scroll target used to pull the thread back out from under the user. + await browser.pause(1000) + const beforeDrag = await boundsOf(T.CHAT_SEARCH_HIT) + // Several drags toward older messages, which is what asks the thread to page more in. The + // prepend that follows shifts every row's index, and that is what used to re-centre the list + // out from under the reader. + for (let drag = 0; drag < 3; drag++) { + await dragThread(260) + await browser.pause(250) + } + await browser.pause(300) + const afterDrag = await boundsOf(T.CHAT_SEARCH_HIT) + + // The drag has to have actually moved the thread, or the rest of this proves nothing. + expect(Math.abs(afterDrag.y - beforeDrag.y)).toBeGreaterThan(40) + + await browser.pause(1500) + const settled = await boundsOf(T.CHAT_SEARCH_HIT) + if (Math.abs(settled.y - afterDrag.y) > 30) { + console.log(`thread snapped back: row was at ${afterDrag.y} after the drag, ${settled.y} a moment later`) + } + expect(Math.abs(settled.y - afterDrag.y)).toBeLessThanOrEqual(30) + + await closeThreadSearch() + }) +}) diff --git a/shared/tests/e2e/shared/test-ids.ts b/shared/tests/e2e/shared/test-ids.ts index bafe3dbeb78f..fa39d7fd8cfd 100644 --- a/shared/tests/e2e/shared/test-ids.ts +++ b/shared/tests/e2e/shared/test-ids.ts @@ -31,6 +31,13 @@ export const CHAT_INFO_PANEL_SETTINGS_TAB = 'chat-info-panel-settings-tab' // Android only: iOS 26 folds Search/Info into one native "More" header menu, // but the Android header keeps the plain info icon — icons have no tappable text export const CHAT_HEADER_INFO_BUTTON = 'chat-header-info-button' +export const CHAT_HEADER_SEARCH_BUTTON = 'chat-header-search-button' +export const CHAT_THREAD_SEARCH_CANCEL = 'chat-thread-search-cancel' +export const CHAT_THREAD_SEARCH_PREV = 'chat-thread-search-prev' +export const CHAT_THREAD_SEARCH_NEXT = 'chat-thread-search-next' +// The message a thread search is currently sitting on. Present only while that row is highlighted, +// so asserting it exists is asserting the hit is on screen. +export const CHAT_SEARCH_HIT = 'chat-search-hit' // Files export const FILES_BROWSER = 'files-browser' From 103105f55cd546888713bafaa60cd13646ac7c1b Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 11 Aug 2026 18:11:58 -0400 Subject: [PATCH 08/38] test(e2e): make the drag-after-hit case provoke a real page-in The case is "search, wait, then scroll away" - the failure people actually reported. It only means anything if the thread loads another page while the reader is dragging, since the prepend that follows is what used to re-centre the list out from under them. The previous version dragged a fixed distance and asserted the row had not moved much, which survived every mutation I tried. Drive it instead: fling back through the thread until the top-of-thread marker jumps thousands of pixels away, which is a prepend and nothing else, then assert the hit has not come back on screen. If no page-in happens within the fling budget, fail rather than pass - the case was never exercised, and saying so is worth more than a green tick. Verified by mutation, against a freshly bundled app: restoring the index-shift re-centre in list-area fails this case three times over with "the hit never left the viewport", and it passes without it. Also updates the legend-list patch to the reworked fork branch: the padding side for horizontal lists, a bound on how far a scroll may reach past the committed content, a data change no longer counted as a measurement, and web user-interaction detection that follows movement rather than contact. --- shared/patches/@legendapp+list+3.3.5.patch | 10356 +++++++--------- .../ios-appium/flows/chat-search-hit.test.ts | 106 +- 2 files changed, 4333 insertions(+), 6129 deletions(-) diff --git a/shared/patches/@legendapp+list+3.3.5.patch b/shared/patches/@legendapp+list+3.3.5.patch index b000e0b9e9bc..3ac6233dd328 100644 --- a/shared/patches/@legendapp+list+3.3.5.patch +++ b/shared/patches/@legendapp+list+3.3.5.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js -index b3c5a30..923b748 100644 +index b3c5a30..1fb0c56 100644 --- a/node_modules/@legendapp/list/react-native.js +++ b/node_modules/@legendapp/list/react-native.js @@ -457,611 +457,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -1205,16 +1205,21 @@ index b3c5a30..923b748 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2276,6 +2104,309 @@ function scrollTo(ctx, params) { +@@ -2276,6 +2104,316 @@ function scrollTo(ctx, params) { } } +// src/core/scrollTargetSettle.ts -+var SETTLE_POSITION_EPSILON = 0.5; ++var SETTLE_POSITION_EPSILON = 1; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; +var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; ++function releaseScrollTargetForUserInteraction(state) { ++ if (state.scrollTargetSettle) { ++ clearScrollTargetSettle(state); ++ } ++} +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; + state.scheduledWork.cancel("scrollTargetSettle"); @@ -1229,6 +1234,7 @@ index b3c5a30..923b748 100644 + clearScrollTargetSettle(state); + return; + } ++ clearScrollTargetSettle(state); + const now = Date.now(); + state.scrollTargetSettle = { + corrections: 0, @@ -1289,6 +1295,7 @@ index b3c5a30..923b748 100644 + const measured = options == null ? void 0 : options.minIndexSizeChanged; + if (measured !== void 0) { + settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); ++ settle.expiresAt = Date.now() + SETTLE_TTL_MS; + } + if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { @@ -1515,15 +1522,15 @@ index b3c5a30..923b748 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4320,6 +4451,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4320,6 +4458,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); -+ const minIndexSizeChangedThisPass = dataChanged ? 0 : minIndexSizeChanged; ++ const minIndexSizeChangedThisPass = minIndexSizeChanged; if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4337,8 +4469,17 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4337,8 +4476,17 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -1543,16 +1550,16 @@ index b3c5a30..923b748 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -7652,6 +7793,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7652,6 +7800,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); -+ clearScrollTargetSettle(state); ++ releaseScrollTargetForUserInteraction(state); (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs -index 40e87cd..b47d103 100644 +index 40e87cd..64bfde3 100644 --- a/node_modules/@legendapp/list/react-native.mjs +++ b/node_modules/@legendapp/list/react-native.mjs @@ -436,611 +436,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -2758,16 +2765,21 @@ index 40e87cd..b47d103 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2255,6 +2083,309 @@ function scrollTo(ctx, params) { +@@ -2255,6 +2083,316 @@ function scrollTo(ctx, params) { } } +// src/core/scrollTargetSettle.ts -+var SETTLE_POSITION_EPSILON = 0.5; ++var SETTLE_POSITION_EPSILON = 1; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; +var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; ++function releaseScrollTargetForUserInteraction(state) { ++ if (state.scrollTargetSettle) { ++ clearScrollTargetSettle(state); ++ } ++} +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; + state.scheduledWork.cancel("scrollTargetSettle"); @@ -2782,6 +2794,7 @@ index 40e87cd..b47d103 100644 + clearScrollTargetSettle(state); + return; + } ++ clearScrollTargetSettle(state); + const now = Date.now(); + state.scrollTargetSettle = { + corrections: 0, @@ -2842,6 +2855,7 @@ index 40e87cd..b47d103 100644 + const measured = options == null ? void 0 : options.minIndexSizeChanged; + if (measured !== void 0) { + settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); ++ settle.expiresAt = Date.now() + SETTLE_TTL_MS; + } + if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { @@ -3068,15 +3082,15 @@ index 40e87cd..b47d103 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4299,6 +4430,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4299,6 +4437,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); -+ const minIndexSizeChangedThisPass = dataChanged ? 0 : minIndexSizeChanged; ++ const minIndexSizeChangedThisPass = minIndexSizeChanged; if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4316,8 +4448,17 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4316,8 +4455,17 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -3096,19 +3110,19 @@ index 40e87cd..b47d103 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -7631,6 +7772,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7631,6 +7779,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); -+ clearScrollTargetSettle(state); ++ releaseScrollTargetForUserInteraction(state); (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js -index 914d2da..c3ed202 100644 +index 914d2da..f03ba1e 100644 --- a/node_modules/@legendapp/list/react-native.web.js +++ b/node_modules/@legendapp/list/react-native.web.js -@@ -434,178 +434,266 @@ var EDGE_POSITION_EPSILON = 1; +@@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; var ENABLE_DEVMODE = IS_DEV && false; var ENABLE_DEBUG_VIEW = IS_DEV && false; @@ -3118,56 +3132,22 @@ index 914d2da..c3ed202 100644 - scheduledWork.cancel("checkFinishedScrollRetryFrame"); - scheduledWork.cancel("checkFinishedScrollFallback"); - scheduledWork.cancel("platformScrollCompletion"); -+// src/core/getStartOffsetAdjustment.ts -+function getStartOffsetAdjustment(ctx) { -+ const { state } = ctx; -+ const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; -+ return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); - } +-} -function settlePendingImperativeScroll(state) { - var _a3, _b; - const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; - state.pendingScrollResolve = void 0; - state.pendingScrollToEnd = void 0; - resolvePendingScroll == null ? void 0 : resolvePendingScroll(); -+ -+// src/utils/getId.ts -+function getId(state, index) { -+ const { data, keyExtractor } = state.props; -+ if (!data) { -+ return ""; -+ } -+ const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; -+ const id = ret; -+ state.idCache[index] = id; -+ return id; - } +-} -function cancelImperativeScroll(state) { - cancelScrollCompletionChecks(state); - state.scheduledWork.cancel("imperativeScrollReady"); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; - settlePendingImperativeScroll(state); -+ -+// src/core/updateContentMetricsState.ts -+function getRawContentLength(ctx) { -+ var _a3, _b, _c; -+ const { state, values } = ctx; -+ return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); -+} -+function getAlignItemsAtEndPadding(ctx) { -+ const { state } = ctx; -+ const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; -+ return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; -+} -+function updateContentMetricsState(ctx) { -+ const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; -+ const nextPadding = getAlignItemsAtEndPadding(ctx); -+ if (previousPadding !== nextPadding) { -+ set$(ctx, "alignItemsAtEndPadding", nextPadding); -+ } - } - +-} +- -// src/core/deferredPublicOnScroll.ts -function withResolvedContentOffset(state, event, resolvedOffset) { - return { @@ -3175,40 +3155,12 @@ index 914d2da..c3ed202 100644 - nativeEvent: { - ...event.nativeEvent, - contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -+// src/core/addTotalSize.ts -+function addTotalSize(ctx, key, add, notifyTotalSize = true) { -+ const state = ctx.state; -+ const prevTotalSize = state.totalSize; -+ let totalSize = state.totalSize; -+ if (key === null) { -+ totalSize = add; -+ if (state.timeoutSetPaddingTop) { -+ clearTimeout(state.timeoutSetPaddingTop); -+ state.timeoutSetPaddingTop = void 0; - } +- } - }; -+ } else { -+ totalSize += add; -+ } -+ if (prevTotalSize !== totalSize) { -+ { -+ state.pendingTotalSize = void 0; -+ state.totalSize = totalSize; -+ if (notifyTotalSize) { -+ set$(ctx, "totalSize", totalSize); -+ } -+ updateContentMetricsState(ctx); -+ } -+ } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { -+ set$(ctx, "totalSize", totalSize); -+ } - } +-} -function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { - var _a3, _b, _c, _d; -+ -+// src/core/setSize.ts -+function setSize(ctx, itemKey, size, notifyTotalSize = true) { - const state = ctx.state; +- const state = ctx.state; - const deferredEvent = state.deferredPublicOnScrollEvent; - state.deferredPublicOnScrollEvent = void 0; - if (deferredEvent) { @@ -3220,15 +3172,9 @@ index 914d2da..c3ed202 100644 - (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 - ) - ); -+ const { sizes } = state; -+ const previousSize = sizes.get(itemKey); -+ const diff = previousSize !== void 0 ? size - previousSize : size; -+ if (diff !== 0) { -+ addTotalSize(ctx, itemKey, diff, notifyTotalSize); - } -+ sizes.set(itemKey, size); - } - +- } +-} +- -// src/core/initialScrollSession.ts -var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -function hasInitialScrollSessionCompletion(completion) { @@ -3237,10 +3183,7 @@ index 914d2da..c3ed202 100644 -function clearInitialScrollSession(state) { - state.initialScrollSession = void 0; - return void 0; -+// src/utils/helpers.ts -+function isFunction(obj) { -+ return typeof obj === "function"; - } +-} -function createInitialScrollSession(options) { - const { bootstrap, completion, kind, previousDataLength } = options; - return kind === "offset" ? { @@ -3253,9 +3196,7 @@ index 914d2da..c3ed202 100644 - kind, - previousDataLength - }; -+function isArray(obj) { -+ return Array.isArray(obj); - } +-} -function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { - var _a4, _b2; - if (!state.initialScrollSession) { @@ -3271,15 +3212,10 @@ index 914d2da..c3ed202 100644 - kind, - previousDataLength: state.initialScrollSession.previousDataLength - }); -+var warned = /* @__PURE__ */ new Set(); -+function warnDevOnce(id, text) { -+ if (IS_DEV && !warned.has(id)) { -+ warned.add(id); -+ console.warn(`[legend-list] ${text}`); - } +- } - (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; - return state.initialScrollSession.completion; - } +-} -var initialScrollCompletion = { - didDispatchNativeScroll(state) { - var _a3, _b; @@ -3302,26 +3238,7 @@ index 914d2da..c3ed202 100644 - const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); - completion.didDispatchNativeScroll = void 0; - completion.didRetrySilentInitialScroll = void 0; -+function roundSize(size) { -+ return Math.floor(size * 8) / 8; -+} -+function isNullOrUndefined(value) { -+ return value === null || value === void 0; -+} -+function getPadding(s, type) { -+ var _a3, _b, _c; -+ const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; -+ return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; -+} -+function extractPadding(style, contentContainerStyle, type) { -+ return getPadding(style, type) + getPadding(contentContainerStyle, type); -+} -+function findContainerId(ctx, key) { -+ var _a3, _b; -+ const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); -+ if (directMatch !== void 0) { -+ return directMatch; - } +- } -}; -var initialScrollWatchdog = { - clear(state) { @@ -3345,12 +3262,7 @@ index 914d2da..c3ed202 100644 - var _a3, _b; - if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { - return; -+ const numContainers = peek$(ctx, "numContainers"); -+ for (let i = 0; i < numContainers; i++) { -+ const itemKey = peek$(ctx, `containerItemKey${i}`); -+ if (itemKey === key) { -+ return i; - } +- } - const completion = ensureInitialScrollSessionCompletion(state); - completion.watchdog = watchdog ? { - startScroll: watchdog.startScroll, @@ -3370,7 +3282,7 @@ index 914d2da..c3ed202 100644 - } - if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { - return clearInitialScrollSession(state); - } +- } - const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; - state.initialScrollSession = createInitialScrollSession({ - bootstrap, @@ -3379,204 +3291,264 @@ index 914d2da..c3ed202 100644 - previousDataLength - }); - return state.initialScrollSession; -+ return -1; - } - +-} +- -// src/utils/checkThreshold.ts -var HYSTERESIS_MULTIPLIER = 1.3; -function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { - const absDistance = Math.abs(distance); - return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; -+// src/utils/getItemSize.ts -+function getKnownOrFixedSize(ctx, key, index, data, resolved) { -+ var _a3, _b; -+ const state = ctx.state; -+ const { getFixedItemSize, getItemType } = state.props; -+ let size = key ? state.sizesKnown.get(key) : void 0; -+ if (size === void 0 && key && getFixedItemSize) { -+ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; -+ const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); -+ if (fixedSize !== void 0) { -+ size = fixedSize + ctx.scrollAxisGap; -+ state.sizesKnown.set(key, size); -+ } -+ } -+ return size; -+} -+function getKnownOrFixedItemSize(ctx, index) { -+ const key = getId(ctx.state, index); -+ return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); -+} -+function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { -+ for (let index = startIndex; index <= endIndex; index++) { -+ if (getKnownOrFixedItemSize(ctx, index) === void 0) { -+ return false; -+ } -+ } -+ return true; -+} -+function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { -+ var _a3, _b, _c, _d; -+ const state = ctx.state; -+ const { -+ sizes, -+ averageSizes, -+ props: { estimatedItemSize, getItemType }, -+ scrollingTo -+ } = state; -+ const sizeKnown = state.sizesKnown.get(key); -+ if (sizeKnown !== void 0) { -+ return sizeKnown; -+ } -+ let size; -+ const renderedSize = sizes.get(key); -+ if (preferCachedSize) { -+ if (renderedSize !== void 0) { -+ return renderedSize; -+ } -+ } -+ size = getKnownOrFixedSize(ctx, key, index, data, resolved); -+ if (size !== void 0) { -+ setSize(ctx, key, size, notifyTotalSize); -+ return size; -+ } -+ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; -+ if (useAverageSize && !scrollingTo) { -+ const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; -+ if (averageSizeForType !== void 0) { -+ size = roundSize(averageSizeForType); -+ } -+ } -+ if (size === void 0 && renderedSize !== void 0) { -+ return renderedSize; -+ } -+ if (size === void 0 && useAverageSize && scrollingTo) { -+ const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; -+ if (averageSizeForType !== void 0) { -+ size = roundSize(averageSizeForType); -+ } -+ } -+ if (size === void 0) { -+ size = estimatedItemSize + ctx.scrollAxisGap; -+ } -+ setSize(ctx, key, size, notifyTotalSize); -+ return size; -+} -+function getItemSizeAtIndex(ctx, index) { -+ if (index === void 0 || index < 0) { -+ return void 0; -+ } -+ const targetId = getId(ctx.state, index); -+ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+} -+ -+// src/core/calculateOffsetWithOffsetPosition.ts -+function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -+ var _a3; -+ const state = ctx.state; -+ const { index, viewOffset, viewPosition } = params; -+ let offset = offsetParam; -+ if (viewOffset) { -+ offset -= viewOffset; -+ } -+ if (index !== void 0) { -+ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -+ if (startOffsetAdjustment) { -+ offset += startOffsetAdjustment; -+ } -+ } -+ if (viewPosition !== void 0 && index !== void 0) { -+ const dataLength = state.props.data.length; -+ if (dataLength === 0) { -+ return offset; -+ } -+ const isOutOfBounds = index < 0 || index >= dataLength; -+ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -+ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -+ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -+ const trailingInset = getContentInsetEnd(ctx); -+ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -+ if (!isOutOfBounds && index === state.props.data.length - 1) { -+ const footerSize = peek$(ctx, "footerSize") || 0; -+ offset += footerSize; -+ } -+ } -+ return offset; -+} -+ -+// src/core/clampScrollOffset.ts -+function clampScrollOffset(ctx, offset, scrollTarget) { -+ const state = ctx.state; -+ const contentSize = getContentSize(ctx); -+ let clampedOffset = offset; -+ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { -+ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); -+ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; -+ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; -+ const maxOffset = baseMaxOffset + extraEndOffset; -+ clampedOffset = Math.min(offset, maxOffset); -+ } -+ clampedOffset = Math.max(0, clampedOffset); -+ return clampedOffset; -+} -+ -+// src/utils/checkThreshold.ts -+var HYSTERESIS_MULTIPLIER = 1.3; -+function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { -+ const absDistance = Math.abs(distance); -+ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; - } - var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { - const absDistance = Math.abs(distance); -@@ -815,642 +903,346 @@ function recalculateSettledScroll(ctx) { - } - checkThresholds(ctx); - } --var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; --var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; --var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; --function clearAdaptiveRenderExitTimeout(ctx) { -- ctx.state.scheduledWork.cancel("adaptiveRender"); -} --function scheduleAdaptiveRenderExit(ctx, exitDelay) { +-var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { +- const absDistance = Math.abs(distance); +- const within = atThreshold || threshold > 0 && absDistance <= threshold; +- const updateSnapshot = () => { +- setSnapshot({ +- atThreshold, +- contentSize: context.contentSize, +- dataLength: context.dataLength, +- scrollPosition: context.scrollPosition +- }); +- }; +- if (!wasReached) { +- if (!within) { +- return false; +- } +- onReached(distance); +- updateSnapshot(); +- return true; +- } +- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); +- if (reset) { +- setSnapshot(void 0); +- return false; +- } +- if (within) { +- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; +- if (changed) { +- updateSnapshot(); +- } +- } +- return true; +-}; +- +-// src/utils/edgeReachedGate.ts +-function resetEdgeLatch(ctx, edge) { - const state = ctx.state; -- clearAdaptiveRenderExitTimeout(ctx); -- if (exitDelay <= 0) { -- setAdaptiveRender(ctx, "normal", "scroll"); +- if (edge === "start") { +- state.isStartReached = false; +- state.startReachedSnapshot = void 0; - } else { -- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); +- state.isEndReached = false; +- state.endReachedSnapshot = void 0; - } -} --function setAdaptiveRender(ctx, mode, reason) { -- var _a3, _b; -- const previousMode = peek$(ctx, "adaptiveRender"); -- if (previousMode !== mode) { -- set$(ctx, "adaptiveRender", mode); -- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); +-function resetSharedEdgeGateIfOutsideHysteresis(ctx) { +- const state = ctx.state; +- if (!state.edgeReachedGate) { +- return; +- } +- const contentSize = getContentSize(ctx); +- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); +- const isContentLess = contentSize < state.scrollLength; +- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; +- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; +- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); +- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); +- if (isOutsideStart && isOutsideEnd) { +- state.edgeReachedGate = void 0; - } -} --function resetAdaptiveRender(ctx) { -+ -+// src/core/finishScrollTo.ts -+function finishScrollTo(ctx) { - var _a3, _b; -- clearAdaptiveRenderExitTimeout(ctx); -- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; -- if (peek$(ctx, "adaptiveRender") !== mode) { -- setAdaptiveRender(ctx, mode, "initial"); +-function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { +- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; +-} +-function markReachedEdge(ctx) { +- ctx.state.edgeReachedGate = "closed"; +-} +-function prepareReachedEdgeForNextUserScroll(ctx) { +- if (ctx.state.edgeReachedGate) { +- ctx.state.edgeReachedGate = "prepared"; - } -} --function updateAdaptiveRender(ctx, scrollVelocity, options) { -- var _a3, _b, _c; - const state = ctx.state; -- const adaptiveRender = state.props.adaptiveRender; -- const currentMode = peek$(ctx, "adaptiveRender"); -- if (peek$(ctx, "readyToRender")) { -- if (adaptiveRender) { -- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; -- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; -- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; -- const threshold = currentMode === "light" ? exitVelocity : enterVelocity; +-function beginReachedEdgeUserScroll(ctx, scrollDelta) { +- const state = ctx.state; +- if (state.edgeReachedGate !== "prepared") { +- return void 0; +- } +- const allowedEdge = scrollDelta < 0 ? "start" : "end"; +- state.edgeReachedGate = "closed"; +- resetEdgeLatch(ctx, allowedEdge); +- return allowedEdge; +-} +- +-// src/utils/hasActiveInitialScroll.ts +-function hasActiveInitialScroll(state) { +- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; +-} +- +-// src/utils/checkAtBottom.ts +-function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { +- var _a3; +- const state = ctx.state; +- if (!state) { +- return; +- } +- const { +- queuedInitialLayout, +- scrollLength, +- scroll, +- maintainingScrollAtEnd, +- props: { maintainScrollAtEndThreshold, onEndReachedThreshold } +- } = state; +- const contentSize = getContentSize(ctx); +- resetSharedEdgeGateIfOutsideHysteresis(ctx); +- if (contentSize > 0 && queuedInitialLayout) { +- const insetEnd = getContentInsetEnd(ctx); +- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; +- const isContentLess = contentSize < scrollLength; +- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); +- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); +- set$( +- ctx, +- "isWithinMaintainScrollAtEndThreshold", +- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength +- ); +- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; +- if (!shouldSkipThresholdChecks) { +- state.isEndReached = checkThreshold( +- distanceFromEnd, +- isContentLess, +- onEndReachedThreshold * scrollLength, +- state.isEndReached, +- state.endReachedSnapshot, +- { +- contentSize, +- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, +- scrollPosition: scroll +- }, +- (distance) => { +- var _a4, _b; +- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { +- markReachedEdge(ctx); +- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); +- } +- }, +- (snapshot) => { +- state.endReachedSnapshot = snapshot; +- } +- ); +- } +- } +-} +- +-// src/utils/checkAtTop.ts +-function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { +- const state = ctx == null ? void 0 : ctx.state; +- if (!state) { +- return; +- } +- const { +- isStartReached, +- props: { data, onStartReachedThreshold }, +- scroll, +- scrollLength, +- startReachedSnapshot, +- totalSize +- } = state; +- const dataLength = data.length; +- const threshold = onStartReachedThreshold * scrollLength; +- resetSharedEdgeGateIfOutsideHysteresis(ctx); +- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { +- state.isStartReached = false; +- state.startReachedSnapshot = void 0; +- } +- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); +- set$(ctx, "isNearStart", scroll <= threshold); +- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; +- if (!shouldSkipThresholdChecks) { +- state.isStartReached = checkThreshold( +- scroll, +- false, +- threshold, +- state.isStartReached, +- startReachedSnapshot, +- { +- contentSize: totalSize, +- dataLength, +- scrollPosition: scroll +- }, +- (distance) => { +- var _a3, _b; +- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { +- markReachedEdge(ctx); +- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); +- } +- }, +- (snapshot) => { +- state.startReachedSnapshot = snapshot; +- } +- ); +- } +-} +- +-// src/utils/checkThresholds.ts +-function checkThresholds(ctx, allowedEdge) { +- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; +- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); +- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); +-} +- +-// src/core/recalculateSettledScroll.ts +-function recalculateSettledScroll(ctx) { +- var _a3, _b; +- const state = ctx.state; +- if ((_a3 = state.props) == null ? void 0 : _a3.data) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); +- } +- checkThresholds(ctx); +-} +-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; +-function clearAdaptiveRenderExitTimeout(ctx) { +- ctx.state.scheduledWork.cancel("adaptiveRender"); +-} +-function scheduleAdaptiveRenderExit(ctx, exitDelay) { +- const state = ctx.state; +- clearAdaptiveRenderExitTimeout(ctx); +- if (exitDelay <= 0) { +- setAdaptiveRender(ctx, "normal", "scroll"); +- } else { +- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); +- } +-} +-function setAdaptiveRender(ctx, mode, reason) { +- var _a3, _b; +- const previousMode = peek$(ctx, "adaptiveRender"); +- if (previousMode !== mode) { +- set$(ctx, "adaptiveRender", mode); +- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); +- } +-} +-function resetAdaptiveRender(ctx) { +- var _a3, _b; +- clearAdaptiveRenderExitTimeout(ctx); +- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; +- if (peek$(ctx, "adaptiveRender") !== mode) { +- setAdaptiveRender(ctx, mode, "initial"); +- } +-} +-function updateAdaptiveRender(ctx, scrollVelocity, options) { +- var _a3, _b, _c; +- const state = ctx.state; +- const adaptiveRender = state.props.adaptiveRender; +- const currentMode = peek$(ctx, "adaptiveRender"); +- if (peek$(ctx, "readyToRender")) { +- if (adaptiveRender) { +- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; +- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; +- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; +- const threshold = currentMode === "light" ? exitVelocity : enterVelocity; - const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; - const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; - if (nextMode !== previousMode) { @@ -3589,40 +3561,10 @@ index 914d2da..c3ed202 100644 - } - } else { - resetAdaptiveRender(ctx); -+ if (state == null ? void 0 : state.scrollingTo) { -+ cancelScrollCompletionChecks(state); -+ const resolvePendingScroll = state.pendingScrollResolve; -+ state.pendingScrollResolve = void 0; -+ const scrollingTo = state.scrollingTo; -+ state.scrollHistory.length = 0; -+ state.scrollingTo = void 0; -+ state.scrollTargetPinnedRange = void 0; -+ if (state.pendingTotalSize !== void 0) { -+ addTotalSize(ctx, null, state.pendingTotalSize); -+ } -+ { -+ state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); -+ } -+ if (scrollingTo.isInitialScroll || state.initialScroll) { -+ const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; -+ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; -+ finishInitialScroll(ctx, { -+ onFinished: () => { -+ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); -+ }, -+ preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, -+ recalculateItems: true, -+ schedulePreservedTargetClear: shouldPreserveResizeTarget, -+ syncObservedOffset: isOffsetSession, -+ waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame -+ }); -+ return; - } -+ recalculateSettledScroll(ctx); -+ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); - } - } - +- } +- } +-} +- -// src/utils/getEffectiveDrawDistance.ts -var INITIAL_DRAW_DISTANCE = 50; -function getEffectiveDrawDistance(ctx, mode) { @@ -3636,21 +3578,8 @@ index 914d2da..c3ed202 100644 -function scheduleFullDrawDistancePrewarm(ctx) { - const { state } = ctx; - if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { -+// src/core/doScrollTo.ts -+var SCROLL_END_IDLE_MS = 80; -+var SCROLL_END_MAX_MS = 1500; -+var SMOOTH_SCROLL_DURATION_MS = 320; -+var SCROLL_END_TARGET_EPSILON = 1; -+function doScrollTo(ctx, params) { -+ var _a3, _b; -+ const state = ctx.state; -+ const { animated, horizontal, offset } = params; -+ state.scheduledWork.cancel("platformScrollCompletion"); -+ const scroller = state.refScroller.current; -+ const node = scroller == null ? void 0 : scroller.getScrollableNode(); -+ if (!scroller || !node) { - return; - } +- return; +- } - state.scheduledWork.frame(() => { - var _a3; - return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); @@ -3669,35 +3598,10 @@ index 914d2da..c3ed202 100644 - } - if (resetInitialScroll) { - state.didFinishInitialScroll = false; -+ const isAnimated = !!animated; -+ const isHorizontal = !!horizontal; -+ const contentSize = isHorizontal ? getContentSize(ctx) : void 0; -+ const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; -+ const top = isHorizontal ? 0 : offset; -+ scroller.scrollTo({ animated: isAnimated, x: left, y: top }); -+ if (isAnimated) { -+ const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; -+ listenForScrollEnd(ctx, { -+ readOffset: () => scroller.getCurrentScrollOffset(), -+ target, -+ targetOffset: offset -+ }); -+ } else { -+ state.scroll = offset; -+ const targetToken = state.scrollingTo; -+ state.scheduledWork.timeout( -+ () => { -+ if (targetToken === state.scrollingTo) { -+ finishScrollTo(ctx); -+ } -+ }, -+ 100, -+ "platformScrollCompletion" -+ ); - } +- } - set$(ctx, "readyToRender", false); - resetAdaptiveRender(ctx); - } +-} -function setInitialRenderState(ctx, { - didLayout, - didInitialScroll @@ -3726,12 +3630,7 @@ index 914d2da..c3ed202 100644 - onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); - } - } -+function listenForScrollEnd(ctx, params) { -+ const { readOffset, target, targetOffset } = params; -+ if (!target) { -+ finishScrollTo(ctx); -+ return; - } +- } -} - -// src/core/finishInitialScroll.ts @@ -3757,23 +3656,12 @@ index 914d2da..c3ed202 100644 - if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { - if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { - cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -+ const supportsScrollEnd = "onscrollend" in target; -+ let idleTimeout; -+ let settled = false; -+ const { scheduledWork, scrollingTo: targetToken } = ctx.state; -+ const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); -+ const cleanup = () => { -+ target.removeEventListener("scroll", onScroll2); -+ if (supportsScrollEnd) { -+ target.removeEventListener("scrollend", onScrollEnd); - } +- } - if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { - cancelScrollCompletionChecks(state); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; -+ if (idleTimeout !== void 0) { -+ clearTimeout(idleTimeout); - } +- } - initialScrollCompletion.resetFlags(state); - setInitialScrollSession(state, { bootstrap: null }); - finishInitialScroll(ctx); @@ -3788,13 +3676,7 @@ index 914d2da..c3ed202 100644 - const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); - if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { - syncInitialScrollOffset(state, observedOffset); -+ clearTimeout(maxTimeout); -+ }; -+ const cancel = () => { -+ if (!settled) { -+ settled = true; -+ cleanup(); - } +- } - } - const complete = () => { - var _a4, _b2, _c2, _d, _e; @@ -3820,422 +3702,290 @@ index 914d2da..c3ed202 100644 - } - } else { - clearPreservedInitialScrollTarget(state); -+ }; -+ const finish = (reason) => { -+ if (settled) return; -+ if (targetToken !== ctx.state.scrollingTo) { -+ scheduledWork.cancel("platformScrollCompletion"); -+ return; - } +- } - if (options == null ? void 0 : options.recalculateItems) { - recalculateSettledScroll(ctx); -+ const currentOffset = readOffset(); -+ const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; -+ if (reason === "scrollend" && !isNearTarget) { -+ return; - } +- } - setInitialRenderState(ctx, { didInitialScroll: true }); - if (shouldReleaseDeferredPublicOnScroll) { - releaseDeferredPublicOnScroll(ctx, finalScrollOffset); -+ scheduledWork.cancel("platformScrollCompletion"); -+ finishScrollTo(ctx); -+ }; -+ const onScroll2 = () => { -+ if (idleTimeout !== void 0) { -+ clearTimeout(idleTimeout); - } +- } - (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); -+ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); - }; +- }; - if (options == null ? void 0 : options.waitForCompletionFrame) { - requestAnimationFrame(complete); - return; -+ const onScrollEnd = () => finish("scrollend"); -+ target.addEventListener("scroll", onScroll2); -+ if (supportsScrollEnd) { -+ target.addEventListener("scrollend", onScrollEnd); -+ } else { -+ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); - } +- } - complete(); -+ scheduledWork.register("platformScrollCompletion", cancel); - } - +-} +- -// src/core/calculateOffsetForIndex.ts -function calculateOffsetForIndex(ctx, index) { - const state = ctx.state; - return index !== void 0 ? state.positions[index] || 0 : 0; -+// src/core/initialScrollSession.ts -+var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -+function hasInitialScrollSessionCompletion(completion) { -+ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); - } -- --// src/core/getStartOffsetAdjustment.ts --function getStartOffsetAdjustment(ctx) { -- const { state } = ctx; -- const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; -- return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); -+function clearInitialScrollSession(state) { -+ state.initialScrollSession = void 0; -+ return void 0; - } +-} - --// src/utils/getId.ts --function getId(state, index) { -- const { data, keyExtractor } = state.props; -- if (!data) { -- return ""; -- } -- const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; -- const id = ret; -- state.idCache[index] = id; -- return id; -+function createInitialScrollSession(options) { -+ const { bootstrap, completion, kind, previousDataLength } = options; -+ return kind === "offset" ? { -+ completion, -+ kind, -+ previousDataLength -+ } : { -+ bootstrap, -+ completion, -+ kind, -+ previousDataLength -+ }; - } -- --// src/core/updateContentMetricsState.ts --function getRawContentLength(ctx) { -- var _a3, _b, _c; -- const { state, values } = ctx; -- return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); -+function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { -+ var _a4, _b2; -+ if (!state.initialScrollSession) { -+ state.initialScrollSession = createInitialScrollSession({ -+ completion: {}, -+ kind, -+ previousDataLength: 0 -+ }); -+ } else if (state.initialScrollSession.kind !== kind) { -+ state.initialScrollSession = createInitialScrollSession({ -+ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, -+ completion: state.initialScrollSession.completion, -+ kind, -+ previousDataLength: state.initialScrollSession.previousDataLength + // src/core/getStartOffsetAdjustment.ts + function getStartOffsetAdjustment(ctx) { + const { state } = ctx; +@@ -1234,58 +635,273 @@ function getItemSizeAtIndex(ctx, index) { + if (index === void 0 || index < 0) { + return void 0; + } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; ++ } ++ if (index !== void 0) { ++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); ++ if (startOffsetAdjustment) { ++ offset += startOffsetAdjustment; ++ } ++ } ++ if (viewPosition !== void 0 && index !== void 0) { ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return offset; ++ } ++ const isOutOfBounds = index < 0 || index >= dataLength; ++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; ++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); ++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); ++ const trailingInset = getContentInsetEnd(ctx); ++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); ++ if (!isOutOfBounds && index === state.props.data.length - 1) { ++ const footerSize = peek$(ctx, "footerSize") || 0; ++ offset += footerSize; ++ } ++ } ++ return offset; ++} ++ ++// src/core/clampScrollOffset.ts ++function clampScrollOffset(ctx, offset, scrollTarget) { ++ const state = ctx.state; ++ const contentSize = getContentSize(ctx); ++ let clampedOffset = offset; ++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { ++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); ++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; ++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; ++ const maxOffset = baseMaxOffset + extraEndOffset; ++ clampedOffset = Math.min(offset, maxOffset); ++ } ++ clampedOffset = Math.max(0, clampedOffset); ++ return clampedOffset; ++} ++ ++// src/utils/checkThreshold.ts ++var HYSTERESIS_MULTIPLIER = 1.3; ++function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { ++ const absDistance = Math.abs(distance); ++ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; ++} ++var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { ++ const absDistance = Math.abs(distance); ++ const within = atThreshold || threshold > 0 && absDistance <= threshold; ++ const updateSnapshot = () => { ++ setSnapshot({ ++ atThreshold, ++ contentSize: context.contentSize, ++ dataLength: context.dataLength, ++ scrollPosition: context.scrollPosition + }); ++ }; ++ if (!wasReached) { ++ if (!within) { ++ return false; ++ } ++ onReached(distance); ++ updateSnapshot(); ++ return true; + } -+ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; -+ return state.initialScrollSession.completion; - } --function getAlignItemsAtEndPadding(ctx) { -- const { state } = ctx; -- const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; -- return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; --} --function updateContentMetricsState(ctx) { -- const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; -- const nextPadding = getAlignItemsAtEndPadding(ctx); -- if (previousPadding !== nextPadding) { -- set$(ctx, "alignItemsAtEndPadding", nextPadding); -- } --} -- --// src/core/addTotalSize.ts --function addTotalSize(ctx, key, add, notifyTotalSize = true) { -- const state = ctx.state; -- const prevTotalSize = state.totalSize; -- let totalSize = state.totalSize; -- if (key === null) { -- totalSize = add; -- if (state.timeoutSetPaddingTop) { -- clearTimeout(state.timeoutSetPaddingTop); -- state.timeoutSetPaddingTop = void 0; -+var initialScrollCompletion = { -+ didDispatchNativeScroll(state) { -+ var _a3, _b; -+ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); -+ }, -+ didRetrySilentInitialScroll(state) { -+ var _a3, _b; -+ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); -+ }, -+ markInitialScrollNativeDispatch(state) { -+ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; -+ }, -+ markSilentInitialScrollRetry(state) { -+ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; -+ }, -+ resetFlags(state) { -+ if (!state.initialScrollSession) { -+ return; - } -- } else { -- totalSize += add; -+ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); -+ completion.didDispatchNativeScroll = void 0; -+ completion.didRetrySilentInitialScroll = void 0; - } -- if (prevTotalSize !== totalSize) { -- { -- state.pendingTotalSize = void 0; -- state.totalSize = totalSize; -- if (notifyTotalSize) { -- set$(ctx, "totalSize", totalSize); -- } -- updateContentMetricsState(ctx); -+}; -+var initialScrollWatchdog = { -+ clear(state) { -+ initialScrollWatchdog.set(state, void 0); -+ }, -+ didReachTarget(newScroll, watchdog) { -+ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); -+ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ get(state) { -+ var _a3, _b; -+ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; -+ }, -+ hasNonZeroTargetOffset(targetOffset) { -+ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ isAtZeroTargetOffset(targetOffset) { -+ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ set(state, watchdog) { -+ var _a3, _b; -+ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { -+ return; - } -- } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { -- set$(ctx, "totalSize", totalSize); -+ const completion = ensureInitialScrollSessionCompletion(state); -+ completion.watchdog = watchdog ? { -+ startScroll: watchdog.startScroll, -+ targetOffset: watchdog.targetOffset -+ } : void 0; - } --} -- --// src/core/setSize.ts --function setSize(ctx, itemKey, size, notifyTotalSize = true) { -- const state = ctx.state; -- const { sizes } = state; -- const previousSize = sizes.get(itemKey); -- const diff = previousSize !== void 0 ? size - previousSize : size; -- if (diff !== 0) { -- addTotalSize(ctx, itemKey, diff, notifyTotalSize); ++ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); ++ if (reset) { ++ setSnapshot(void 0); ++ return false; ++ } ++ if (within) { ++ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; ++ if (changed) { ++ updateSnapshot(); ++ } ++ } ++ return true; +}; -+function setInitialScrollSession(state, options = {}) { -+ var _a3, _b, _c, _d; -+ const existingSession = state.initialScrollSession; -+ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; -+ const completion = existingSession == null ? void 0 : existingSession.completion; -+ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; -+ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; -+ if (!kind) { -+ return clearInitialScrollSession(state); - } -- sizes.set(itemKey, size); --} -- --// src/utils/helpers.ts --function isFunction(obj) { -- return typeof obj === "function"; --} --function isArray(obj) { -- return Array.isArray(obj); --} --var warned = /* @__PURE__ */ new Set(); --function warnDevOnce(id, text) { -- if (IS_DEV && !warned.has(id)) { -- warned.add(id); -- console.warn(`[legend-list] ${text}`); -+ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { -+ return clearInitialScrollSession(state); - } -+ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; -+ state.initialScrollSession = createInitialScrollSession({ -+ bootstrap, -+ completion, -+ kind, -+ previousDataLength -+ }); -+ return state.initialScrollSession; - } --function roundSize(size) { -- return Math.floor(size * 8) / 8; --} --function isNullOrUndefined(value) { -- return value === null || value === void 0; --} --function getPadding(s, type) { -- var _a3, _b, _c; -- const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; -- return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; -+var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; -+var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; -+var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; -+function clearAdaptiveRenderExitTimeout(ctx) { -+ ctx.state.scheduledWork.cancel("adaptiveRender"); - } --function extractPadding(style, contentContainerStyle, type) { -- return getPadding(style, type) + getPadding(contentContainerStyle, type); -+function scheduleAdaptiveRenderExit(ctx, exitDelay) { ++ ++// src/utils/edgeReachedGate.ts ++function resetEdgeLatch(ctx, edge) { + const state = ctx.state; -+ clearAdaptiveRenderExitTimeout(ctx); -+ if (exitDelay <= 0) { -+ setAdaptiveRender(ctx, "normal", "scroll"); ++ if (edge === "start") { ++ state.isStartReached = false; ++ state.startReachedSnapshot = void 0; + } else { -+ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); ++ state.isEndReached = false; ++ state.endReachedSnapshot = void 0; + } - } --function findContainerId(ctx, key) { -+function setAdaptiveRender(ctx, mode, reason) { - var _a3, _b; -- const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); -- if (directMatch !== void 0) { -- return directMatch; -- } -- const numContainers = peek$(ctx, "numContainers"); -- for (let i = 0; i < numContainers; i++) { -- const itemKey = peek$(ctx, `containerItemKey${i}`); -- if (itemKey === key) { -- return i; -- } -+ const previousMode = peek$(ctx, "adaptiveRender"); -+ if (previousMode !== mode) { -+ set$(ctx, "adaptiveRender", mode); -+ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); - } -- return -1; - } -- --// src/utils/getItemSize.ts --function getKnownOrFixedSize(ctx, key, index, data, resolved) { -+function resetAdaptiveRender(ctx) { - var _a3, _b; -- const state = ctx.state; -- const { getFixedItemSize, getItemType } = state.props; -- let size = key ? state.sizesKnown.get(key) : void 0; -- if (size === void 0 && key && getFixedItemSize) { -- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; -- const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); -- if (fixedSize !== void 0) { -- size = fixedSize + ctx.scrollAxisGap; -- state.sizesKnown.set(key, size); -- } -+ clearAdaptiveRenderExitTimeout(ctx); -+ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; -+ if (peek$(ctx, "adaptiveRender") !== mode) { -+ setAdaptiveRender(ctx, mode, "initial"); - } -- return size; --} --function getKnownOrFixedItemSize(ctx, index) { -- const key = getId(ctx.state, index); -- return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); - } --function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { -- for (let index = startIndex; index <= endIndex; index++) { -- if (getKnownOrFixedItemSize(ctx, index) === void 0) { -- return false; -+function updateAdaptiveRender(ctx, scrollVelocity, options) { -+ var _a3, _b, _c; ++} ++function resetSharedEdgeGateIfOutsideHysteresis(ctx) { + const state = ctx.state; -+ const adaptiveRender = state.props.adaptiveRender; -+ const currentMode = peek$(ctx, "adaptiveRender"); -+ if (peek$(ctx, "readyToRender")) { -+ if (adaptiveRender) { -+ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; -+ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; -+ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; -+ const threshold = currentMode === "light" ? exitVelocity : enterVelocity; -+ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; -+ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; -+ if (nextMode !== previousMode) { -+ if (nextMode === "light") { -+ setAdaptiveRender(ctx, "light", "scroll"); -+ scheduleAdaptiveRenderExit(ctx, exitDelay); -+ } else if (currentMode === "light") { -+ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ if (!state.edgeReachedGate) { ++ return; ++ } ++ const contentSize = getContentSize(ctx); ++ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); ++ const isContentLess = contentSize < state.scrollLength; ++ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; ++ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; ++ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); ++ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); ++ if (isOutsideStart && isOutsideEnd) { ++ state.edgeReachedGate = void 0; ++ } ++} ++function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { ++ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; ++} ++function markReachedEdge(ctx) { ++ ctx.state.edgeReachedGate = "closed"; ++} ++function prepareReachedEdgeForNextUserScroll(ctx) { ++ if (ctx.state.edgeReachedGate) { ++ ctx.state.edgeReachedGate = "prepared"; ++ } ++} ++function beginReachedEdgeUserScroll(ctx, scrollDelta) { ++ const state = ctx.state; ++ if (state.edgeReachedGate !== "prepared") { ++ return void 0; ++ } ++ const allowedEdge = scrollDelta < 0 ? "start" : "end"; ++ state.edgeReachedGate = "closed"; ++ resetEdgeLatch(ctx, allowedEdge); ++ return allowedEdge; ++} ++ ++// src/utils/hasActiveInitialScroll.ts ++function hasActiveInitialScroll(state) { ++ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; ++} ++ ++// src/utils/checkAtBottom.ts ++function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { ++ var _a3; ++ const state = ctx.state; ++ if (!state) { ++ return; ++ } ++ const { ++ queuedInitialLayout, ++ scrollLength, ++ scroll, ++ maintainingScrollAtEnd, ++ props: { maintainScrollAtEndThreshold, onEndReachedThreshold } ++ } = state; ++ const contentSize = getContentSize(ctx); ++ resetSharedEdgeGateIfOutsideHysteresis(ctx); ++ if (contentSize > 0 && queuedInitialLayout) { ++ const insetEnd = getContentInsetEnd(ctx); ++ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; ++ const isContentLess = contentSize < scrollLength; ++ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); ++ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); ++ set$( ++ ctx, ++ "isWithinMaintainScrollAtEndThreshold", ++ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength ++ ); ++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; ++ if (!shouldSkipThresholdChecks) { ++ state.isEndReached = checkThreshold( ++ distanceFromEnd, ++ isContentLess, ++ onEndReachedThreshold * scrollLength, ++ state.isEndReached, ++ state.endReachedSnapshot, ++ { ++ contentSize, ++ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, ++ scrollPosition: scroll ++ }, ++ (distance) => { ++ var _a4, _b; ++ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { ++ markReachedEdge(ctx); ++ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); ++ } ++ }, ++ (snapshot) => { ++ state.endReachedSnapshot = snapshot; ++ } ++ ); ++ } ++ } ++} ++ ++// src/utils/checkAtTop.ts ++function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { ++ const state = ctx == null ? void 0 : ctx.state; ++ if (!state) { ++ return; ++ } ++ const { ++ isStartReached, ++ props: { data, onStartReachedThreshold }, ++ scroll, ++ scrollLength, ++ startReachedSnapshot, ++ totalSize ++ } = state; ++ const dataLength = data.length; ++ const threshold = onStartReachedThreshold * scrollLength; ++ resetSharedEdgeGateIfOutsideHysteresis(ctx); ++ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { ++ state.isStartReached = false; ++ state.startReachedSnapshot = void 0; ++ } ++ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); ++ set$(ctx, "isNearStart", scroll <= threshold); ++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; ++ if (!shouldSkipThresholdChecks) { ++ state.isStartReached = checkThreshold( ++ scroll, ++ false, ++ threshold, ++ state.isStartReached, ++ startReachedSnapshot, ++ { ++ contentSize: totalSize, ++ dataLength, ++ scrollPosition: scroll ++ }, ++ (distance) => { ++ var _a3, _b; ++ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { ++ markReachedEdge(ctx); ++ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); + } ++ }, ++ (snapshot) => { ++ state.startReachedSnapshot = snapshot; + } -+ } else { -+ resetAdaptiveRender(ctx); - } - } -- return true; ++ ); ++ } } --function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { -- var _a3, _b, _c, _d; -+ -+// src/core/doMaintainScrollAtEnd.ts -+function doMaintainScrollAtEnd(ctx) { - const state = ctx.state; - const { -- sizes, -- averageSizes, -- props: { estimatedItemSize, getItemType }, -- scrollingTo -+ didContainersLayout, -+ pendingNativeMVCPAdjust, -+ refScroller, -+ props: { maintainScrollAtEnd } - } = state; -- const sizeKnown = state.sizesKnown.get(key); -- if (sizeKnown !== void 0) { -- return sizeKnown; -+ const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); -+ const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout); -+ if (pendingNativeMVCPAdjust) { -+ state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd; -+ return false; - } -- let size; -- const renderedSize = sizes.get(key); -- if (preferCachedSize) { -- if (renderedSize !== void 0) { -- return renderedSize; -- } -- } -- size = getKnownOrFixedSize(ctx, key, index, data, resolved); -- if (size !== void 0) { -- setSize(ctx, key, size, notifyTotalSize); -- return size; -- } -- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; -- if (useAverageSize && !scrollingTo) { -- const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; -- if (averageSizeForType !== void 0) { -- size = roundSize(averageSizeForType); -- } -- } -- if (size === void 0 && renderedSize !== void 0) { -- return renderedSize; -- } -- if (size === void 0 && useAverageSize && scrollingTo) { -- const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; -- if (averageSizeForType !== void 0) { -- size = roundSize(averageSizeForType); -- } -- } -- if (size === void 0) { -- size = estimatedItemSize + ctx.scrollAxisGap; -- } -- setSize(ctx, key, size, notifyTotalSize); -- return size; --} --function getItemSizeAtIndex(ctx, index) { -- if (index === void 0 || index < 0) { -- return void 0; -- } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); --} -- + -// src/core/calculateOffsetWithOffsetPosition.ts -function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { - var _a3; @@ -4268,11 +4018,19 @@ index 914d2da..c3ed202 100644 - } - } - return offset; --} -- ++// src/utils/checkThresholds.ts ++function checkThresholds(ctx, allowedEdge) { ++ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; ++ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); ++ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + } + -// src/core/clampScrollOffset.ts -function clampScrollOffset(ctx, offset, scrollTarget) { -- const state = ctx.state; ++// src/core/recalculateSettledScroll.ts ++function recalculateSettledScroll(ctx) { ++ var _a3, _b; + const state = ctx.state; - const contentSize = getContentSize(ctx); - let clampedOffset = offset; - if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { @@ -4281,204 +4039,211 @@ index 914d2da..c3ed202 100644 - const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; - const maxOffset = baseMaxOffset + extraEndOffset; - clampedOffset = Math.min(offset, maxOffset); -- } ++ if ((_a3 = state.props) == null ? void 0 : _a3.data) { ++ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); + } - clampedOffset = Math.max(0, clampedOffset); - return clampedOffset; --} -- --// src/core/finishScrollTo.ts --function finishScrollTo(ctx) { -- var _a3, _b; -- const state = ctx.state; -- if (state == null ? void 0 : state.scrollingTo) { -- cancelScrollCompletionChecks(state); -- const resolvePendingScroll = state.pendingScrollResolve; -- state.pendingScrollResolve = void 0; -- const scrollingTo = state.scrollingTo; -- state.scrollHistory.length = 0; -- state.scrollingTo = void 0; -- state.scrollTargetPinnedRange = void 0; -- if (state.pendingTotalSize !== void 0) { -- addTotalSize(ctx, null, state.pendingTotalSize); -- } -- { -- state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); -- } -- if (scrollingTo.isInitialScroll || state.initialScroll) { -- const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; -- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; -- finishInitialScroll(ctx, { -- onFinished: () => { -- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); -- }, -- preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, -- recalculateItems: true, -- schedulePreservedTargetClear: shouldPreserveResizeTarget, -- syncObservedOffset: isOffsetSession, -- waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame -- }); -- return; -- } -- recalculateSettledScroll(ctx); -- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); -- } --} -- --// src/core/doScrollTo.ts --var SCROLL_END_IDLE_MS = 80; --var SCROLL_END_MAX_MS = 1500; --var SMOOTH_SCROLL_DURATION_MS = 320; --var SCROLL_END_TARGET_EPSILON = 1; --function doScrollTo(ctx, params) { -- var _a3, _b; -- const state = ctx.state; -- const { animated, horizontal, offset } = params; -- state.scheduledWork.cancel("platformScrollCompletion"); -- const scroller = state.refScroller.current; -- const node = scroller == null ? void 0 : scroller.getScrollableNode(); -- if (!scroller || !node) { -- return; -- } -- const isAnimated = !!animated; -- const isHorizontal = !!horizontal; -- const contentSize = isHorizontal ? getContentSize(ctx) : void 0; -- const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; -- const top = isHorizontal ? 0 : offset; -- scroller.scrollTo({ animated: isAnimated, x: left, y: top }); -- if (isAnimated) { -- const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; -- listenForScrollEnd(ctx, { -- readOffset: () => scroller.getCurrentScrollOffset(), -- target, -- targetOffset: offset -- }); -- } else { -- state.scroll = offset; -- const targetToken = state.scrollingTo; -- state.scheduledWork.timeout( -- () => { -- if (targetToken === state.scrollingTo) { -- finishScrollTo(ctx); -- } -- }, -- 100, -- "platformScrollCompletion" -- ); -- } --} --function listenForScrollEnd(ctx, params) { -- const { readOffset, target, targetOffset } = params; -- if (!target) { -- finishScrollTo(ctx); -- return; -- } -- const supportsScrollEnd = "onscrollend" in target; -- let idleTimeout; -- let settled = false; -- const { scheduledWork, scrollingTo: targetToken } = ctx.state; -- const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); -- const cleanup = () => { -- target.removeEventListener("scroll", onScroll2); -- if (supportsScrollEnd) { -- target.removeEventListener("scrollend", onScrollEnd); -- } -- if (idleTimeout !== void 0) { -- clearTimeout(idleTimeout); -- } -- clearTimeout(maxTimeout); -- }; -- const cancel = () => { -- if (!settled) { -- settled = true; -- cleanup(); -- } -- }; -- const finish = (reason) => { -- if (settled) return; -- if (targetToken !== ctx.state.scrollingTo) { -- scheduledWork.cancel("platformScrollCompletion"); -- return; -- } -- const currentOffset = readOffset(); -- const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; -- if (reason === "scrollend" && !isNearTarget) { -- return; -- } -- scheduledWork.cancel("platformScrollCompletion"); -- finishScrollTo(ctx); -- }; -- const onScroll2 = () => { -- if (idleTimeout !== void 0) { -- clearTimeout(idleTimeout); -- } -- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); -- }; -- const onScrollEnd = () => finish("scrollend"); -- target.addEventListener("scroll", onScroll2); -- if (supportsScrollEnd) { -- target.addEventListener("scrollend", onScrollEnd); -- } else { -- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); -- } ++ checkThresholds(ctx); + } + + // src/core/finishScrollTo.ts +@@ -1422,7 +1038,182 @@ function listenForScrollEnd(ctx, params) { + } else { + idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); + } - scheduledWork.register("platformScrollCompletion", cancel); --} -- --// src/core/doMaintainScrollAtEnd.ts --function doMaintainScrollAtEnd(ctx) { -- const state = ctx.state; -- const { -- didContainersLayout, -- pendingNativeMVCPAdjust, -- refScroller, -- props: { maintainScrollAtEnd } -- } = state; -- const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); -- const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout); -- if (pendingNativeMVCPAdjust) { -- state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd; -- return false; -- } -- if (shouldMaintainScrollAtEnd) { -- state.pendingMaintainScrollAtEnd = false; -- const contentSize = getContentSize(ctx); -- if (contentSize < state.scrollLength) { -- state.scroll = 0; -+ if (shouldMaintainScrollAtEnd) { -+ state.pendingMaintainScrollAtEnd = false; -+ const contentSize = getContentSize(ctx); -+ if (contentSize < state.scrollLength) { -+ state.scroll = 0; - } - if (!state.maintainingScrollAtEnd) { - const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; - const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; ++ scheduledWork.register("platformScrollCompletion", cancel); ++} ++ ++// src/core/initialScrollSession.ts ++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; ++function hasInitialScrollSessionCompletion(completion) { ++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); ++} ++function clearInitialScrollSession(state) { ++ state.initialScrollSession = void 0; ++ return void 0; ++} ++function createInitialScrollSession(options) { ++ const { bootstrap, completion, kind, previousDataLength } = options; ++ return kind === "offset" ? { ++ completion, ++ kind, ++ previousDataLength ++ } : { ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }; ++} ++function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { ++ var _a4, _b2; ++ if (!state.initialScrollSession) { ++ state.initialScrollSession = createInitialScrollSession({ ++ completion: {}, ++ kind, ++ previousDataLength: 0 ++ }); ++ } else if (state.initialScrollSession.kind !== kind) { ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, ++ completion: state.initialScrollSession.completion, ++ kind, ++ previousDataLength: state.initialScrollSession.previousDataLength ++ }); ++ } ++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; ++ return state.initialScrollSession.completion; ++} ++var initialScrollCompletion = { ++ didDispatchNativeScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); ++ }, ++ didRetrySilentInitialScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); ++ }, ++ markInitialScrollNativeDispatch(state) { ++ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; ++ }, ++ markSilentInitialScrollRetry(state) { ++ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; ++ }, ++ resetFlags(state) { ++ if (!state.initialScrollSession) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); ++ completion.didDispatchNativeScroll = void 0; ++ completion.didRetrySilentInitialScroll = void 0; ++ } ++}; ++var initialScrollWatchdog = { ++ clear(state) { ++ initialScrollWatchdog.set(state, void 0); ++ }, ++ didReachTarget(newScroll, watchdog) { ++ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); ++ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ get(state) { ++ var _a3, _b; ++ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; ++ }, ++ hasNonZeroTargetOffset(targetOffset) { ++ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ isAtZeroTargetOffset(targetOffset) { ++ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ set(state, watchdog) { ++ var _a3, _b; ++ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state); ++ completion.watchdog = watchdog ? { ++ startScroll: watchdog.startScroll, ++ targetOffset: watchdog.targetOffset ++ } : void 0; ++ } ++}; ++function setInitialScrollSession(state, options = {}) { ++ var _a3, _b, _c, _d; ++ const existingSession = state.initialScrollSession; ++ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; ++ const completion = existingSession == null ? void 0 : existingSession.completion; ++ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; ++ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; ++ if (!kind) { ++ return clearInitialScrollSession(state); ++ } ++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { ++ return clearInitialScrollSession(state); ++ } ++ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }); ++ return state.initialScrollSession; ++} ++var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; ++function clearAdaptiveRenderExitTimeout(ctx) { ++ ctx.state.scheduledWork.cancel("adaptiveRender"); ++} ++function scheduleAdaptiveRenderExit(ctx, exitDelay) { ++ const state = ctx.state; ++ clearAdaptiveRenderExitTimeout(ctx); ++ if (exitDelay <= 0) { ++ setAdaptiveRender(ctx, "normal", "scroll"); ++ } else { ++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); ++ } ++} ++function setAdaptiveRender(ctx, mode, reason) { ++ var _a3, _b; ++ const previousMode = peek$(ctx, "adaptiveRender"); ++ if (previousMode !== mode) { ++ set$(ctx, "adaptiveRender", mode); ++ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); ++ } ++} ++function resetAdaptiveRender(ctx) { ++ var _a3, _b; ++ clearAdaptiveRenderExitTimeout(ctx); ++ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; ++ if (peek$(ctx, "adaptiveRender") !== mode) { ++ setAdaptiveRender(ctx, mode, "initial"); ++ } ++} ++function updateAdaptiveRender(ctx, scrollVelocity, options) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ const adaptiveRender = state.props.adaptiveRender; ++ const currentMode = peek$(ctx, "adaptiveRender"); ++ if (peek$(ctx, "readyToRender")) { ++ if (adaptiveRender) { ++ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; ++ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; ++ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; ++ const threshold = currentMode === "light" ? exitVelocity : enterVelocity; ++ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; ++ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; ++ if (nextMode !== previousMode) { ++ if (nextMode === "light") { ++ setAdaptiveRender(ctx, "light", "scroll"); ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } else if (currentMode === "light") { ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } ++ } ++ } else { ++ resetAdaptiveRender(ctx); ++ } ++ } + } + + // src/core/doMaintainScrollAtEnd.ts +@@ -1451,6 +1242,7 @@ function doMaintainScrollAtEnd(ctx) { + const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; state.maintainingScrollAtEnd = pendingState; + clearScrollTargetSettle(state); requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1809,23 +1601,44 @@ function prepareMVCP(ctx, dataChanged) { +@@ -1809,6 +1601,27 @@ function prepareMVCP(ctx, dataChanged) { } } --// src/utils/getScrollVelocity.ts --var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; --var SCROLL_VELOCITY_HALF_LIFE_MS = 200; --var getScrollVelocity = (state) => { -- const { scrollHistory } = state; -- const newestIndex = scrollHistory.length - 1; -- if (newestIndex < 1) { -- return 0; -- } -- const newest = scrollHistory[newestIndex]; -- if (Date.now() - newest.time > MAX_SCROLL_VELOCITY_WINDOW_MS) { -- return 0; -- } -- let direction = 0; -- let weightedVelocity = 0; -- let totalWeight = 0; -- for (let i = newestIndex; i > 0; i--) { +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { @@ -4500,297 +4265,67 @@ index 914d2da..c3ed202 100644 + }, "fullDrawDistancePrewarm"); +} + -+// src/utils/getScrollVelocity.ts -+var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; -+var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -+var getScrollVelocity = (state) => { -+ const { scrollHistory } = state; -+ const newestIndex = scrollHistory.length - 1; -+ if (newestIndex < 1) { -+ return 0; -+ } -+ const newest = scrollHistory[newestIndex]; -+ if (Date.now() - newest.time > MAX_SCROLL_VELOCITY_WINDOW_MS) { -+ return 0; -+ } -+ let direction = 0; -+ let weightedVelocity = 0; -+ let totalWeight = 0; -+ for (let i = newestIndex; i > 0; i--) { - const current = scrollHistory[i]; - const previous = scrollHistory[i - 1]; - const scrollDiff = current.scroll - previous.scroll; -@@ -1844,281 +1657,604 @@ var getScrollVelocity = (state) => { - if (scrollDiff === 0 || timeDiff <= 0) { - continue; + // src/utils/getScrollVelocity.ts + var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; + var SCROLL_VELOCITY_HALF_LIFE_MS = 200; +@@ -2055,70 +1868,395 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { + if (nextTop === void 0 || nextTop > viewportEnd) { + break; } -- const age = newest.time - current.time; -- const weight = Math.exp(-age / SCROLL_VELOCITY_HALF_LIFE_MS); -- weightedVelocity += scrollDiff / timeDiff * weight; -- totalWeight += weight; +- end++; - } -- return totalWeight > 0 ? weightedVelocity / totalWeight : 0; --}; -- --// src/utils/hasActiveMVCPAnchorLock.ts --function hasActiveMVCPAnchorLock(state) { -- const lock = state.mvcpAnchorLock; -- if (!lock) { -- return false; -+ const age = newest.time - current.time; -+ const weight = Math.exp(-age / SCROLL_VELOCITY_HALF_LIFE_MS); -+ weightedVelocity += scrollDiff / timeDiff * weight; -+ totalWeight += weight; -+ } -+ return totalWeight > 0 ? weightedVelocity / totalWeight : 0; -+}; -+ -+// src/utils/hasActiveMVCPAnchorLock.ts -+function hasActiveMVCPAnchorLock(state) { -+ const lock = state.mvcpAnchorLock; -+ if (!lock) { -+ return false; -+ } -+ if (Date.now() > lock.expiresAt) { -+ state.mvcpAnchorLock = void 0; -+ return false; +- return { end, start }; +-} +-function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); +- if (range) { +- ctx.state.scrollTargetPinnedRange = range; +- ctx.state.scrollForNextCalculateItemsInView = void 0; +- } else { +- ctx.state.scrollTargetPinnedRange = void 0; ++ end++; + } -+ return true; ++ return { end, start }; +} -+ -+// src/utils/isInMVCPActiveMode.ts -+function isInMVCPActiveMode(state) { -+ return state.dataChangeNeedsScrollUpdate || hasActiveMVCPAnchorLock(state); ++function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { ++ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); ++ if (range) { ++ ctx.state.scrollTargetPinnedRange = range; ++ ctx.state.scrollForNextCalculateItemsInView = void 0; ++ } else { ++ ctx.state.scrollTargetPinnedRange = void 0; ++ } +} -+ -+// src/core/updateScroll.ts -+function updateScroll(ctx, newScroll, forceUpdate, options) { -+ var _a3; ++function scrollTo(ctx, params) { ++ var _a3, _b, _c; + const state = ctx.state; -+ const { ignoreScrollFromMVCP, lastScrollAdjustForHistory, scrollAdjustHandler, scrollHistory, scrollingTo } = state; -+ const prevScroll = state.scroll; -+ if ((options == null ? void 0 : options.markHasScrolled) !== false) { -+ state.hasScrolled = true; -+ } -+ const currentTime = Date.now(); -+ state.lastBatchingAction = currentTime; -+ const adjust = scrollAdjustHandler.getAdjust(); -+ const adjustChanged = lastScrollAdjustForHistory !== void 0 && Math.abs(adjust - lastScrollAdjustForHistory) > 0.1; -+ if (adjustChanged) { -+ scrollHistory.length = 0; -+ } -+ state.lastScrollAdjustForHistory = adjust; -+ if (scrollingTo === void 0 && !(scrollHistory.length === 0 && newScroll === state.scroll)) { -+ if (!adjustChanged) { -+ scrollHistory.push({ scroll: newScroll, time: currentTime }); -+ } -+ } -+ if (scrollHistory.length > 5) { -+ scrollHistory.shift(); -+ } -+ if (ignoreScrollFromMVCP && !scrollingTo) { -+ const { lt, gt } = ignoreScrollFromMVCP; -+ if (lt && newScroll < lt || gt && newScroll > gt) { -+ state.ignoreScrollFromMVCPIgnored = true; -+ return; ++ const { noScrollingTo, forceScroll, ...scrollTarget } = params; ++ const { ++ animated, ++ isInitialScroll, ++ offset: scrollTargetOffset, ++ precomputedWithViewOffset, ++ waitForInitialScrollCompletionFrame ++ } = scrollTarget; ++ const { ++ props: { horizontal } ++ } = state; ++ cancelScrollCompletionChecks(state); ++ const requestedOffset = precomputedWithViewOffset ? scrollTargetOffset : calculateOffsetWithOffsetPosition(ctx, scrollTargetOffset, scrollTarget); ++ const shouldPreserveRawInitialOffsetRequest = !!isInitialScroll && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; ++ const targetOffset = clampScrollOffset(ctx, requestedOffset, scrollTarget); ++ const offset = shouldPreserveRawInitialOffsetRequest ? requestedOffset : targetOffset; ++ state.scrollHistory.length = 0; ++ if (!noScrollingTo) { ++ if (isInitialScroll) { ++ initialScrollCompletion.resetFlags(state); + } -+ } -+ state.scrollPrev = prevScroll; -+ state.scrollPrevTime = state.scrollTime; -+ state.scroll = newScroll; -+ state.scrollTime = currentTime; -+ const scrollDelta = Math.abs(newScroll - prevScroll); -+ const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; -+ const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; -+ const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); -+ const scrollLength = state.scrollLength; -+ const isLargeUserScrollJump = scrollLength > 0 && scrollingTo === void 0 && scrollDelta > scrollLength && !state.pendingNativeMVCPAdjust; -+ const scrollVelocity = getScrollVelocity(state); -+ updateAdaptiveRender(ctx, scrollVelocity, { forceLight: isLargeUserScrollJump }); -+ const lastCalculated = state.scrollLastCalculate; -+ const useAggressiveItemRecalculation = isInMVCPActiveMode(state); -+ const shouldUpdate = useAggressiveItemRecalculation || didResolvePendingNativeMVCPAdjust || allowedEdge !== void 0 || forceUpdate || lastCalculated === void 0 || Math.abs(state.scroll - lastCalculated) > 2; -+ if (shouldUpdate) { -+ state.scrollLastCalculate = state.scroll; -+ state.ignoreScrollFromMVCPIgnored = false; -+ state.lastScrollDelta = scrollDelta; -+ const runCalculateItems = () => { -+ var _a4; -+ const calculateItemsParams = { -+ doMVCP: scrollingTo !== void 0, -+ scrollVelocity -+ }; -+ if (isLargeUserScrollJump) { -+ calculateItemsParams.drawDistanceMode = "visible-first"; -+ } -+ (_a4 = state.triggerCalculateItemsInView) == null ? void 0 : _a4.call(state, calculateItemsParams); -+ checkThresholds(ctx, allowedEdge); -+ }; -+ if (isLargeUserScrollJump) { -+ state.mvcpAnchorLock = void 0; -+ state.pendingNativeMVCPAdjust = void 0; -+ state.userScrollAnchorReset = { keys: /* @__PURE__ */ new Set() }; -+ state.scheduledWork.cancel("mvcpRecalculate"); -+ ReactDOM.flushSync(runCalculateItems); -+ scheduleFullDrawDistancePrewarm(ctx); -+ } else { -+ runCalculateItems(); -+ } -+ const shouldMaintainScrollAtEndAfterPendingSettle = !!state.pendingMaintainScrollAtEnd || !!((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onDataChange); -+ if (didResolvePendingNativeMVCPAdjust && shouldMaintainScrollAtEndAfterPendingSettle) { -+ state.pendingMaintainScrollAtEnd = false; -+ doMaintainScrollAtEnd(ctx); -+ } -+ state.dataChangeNeedsScrollUpdate = false; -+ state.lastScrollDelta = 0; -+ } -+} -+ -+// src/core/scrollTo.ts -+function getAverageSizeSnapshot(state) { -+ if (Object.keys(state.averageSizes).length === 0) { -+ return void 0; -+ } -+ const snapshot = {}; -+ for (const itemType in state.averageSizes) { -+ const averages = state.averageSizes[itemType]; -+ snapshot[itemType] = averages.avg; -+ } -+ return snapshot; -+} -+function syncInitialScrollNativeWatchdog(state, options) { -+ var _a3; -+ const { isInitialScroll, requestedOffset, targetOffset } = options; -+ const existingWatchdog = initialScrollWatchdog.get(state); -+ const shouldWatchInitialNativeScroll = !state.didFinishInitialScroll && (isInitialScroll || !!existingWatchdog) && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); -+ const shouldClearInitialNativeScrollWatchdog = !state.didFinishInitialScroll && !!existingWatchdog && initialScrollWatchdog.isAtZeroTargetOffset(requestedOffset); -+ if (shouldWatchInitialNativeScroll) { -+ state.hasScrolled = false; -+ initialScrollWatchdog.set(state, { -+ startScroll: (_a3 = existingWatchdog == null ? void 0 : existingWatchdog.startScroll) != null ? _a3 : state.scroll, -+ targetOffset -+ }); -+ return; -+ } -+ if (shouldClearInitialNativeScrollWatchdog) { -+ initialScrollWatchdog.clear(state); -+ } -+} -+function findPositionIndexAtOrBeforeOffset(ctx, offset) { -+ const state = ctx.state; -+ const dataLength = state.props.data.length; -+ let low = 0; -+ let high = dataLength - 1; -+ let match; -+ while (low <= high) { -+ const mid = Math.floor((low + high) / 2); -+ const top = state.positions[mid]; -+ if (top === void 0) { -+ high = mid - 1; -+ } else { -+ if (top <= offset) { -+ match = mid; -+ low = mid + 1; -+ } else { -+ high = mid - 1; -+ } -+ } -+ } -+ return match; -+} -+function getItemBottom(ctx, index) { -+ var _a3; -+ const top = ctx.state.positions[index]; -+ if (top === void 0) { -+ return void 0; -+ } -+ const itemSize = (_a3 = getItemSizeAtIndex(ctx, index)) != null ? _a3 : 0; -+ return top + (Number.isFinite(itemSize) ? itemSize : 0); -+} -+function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { -+ const state = ctx.state; -+ const dataLength = state.props.data.length; -+ if (dataLength === 0) { -+ return void 0; -+ } -+ const viewportStart = Math.max(0, targetOffset); -+ const viewportEnd = Math.max(viewportStart, targetOffset + state.scrollLength); -+ let start = targetIndex !== void 0 ? Math.max(0, Math.min(dataLength - 1, targetIndex)) : findPositionIndexAtOrBeforeOffset(ctx, viewportStart); -+ if (start === void 0) { -+ return void 0; -+ } -+ if (targetIndex !== void 0 && state.positions[start] === void 0) { -+ return { end: start, start }; -+ } -+ if (targetIndex === void 0) { -+ const startBottom = getItemBottom(ctx, start); -+ if (startBottom === void 0 || startBottom <= viewportStart) { -+ return void 0; -+ } -+ } -+ while (start > 0) { -+ const top = state.positions[start]; -+ if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { -+ break; -+ } -+ start--; -+ } -+ while (start > 0) { -+ const previousBottom = getItemBottom(ctx, start - 1); -+ if (previousBottom === void 0 || previousBottom <= viewportStart) { -+ break; -+ } -+ start--; -+ } -+ let end = start; -+ while (end + 1 < dataLength) { -+ const nextTop = state.positions[end + 1]; -+ if (nextTop === void 0 || nextTop > viewportEnd) { -+ break; -+ } -+ end++; -+ } -+ return { end, start }; -+} -+function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -+ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -+ if (range) { -+ ctx.state.scrollTargetPinnedRange = range; -+ ctx.state.scrollForNextCalculateItemsInView = void 0; -+ } else { -+ ctx.state.scrollTargetPinnedRange = void 0; -+ } -+} -+function scrollTo(ctx, params) { -+ var _a3, _b, _c; -+ const state = ctx.state; -+ const { noScrollingTo, forceScroll, ...scrollTarget } = params; -+ const { -+ animated, -+ isInitialScroll, -+ offset: scrollTargetOffset, -+ precomputedWithViewOffset, -+ waitForInitialScrollCompletionFrame -+ } = scrollTarget; -+ const { -+ props: { horizontal } -+ } = state; -+ cancelScrollCompletionChecks(state); -+ const requestedOffset = precomputedWithViewOffset ? scrollTargetOffset : calculateOffsetWithOffsetPosition(ctx, scrollTargetOffset, scrollTarget); -+ const shouldPreserveRawInitialOffsetRequest = !!isInitialScroll && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; -+ const targetOffset = clampScrollOffset(ctx, requestedOffset, scrollTarget); -+ const offset = shouldPreserveRawInitialOffsetRequest ? requestedOffset : targetOffset; -+ state.scrollHistory.length = 0; -+ if (!noScrollingTo) { -+ if (isInitialScroll) { -+ initialScrollCompletion.resetFlags(state); -+ } -+ const averageSizeSnapshot = getAverageSizeSnapshot(state); -+ state.scrollingTo = { -+ ...scrollTarget, -+ ...averageSizeSnapshot ? { averageSizeSnapshot } : {}, -+ targetOffset, -+ waitForInitialScrollCompletionFrame ++ const averageSizeSnapshot = getAverageSizeSnapshot(state); ++ state.scrollingTo = { ++ ...scrollTarget, ++ ...averageSizeSnapshot ? { averageSizeSnapshot } : {}, ++ targetOffset, ++ waitForInitialScrollCompletionFrame + }; + if (!isInitialScroll) { + pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); @@ -4815,30 +4350,23 @@ index 914d2da..c3ed202 100644 + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); + } - } -- if (Date.now() > lock.expiresAt) { -- state.mvcpAnchorLock = void 0; -- return false; ++ } + if (forceScroll || !isInitialScroll || Platform.OS === "android") { + doScrollTo(ctx, { animated, horizontal, offset }); + } else { + state.scroll = offset; - } -- return true; - } - --// src/utils/isInMVCPActiveMode.ts --function isInMVCPActiveMode(state) { -- return state.dataChangeNeedsScrollUpdate || hasActiveMVCPAnchorLock(state); ++ } ++} ++ +// src/core/scrollTargetSettle.ts -+var SETTLE_POSITION_EPSILON = 0.5; ++var SETTLE_POSITION_EPSILON = 1; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; +var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; -+function releaseScrollTargetForUserInteraction(ctx) { -+ if (ctx.state.scrollTargetSettle) { -+ clearScrollTargetSettle(ctx.state); ++function releaseScrollTargetForUserInteraction(state) { ++ if (state.scrollTargetSettle) { ++ clearScrollTargetSettle(state); + } +} +function clearScrollTargetSettle(state) { @@ -4855,6 +4383,7 @@ index 914d2da..c3ed202 100644 + clearScrollTargetSettle(state); + return; + } ++ clearScrollTargetSettle(state); + const now = Date.now(); + state.scrollTargetSettle = { + corrections: 0, @@ -4867,48 +4396,28 @@ index 914d2da..c3ed202 100644 + viewPosition + }; + state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline"); - } -- --// src/core/updateScroll.ts --function updateScroll(ctx, newScroll, forceUpdate, options) { ++} +function getSettleTargetOffset(ctx, settle, index, position) { + const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; + return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} +function applyScrollTargetCorrection(ctx, id) { - var _a3; - const state = ctx.state; -- const { ignoreScrollFromMVCP, lastScrollAdjustForHistory, scrollAdjustHandler, scrollHistory, scrollingTo } = state; -- const prevScroll = state.scroll; -- if ((options == null ? void 0 : options.markHasScrolled) !== false) { -- state.hasScrolled = true; ++ var _a3; ++ const state = ctx.state; + const settle = state.scrollTargetSettle; + if (!settle || settle.id !== id) { + return; - } -- const currentTime = Date.now(); -- state.lastBatchingAction = currentTime; -- const adjust = scrollAdjustHandler.getAdjust(); -- const adjustChanged = lastScrollAdjustForHistory !== void 0 && Math.abs(adjust - lastScrollAdjustForHistory) > 0.1; -- if (adjustChanged) { -- scrollHistory.length = 0; ++ } + const index = state.indexByKey.get(id); + const position = index === void 0 ? void 0 : state.positions[index]; + if (index === void 0 || position === void 0 || Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return; - } -- state.lastScrollAdjustForHistory = adjust; -- if (scrollingTo === void 0 && !(scrollHistory.length === 0 && newScroll === state.scroll)) { -- if (!adjustChanged) { -- scrollHistory.push({ scroll: newScroll, time: currentTime }); -- } ++ } + if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { + clearScrollTargetSettle(state); + return; - } -- if (scrollHistory.length > 5) { -- scrollHistory.shift(); ++ } + settle.corrections++; + settle.measuredIndex = void 0; + scrollTo(ctx, { @@ -4931,65 +4440,21 @@ index 914d2da..c3ed202 100644 + const settle = state.scrollTargetSettle; + if (!settle) { + return false; - } -- if (ignoreScrollFromMVCP && !scrollingTo) { -- const { lt, gt } = ignoreScrollFromMVCP; -- if (lt && newScroll < lt || gt && newScroll > gt) { -- state.ignoreScrollFromMVCPIgnored = true; -- return; ++ } + const measured = options == null ? void 0 : options.minIndexSizeChanged; + if (measured !== void 0) { + settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); ++ settle.expiresAt = Date.now() + SETTLE_TTL_MS; + } + if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return false; - } ++ } + settle.quietPasses = 0; + settle.expiresAt = Date.now() + SETTLE_TTL_MS; + return false; - } -- state.scrollPrev = prevScroll; -- state.scrollPrevTime = state.scrollTime; -- state.scroll = newScroll; -- state.scrollTime = currentTime; -- const scrollDelta = Math.abs(newScroll - prevScroll); -- const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; -- const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; -- const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); -- const scrollLength = state.scrollLength; -- const isLargeUserScrollJump = scrollLength > 0 && scrollingTo === void 0 && scrollDelta > scrollLength && !state.pendingNativeMVCPAdjust; -- const scrollVelocity = getScrollVelocity(state); -- updateAdaptiveRender(ctx, scrollVelocity, { forceLight: isLargeUserScrollJump }); -- const lastCalculated = state.scrollLastCalculate; -- const useAggressiveItemRecalculation = isInMVCPActiveMode(state); -- const shouldUpdate = useAggressiveItemRecalculation || didResolvePendingNativeMVCPAdjust || allowedEdge !== void 0 || forceUpdate || lastCalculated === void 0 || Math.abs(state.scroll - lastCalculated) > 2; -- if (shouldUpdate) { -- state.scrollLastCalculate = state.scroll; -- state.ignoreScrollFromMVCPIgnored = false; -- state.lastScrollDelta = scrollDelta; -- const runCalculateItems = () => { -- var _a4; -- const calculateItemsParams = { -- doMVCP: scrollingTo !== void 0, -- scrollVelocity -- }; -- if (isLargeUserScrollJump) { -- calculateItemsParams.drawDistanceMode = "visible-first"; -- } -- (_a4 = state.triggerCalculateItemsInView) == null ? void 0 : _a4.call(state, calculateItemsParams); -- checkThresholds(ctx, allowedEdge); -- }; -- if (isLargeUserScrollJump) { -- state.mvcpAnchorLock = void 0; -- state.pendingNativeMVCPAdjust = void 0; -- state.userScrollAnchorReset = { keys: /* @__PURE__ */ new Set() }; -- state.scheduledWork.cancel("mvcpRecalculate"); -- ReactDOM.flushSync(runCalculateItems); -- scheduleFullDrawDistancePrewarm(ctx); -- } else { -- runCalculateItems(); ++ } + const index = state.indexByKey.get(settle.id); + const position = index === void 0 ? void 0 : state.positions[index]; + if (index === void 0 || position === void 0) { @@ -5011,11 +4476,7 @@ index 914d2da..c3ed202 100644 + settle.quietPasses++; + if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { + clearScrollTargetSettle(state); - } -- const shouldMaintainScrollAtEndAfterPendingSettle = !!state.pendingMaintainScrollAtEnd || !!((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onDataChange); -- if (didResolvePendingNativeMVCPAdjust && shouldMaintainScrollAtEndAfterPendingSettle) { -- state.pendingMaintainScrollAtEnd = false; -- doMaintainScrollAtEnd(ctx); ++ } + return false; + } + settle.quietPasses = 0; @@ -5054,9 +4515,7 @@ index 914d2da..c3ed202 100644 + nativeEvent: { + ...event.nativeEvent, + contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } - } -- state.dataChangeNeedsScrollUpdate = false; -- state.lastScrollDelta = 0; ++ } + }; +} +function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { @@ -5073,13 +4532,9 @@ index 914d2da..c3ed202 100644 + (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 + ) + ); - } - } - --// src/core/scrollTo.ts --function getAverageSizeSnapshot(state) { -- if (Object.keys(state.averageSizes).length === 0) { -- return void 0; ++ } ++} ++ +// src/utils/setInitialRenderState.ts +function resetInitialRenderState(ctx, { + resetLayout, @@ -5089,31 +4544,13 @@ index 914d2da..c3ed202 100644 + if (resetLayout) { + state.didContainersLayout = false; + state.queuedInitialLayout = false; - } -- const snapshot = {}; -- for (const itemType in state.averageSizes) { -- const averages = state.averageSizes[itemType]; -- snapshot[itemType] = averages.avg; ++ } + if (resetInitialScroll) { + state.didFinishInitialScroll = false; - } -- return snapshot; ++ } + set$(ctx, "readyToRender", false); + resetAdaptiveRender(ctx); - } --function syncInitialScrollNativeWatchdog(state, options) { -- var _a3; -- const { isInitialScroll, requestedOffset, targetOffset } = options; -- const existingWatchdog = initialScrollWatchdog.get(state); -- const shouldWatchInitialNativeScroll = !state.didFinishInitialScroll && (isInitialScroll || !!existingWatchdog) && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); -- const shouldClearInitialNativeScrollWatchdog = !state.didFinishInitialScroll && !!existingWatchdog && initialScrollWatchdog.isAtZeroTargetOffset(requestedOffset); -- if (shouldWatchInitialNativeScroll) { -- state.hasScrolled = false; -- initialScrollWatchdog.set(state, { -- startScroll: (_a3 = existingWatchdog == null ? void 0 : existingWatchdog.startScroll) != null ? _a3 : state.scroll, -- targetOffset -- }); -- return; ++} +function setInitialRenderState(ctx, { + didLayout, + didInitialScroll @@ -5125,30 +4562,10 @@ index 914d2da..c3ed202 100644 + } = state; + if (didLayout) { + state.didContainersLayout = true; - } -- if (shouldClearInitialNativeScrollWatchdog) { -- initialScrollWatchdog.clear(state); ++ } + if (didInitialScroll) { + state.didFinishInitialScroll = true; - } --} --function findPositionIndexAtOrBeforeOffset(ctx, offset) { -- const state = ctx.state; -- const dataLength = state.props.data.length; -- let low = 0; -- let high = dataLength - 1; -- let match; -- while (low <= high) { -- const mid = Math.floor((low + high) / 2); -- const top = state.positions[mid]; -- if (top === void 0) { -- high = mid - 1; -- } else { -- if (top <= offset) { -- match = mid; -- low = mid + 1; -- } else { -- high = mid - 1; ++ } + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); + if (isReadyToRender && !peek$(ctx, "readyToRender")) { + set$(ctx, "readyToRender", true); @@ -5160,19 +4577,10 @@ index 914d2da..c3ed202 100644 + state.didLoad = true; + if (onLoad) { + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); - } - } - } -- return match; - } --function getItemBottom(ctx, index) { -- var _a3; -- const top = ctx.state.positions[index]; -- if (top === void 0) { -- return void 0; -- } -- const itemSize = (_a3 = getItemSizeAtIndex(ctx, index)) != null ? _a3 : 0; -- return top + (Number.isFinite(itemSize) ? itemSize : 0); ++ } ++ } ++ } ++} + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -5180,8 +4588,7 @@ index 914d2da..c3ed202 100644 + state.scroll = offset; + state.scrollPending = offset; + state.scrollPrev = offset; - } --function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { ++} +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -5193,65 +4600,17 @@ index 914d2da..c3ed202 100644 +} +function supersedeInitialScroll(ctx) { + var _a3, _b, _c; - const state = ctx.state; -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return void 0; -- } -- const viewportStart = Math.max(0, targetOffset); -- const viewportEnd = Math.max(viewportStart, targetOffset + state.scrollLength); -- let start = targetIndex !== void 0 ? Math.max(0, Math.min(dataLength - 1, targetIndex)) : findPositionIndexAtOrBeforeOffset(ctx, viewportStart); -- if (start === void 0) { -- return void 0; -- } -- if (targetIndex !== void 0 && state.positions[start] === void 0) { -- return { end: start, start }; -- } -- if (targetIndex === void 0) { -- const startBottom = getItemBottom(ctx, start); -- if (startBottom === void 0 || startBottom <= viewportStart) { -- return void 0; -- } -- } -- while (start > 0) { -- const top = state.positions[start]; -- if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { -- break; -- } -- start--; -- } -- while (start > 0) { -- const previousBottom = getItemBottom(ctx, start - 1); -- if (previousBottom === void 0 || previousBottom <= viewportStart) { -- break; ++ const state = ctx.state; + const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; + if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { + if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { + cancelAnimationFrame(bootstrapInitialScroll.frameHandle); - } -- start--; -- } -- let end = start; -- while (end + 1 < dataLength) { -- const nextTop = state.positions[end + 1]; -- if (nextTop === void 0 || nextTop > viewportEnd) { -- break; ++ } + if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { + cancelScrollCompletionChecks(state); + state.scrollingTo = void 0; + state.scrollTargetPinnedRange = void 0; - } -- end++; -- } -- return { end, start }; --} --function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -- if (range) { -- ctx.state.scrollTargetPinnedRange = range; -- ctx.state.scrollForNextCalculateItemsInView = void 0; -- } else { -- ctx.state.scrollTargetPinnedRange = void 0; ++ } + initialScrollCompletion.resetFlags(state); + setInitialScrollSession(state, { bootstrap: null }); + finishInitialScroll(ctx); @@ -5360,15 +4719,15 @@ index 914d2da..c3ed202 100644 } // src/core/scrollToIndex.ts -@@ -4350,6 +4486,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4350,6 +4488,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); -+ const minIndexSizeChangedThisPass = dataChanged ? 0 : minIndexSizeChanged; ++ const minIndexSizeChangedThisPass = minIndexSizeChanged; if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4367,8 +4504,17 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4367,8 +4506,17 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -5388,11 +4747,33 @@ index 914d2da..c3ed202 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5915,6 +6061,119 @@ function useRafCoalescer(callback) { - return coalescer; +@@ -5927,6 +6075,21 @@ function getDocumentScrollerNode() { + } + return document.scrollingElement || document.documentElement || document.body; + } ++function getScrollAxis(horizontal) { ++ return horizontal ? { ++ contentSizeKey: "scrollWidth", ++ paddingEndProp: "paddingRight", ++ viewportSizeKey: "clientWidth", ++ x: 1, ++ y: 0 ++ } : { ++ contentSizeKey: "scrollHeight", ++ paddingEndProp: "paddingBottom", ++ viewportSizeKey: "clientHeight", ++ x: 0, ++ y: 1 ++ }; ++} + function getWindowScrollPosition() { + var _a3, _b, _c, _d; + if (typeof window === "undefined") { +@@ -6003,9 +6166,175 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll + }; } -+// src/components/temporaryEndPadding.ts ++// src/components/webTemporaryEndPadding.ts +var entriesByNode = /* @__PURE__ */ new WeakMap(); +var nextRequestId = 1; +function readResolvedPadding(node, prop) { @@ -5413,6 +4794,20 @@ index 914d2da..c3ed202 100644 + node.style[prop] = total === 0 ? entry.baseline : `${entry.baselineSize + total}px`; + entry.lastApplied = node.style[prop]; +} ++function drainPendingReleases(entry) { ++ if (entry.resetHandle !== void 0) { ++ cancelAnimationFrame(entry.resetHandle); ++ entry.resetHandle = void 0; ++ } ++ if (entry.pendingReleases.size === 0) { ++ return; ++ } ++ const releases = [...entry.pendingReleases]; ++ entry.pendingReleases.clear(); ++ for (const pending of releases) { ++ pending(); ++ } ++} +function releaseEntry(node, prop, requestId) { + const entries = entriesByNode.get(node); + const entry = entries == null ? void 0 : entries[prop]; @@ -5423,10 +4818,8 @@ index 914d2da..c3ed202 100644 + applyPadding(node, prop, entry); + } + if (entry.requests.size === 0) { -+ if (entry.resetHandle !== void 0) { -+ cancelAnimationFrame(entry.resetHandle); -+ } + entries == null ? true : delete entries[prop]; ++ drainPendingReleases(entry); + } +} +function addTemporaryEndPadding(node, prop, extraSize) { @@ -5479,7 +4872,10 @@ index 914d2da..c3ed202 100644 +} +function getTemporaryEndPadding(node, prop) { + var _a3; -+ const entry = node ? (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop] : void 0; ++ if (!node) { ++ return 0; ++ } ++ const entry = (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop]; + if (!entry || !isOwnedByUs(node, prop, entry)) { + return 0; + } @@ -5495,43 +4891,77 @@ index 914d2da..c3ed202 100644 + if (!entry) { + continue; + } -+ if (entry.resetHandle !== void 0) { -+ cancelAnimationFrame(entry.resetHandle); -+ } + if (isOwnedByUs(node, prop, entry)) { + node.style[prop] = entry.baseline; + } + delete entries[prop]; ++ entry.requests.clear(); ++ drainPendingReleases(entry); + } +} + - // src/components/webConstants.ts - var LEGEND_LIST_CONTENT_CONTAINER_CLASS = "legend-list-content-container"; - var LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS = "legend-list-scrollbar-x-hidden"; -@@ -6006,6 +6265,10 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; +var SMOOTH_SCROLL_MAX_MS = 2e3; ++var MAX_BORROW_VIEWPORTS = 3; ++var BORROW_WATCH_FRAME_INTERVAL = 3; +var BORROW_MEASURE_ATTEMPTS = 2; -+var USER_INTERACTION_EVENTS = ["wheel", "pointerdown", "touchstart", "keydown"]; ++var USER_DRAG_SLOP = 8; ++var WHEEL_MOMENTUM_GRACE_MS = 150; ++var SCROLL_KEYS = /* @__PURE__ */ new Set([ ++ " ", ++ "ArrowDown", ++ "ArrowLeft", ++ "ArrowRight", ++ "ArrowUp", ++ "End", ++ "Home", ++ "PageDown", ++ "PageUp" ++]); ++function isTextEntryTarget(target) { ++ var _a3; ++ const element = target; ++ const tag = (_a3 = element == null ? void 0 : element.tagName) == null ? void 0 : _a3.toLowerCase(); ++ return tag === "input" || tag === "textarea" || tag === "select" || !!(element == null ? void 0 : element.isContentEditable); ++} ++function pointerPosition(event) { ++ var _a3; ++ const touch = (_a3 = event.touches) == null ? void 0 : _a3[0]; ++ if (touch) { ++ return { x: touch.clientX, y: touch.clientY }; ++ } ++ const pointer = event; ++ return typeof pointer.clientX === "number" ? { x: pointer.clientX, y: pointer.clientY } : void 0; ++} ++function isScrollKey(event) { ++ if (event.altKey || event.ctrlKey || event.metaKey) { ++ return false; ++ } ++ return SCROLL_KEYS.has(event.key) && !isTextEntryTarget(event.target); ++} var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6074,6 +6337,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6074,6 +6403,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); -+ const paddingEndProp = horizontal ? isHorizontalRTL(ctx.state) ? "paddingLeft" : "paddingRight" : "paddingBottom"; ++ const paddingEndProp = getScrollAxis(horizontal).paddingEndProp; + const getCommittedMaxScrollOffset = React3.useCallback( + (maxOffset) => Math.max(0, maxOffset - getTemporaryEndPadding(contentRef.current, paddingEndProp)), + [paddingEndProp] + ); ++ const getViewportExtent = React3.useCallback(() => { ++ const layout = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); ++ return horizontal ? layout.width : layout.height; ++ }, [horizontal, isWindowScroll]); const getMaxScrollOffset = React3.useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6094,6 +6362,95 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,8 +6432,153 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -5546,7 +4976,13 @@ index 914d2da..c3ed202 100644 + const contentNode = contentRef.current; + (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); + const committedMaxOffset = getCommittedMaxScrollOffset(getMaxScrollOffset()); -+ if (!contentNode || offset <= committedMaxOffset + SCROLL_EXTENT_EPSILON) { ++ const viewportExtent = getViewportExtent(); ++ const reachLimit = viewportExtent > 0 ? committedMaxOffset + MAX_BORROW_VIEWPORTS * viewportExtent : Number.POSITIVE_INFINITY; ++ if (!contentNode || !Number.isFinite(offset) || offset > reachLimit) { ++ run(clampOffset(offset, maxOffset)); ++ return; ++ } ++ if (offset <= committedMaxOffset + SCROLL_EXTENT_EPSILON) { + run(clampOffset(offset, maxOffset)); + return; + } @@ -5574,12 +5010,13 @@ index 914d2da..c3ed202 100644 + } + const scrollTarget = getScrollTarget(); + const supportsScrollEnd = !!scrollTarget && "onscrollend" in scrollTarget; ++ const timers = {}; + const finish = () => { + if (animatedPaddingReleaseRef.current !== finish) { + return; + } + animatedPaddingReleaseRef.current = void 0; -+ clearTimeout(settleTimeout); ++ clearTimeout(timers.settle); + scrollTarget == null ? void 0 : scrollTarget.removeEventListener("scrollend", finishIfArrived); + cancelAnimationFrame(borrowWatchRef.current); + release(); @@ -5589,25 +5026,36 @@ index 914d2da..c3ed202 100644 + finish(); + } + }; ++ let framesUntilCheck = 0; + const releaseWhenContentCommits = () => { + if (animatedPaddingReleaseRef.current !== finish) { + return; + } -+ if (getCommittedMaxScrollOffset(getMaxScrollOffset()) + SCROLL_EXTENT_EPSILON >= offset) { -+ finish(); -+ return; ++ if (framesUntilCheck-- <= 0) { ++ framesUntilCheck = BORROW_WATCH_FRAME_INTERVAL; ++ if (getCommittedMaxScrollOffset(getMaxScrollOffset()) + SCROLL_EXTENT_EPSILON >= offset) { ++ finish(); ++ return; ++ } + } + borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); + }; + animatedPaddingReleaseRef.current = finish; + cancelAnimationFrame(borrowWatchRef.current); + borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); -+ const settleTimeout = setTimeout(finish, SMOOTH_SCROLL_MAX_MS); ++ timers.settle = setTimeout(finish, SMOOTH_SCROLL_MAX_MS); + if (supportsScrollEnd) { + scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finishIfArrived); + } + }, -+ [getCommittedMaxScrollOffset, getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, paddingEndProp] ++ [ ++ getCommittedMaxScrollOffset, ++ getCurrentScrollOffset, ++ getMaxScrollOffset, ++ getScrollTarget, ++ getViewportExtent, ++ paddingEndProp ++ ] + ); + React3.useEffect( + () => () => { @@ -5621,13 +5069,53 @@ index 914d2da..c3ed202 100644 + }, + [] + ); ++ const interactionArmedAtRef = React3.useRef(0); ++ const dragOriginRef = React3.useRef(void 0); + const reportUserInteraction = React3.useCallback(() => { -+ releaseScrollTargetForUserInteraction(ctx); ++ dragOriginRef.current = void 0; ++ releaseScrollTargetForUserInteraction(ctx.state); + }, [ctx]); ++ const onWheel = React3.useCallback( ++ (event) => { ++ if (event.timeStamp - interactionArmedAtRef.current < WHEEL_MOMENTUM_GRACE_MS) { ++ return; ++ } ++ reportUserInteraction(); ++ }, ++ [reportUserInteraction] ++ ); ++ const onPointerDown = React3.useCallback((event) => { ++ const point = pointerPosition(event); ++ dragOriginRef.current = point; ++ }, []); ++ const onPointerMove = React3.useCallback( ++ (event) => { ++ const origin = dragOriginRef.current; ++ const point = origin && pointerPosition(event); ++ if (!origin || !point) { ++ return; ++ } ++ if (Math.abs(point.x - origin.x) > USER_DRAG_SLOP || Math.abs(point.y - origin.y) > USER_DRAG_SLOP) { ++ reportUserInteraction(); ++ } ++ }, ++ [reportUserInteraction] ++ ); ++ const onKeyDown = React3.useCallback( ++ (event) => { ++ if (isScrollKey(event)) { ++ reportUserInteraction(); ++ } ++ }, ++ [reportUserInteraction] ++ ); const scrollToLocalOffset = React3.useCallback( (offset, animated) => { ++ interactionArmedAtRef.current = typeof performance !== "undefined" ? performance.now() : 0; const scrollElement = scrollRef.current; -@@ -6116,14 +6473,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + const target = getScrollTarget(); + if (!target || typeof target.scrollTo !== "function") { +@@ -6116,14 +6599,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); options.left = left; options.top = top; @@ -5654,7 +5142,7 @@ index 914d2da..c3ed202 100644 ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6513,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6149,8 +6639,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -5664,7 +5152,7 @@ index 914d2da..c3ed202 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6163,7 +6526,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6163,7 +6652,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView if (!onScroll2 || !scrollRef.current) { return; } @@ -5681,25 +5169,74 @@ index 914d2da..c3ed202 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6221,11 +6592,17 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6221,11 +6718,26 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView const target = getScrollTarget(); if (!target) return; target.addEventListener("scroll", handleScroll, { passive: true }); -+ for (const type of USER_INTERACTION_EVENTS) { -+ target.addEventListener(type, reportUserInteraction, { passive: true }); -+ } ++ const listenerOptions = { capture: true, passive: true }; ++ const removeOptions = { capture: true }; ++ const interactionTarget = scrollRef.current; ++ target.addEventListener("wheel", onWheel, listenerOptions); ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener("pointerdown", onPointerDown, listenerOptions); ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener("pointermove", onPointerMove, listenerOptions); ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener("touchstart", onPointerDown, listenerOptions); ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener("touchmove", onPointerMove, listenerOptions); ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener("keydown", onKeyDown, listenerOptions); if ("onscrollend" in target) { target.addEventListener("scrollend", emitScrollEnd); } return () => { target.removeEventListener("scroll", handleScroll); -+ for (const type of USER_INTERACTION_EVENTS) { -+ target.removeEventListener(type, reportUserInteraction); -+ } ++ target.removeEventListener("wheel", onWheel, removeOptions); ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("pointerdown", onPointerDown, removeOptions); ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("pointermove", onPointerMove, removeOptions); ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("touchstart", onPointerDown, removeOptions); ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("touchmove", onPointerMove, removeOptions); ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("keydown", onKeyDown, removeOptions); if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6411,8 +6788,6 @@ function ScrollAdjust() { +@@ -6236,7 +6748,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + } + scrollEventCoalescer.cancel(); + }; +- }, [emitScrollEnd, getScrollTarget, handleScroll, scrollEventCoalescer]); ++ }, [ ++ emitScrollEnd, ++ getScrollTarget, ++ handleScroll, ++ onKeyDown, ++ onPointerDown, ++ onPointerMove, ++ onWheel, ++ scrollEventCoalescer ++ ]); + React3.useEffect(() => { + const doScroll = () => { + if (contentOffset) { +@@ -6379,21 +6900,6 @@ function useValueListener$(key, callback) { + } + + // src/components/ScrollAdjust.tsx +-function getScrollAdjustAxis(horizontal) { +- return horizontal ? { +- contentSizeKey: "scrollWidth", +- paddingEndProp: "paddingRight", +- viewportSizeKey: "clientWidth", +- x: 1, +- y: 0 +- } : { +- contentSizeKey: "scrollHeight", +- paddingEndProp: "paddingBottom", +- viewportSizeKey: "clientHeight", +- x: 0, +- y: 1 +- }; +-} + function getScrollAdjustTarget(ctx, contentNode) { + var _a3, _b, _c; + const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; +@@ -6411,8 +6917,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3__namespace.useRef(0); const lastScrollAdjustUserOffsetRef = React3__namespace.useRef(0); @@ -5708,7 +5245,16 @@ index 914d2da..c3ed202 100644 const contentNodeRef = React3__namespace.useRef(null); const callback = React3__namespace.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6438,29 +6813,10 @@ function ScrollAdjust() { +@@ -6423,7 +6927,7 @@ function ScrollAdjust() { + const target = getScrollAdjustTarget(ctx, contentNodeRef.current); + if (target) { + const horizontal = !!ctx.state.props.horizontal; +- const axis = getScrollAdjustAxis(horizontal); ++ const axis = getScrollAxis(horizontal); + const { contentNode, scrollElement: el } = target; + const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; + const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; +@@ -6438,29 +6942,10 @@ function ScrollAdjust() { const nextScroll = currentScroll + scrollDelta; const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; if (needsTemporaryPadding) { @@ -5740,19 +5286,19 @@ index 914d2da..c3ed202 100644 } else { scrollBy(); } -@@ -8288,6 +8644,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8288,6 +8773,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); -+ clearScrollTargetSettle(state); ++ releaseScrollTargetForUserInteraction(state); (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs -index 95465f2..9ac6d3f 100644 +index 95465f2..ea937e4 100644 --- a/node_modules/@legendapp/list/react-native.web.mjs +++ b/node_modules/@legendapp/list/react-native.web.mjs -@@ -413,178 +413,266 @@ var EDGE_POSITION_EPSILON = 1; +@@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; var ENABLE_DEVMODE = IS_DEV && false; var ENABLE_DEBUG_VIEW = IS_DEV && false; @@ -5762,56 +5308,22 @@ index 95465f2..9ac6d3f 100644 - scheduledWork.cancel("checkFinishedScrollRetryFrame"); - scheduledWork.cancel("checkFinishedScrollFallback"); - scheduledWork.cancel("platformScrollCompletion"); -+// src/core/getStartOffsetAdjustment.ts -+function getStartOffsetAdjustment(ctx) { -+ const { state } = ctx; -+ const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; -+ return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); - } +-} -function settlePendingImperativeScroll(state) { - var _a3, _b; - const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; - state.pendingScrollResolve = void 0; - state.pendingScrollToEnd = void 0; - resolvePendingScroll == null ? void 0 : resolvePendingScroll(); -+ -+// src/utils/getId.ts -+function getId(state, index) { -+ const { data, keyExtractor } = state.props; -+ if (!data) { -+ return ""; -+ } -+ const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; -+ const id = ret; -+ state.idCache[index] = id; -+ return id; - } +-} -function cancelImperativeScroll(state) { - cancelScrollCompletionChecks(state); - state.scheduledWork.cancel("imperativeScrollReady"); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; - settlePendingImperativeScroll(state); -+ -+// src/core/updateContentMetricsState.ts -+function getRawContentLength(ctx) { -+ var _a3, _b, _c; -+ const { state, values } = ctx; -+ return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); -+} -+function getAlignItemsAtEndPadding(ctx) { -+ const { state } = ctx; -+ const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; -+ return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; -+} -+function updateContentMetricsState(ctx) { -+ const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; -+ const nextPadding = getAlignItemsAtEndPadding(ctx); -+ if (previousPadding !== nextPadding) { -+ set$(ctx, "alignItemsAtEndPadding", nextPadding); -+ } - } - +-} +- -// src/core/deferredPublicOnScroll.ts -function withResolvedContentOffset(state, event, resolvedOffset) { - return { @@ -5819,40 +5331,12 @@ index 95465f2..9ac6d3f 100644 - nativeEvent: { - ...event.nativeEvent, - contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -+// src/core/addTotalSize.ts -+function addTotalSize(ctx, key, add, notifyTotalSize = true) { -+ const state = ctx.state; -+ const prevTotalSize = state.totalSize; -+ let totalSize = state.totalSize; -+ if (key === null) { -+ totalSize = add; -+ if (state.timeoutSetPaddingTop) { -+ clearTimeout(state.timeoutSetPaddingTop); -+ state.timeoutSetPaddingTop = void 0; - } +- } - }; -+ } else { -+ totalSize += add; -+ } -+ if (prevTotalSize !== totalSize) { -+ { -+ state.pendingTotalSize = void 0; -+ state.totalSize = totalSize; -+ if (notifyTotalSize) { -+ set$(ctx, "totalSize", totalSize); -+ } -+ updateContentMetricsState(ctx); -+ } -+ } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { -+ set$(ctx, "totalSize", totalSize); -+ } - } +-} -function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { - var _a3, _b, _c, _d; -+ -+// src/core/setSize.ts -+function setSize(ctx, itemKey, size, notifyTotalSize = true) { - const state = ctx.state; +- const state = ctx.state; - const deferredEvent = state.deferredPublicOnScrollEvent; - state.deferredPublicOnScrollEvent = void 0; - if (deferredEvent) { @@ -5864,15 +5348,9 @@ index 95465f2..9ac6d3f 100644 - (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 - ) - ); -+ const { sizes } = state; -+ const previousSize = sizes.get(itemKey); -+ const diff = previousSize !== void 0 ? size - previousSize : size; -+ if (diff !== 0) { -+ addTotalSize(ctx, itemKey, diff, notifyTotalSize); - } -+ sizes.set(itemKey, size); - } - +- } +-} +- -// src/core/initialScrollSession.ts -var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -function hasInitialScrollSessionCompletion(completion) { @@ -5881,10 +5359,7 @@ index 95465f2..9ac6d3f 100644 -function clearInitialScrollSession(state) { - state.initialScrollSession = void 0; - return void 0; -+// src/utils/helpers.ts -+function isFunction(obj) { -+ return typeof obj === "function"; - } +-} -function createInitialScrollSession(options) { - const { bootstrap, completion, kind, previousDataLength } = options; - return kind === "offset" ? { @@ -5897,9 +5372,7 @@ index 95465f2..9ac6d3f 100644 - kind, - previousDataLength - }; -+function isArray(obj) { -+ return Array.isArray(obj); - } +-} -function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { - var _a4, _b2; - if (!state.initialScrollSession) { @@ -5915,15 +5388,10 @@ index 95465f2..9ac6d3f 100644 - kind, - previousDataLength: state.initialScrollSession.previousDataLength - }); -+var warned = /* @__PURE__ */ new Set(); -+function warnDevOnce(id, text) { -+ if (IS_DEV && !warned.has(id)) { -+ warned.add(id); -+ console.warn(`[legend-list] ${text}`); - } +- } - (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; - return state.initialScrollSession.completion; - } +-} -var initialScrollCompletion = { - didDispatchNativeScroll(state) { - var _a3, _b; @@ -5946,26 +5414,7 @@ index 95465f2..9ac6d3f 100644 - const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); - completion.didDispatchNativeScroll = void 0; - completion.didRetrySilentInitialScroll = void 0; -+function roundSize(size) { -+ return Math.floor(size * 8) / 8; -+} -+function isNullOrUndefined(value) { -+ return value === null || value === void 0; -+} -+function getPadding(s, type) { -+ var _a3, _b, _c; -+ const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; -+ return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; -+} -+function extractPadding(style, contentContainerStyle, type) { -+ return getPadding(style, type) + getPadding(contentContainerStyle, type); -+} -+function findContainerId(ctx, key) { -+ var _a3, _b; -+ const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); -+ if (directMatch !== void 0) { -+ return directMatch; - } +- } -}; -var initialScrollWatchdog = { - clear(state) { @@ -5989,12 +5438,7 @@ index 95465f2..9ac6d3f 100644 - var _a3, _b; - if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { - return; -+ const numContainers = peek$(ctx, "numContainers"); -+ for (let i = 0; i < numContainers; i++) { -+ const itemKey = peek$(ctx, `containerItemKey${i}`); -+ if (itemKey === key) { -+ return i; - } +- } - const completion = ensureInitialScrollSessionCompletion(state); - completion.watchdog = watchdog ? { - startScroll: watchdog.startScroll, @@ -6014,7 +5458,7 @@ index 95465f2..9ac6d3f 100644 - } - if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { - return clearInitialScrollSession(state); - } +- } - const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; - state.initialScrollSession = createInitialScrollSession({ - bootstrap, @@ -6023,278 +5467,295 @@ index 95465f2..9ac6d3f 100644 - previousDataLength - }); - return state.initialScrollSession; -+ return -1; - } - +-} +- -// src/utils/checkThreshold.ts -var HYSTERESIS_MULTIPLIER = 1.3; -function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { - const absDistance = Math.abs(distance); - return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; -+// src/utils/getItemSize.ts -+function getKnownOrFixedSize(ctx, key, index, data, resolved) { -+ var _a3, _b; -+ const state = ctx.state; -+ const { getFixedItemSize, getItemType } = state.props; -+ let size = key ? state.sizesKnown.get(key) : void 0; -+ if (size === void 0 && key && getFixedItemSize) { -+ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; -+ const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); -+ if (fixedSize !== void 0) { -+ size = fixedSize + ctx.scrollAxisGap; -+ state.sizesKnown.set(key, size); -+ } -+ } -+ return size; -+} -+function getKnownOrFixedItemSize(ctx, index) { -+ const key = getId(ctx.state, index); -+ return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); -+} -+function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { -+ for (let index = startIndex; index <= endIndex; index++) { -+ if (getKnownOrFixedItemSize(ctx, index) === void 0) { -+ return false; -+ } -+ } -+ return true; -+} -+function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { -+ var _a3, _b, _c, _d; -+ const state = ctx.state; -+ const { -+ sizes, -+ averageSizes, -+ props: { estimatedItemSize, getItemType }, -+ scrollingTo -+ } = state; -+ const sizeKnown = state.sizesKnown.get(key); -+ if (sizeKnown !== void 0) { -+ return sizeKnown; -+ } -+ let size; -+ const renderedSize = sizes.get(key); -+ if (preferCachedSize) { -+ if (renderedSize !== void 0) { -+ return renderedSize; -+ } -+ } -+ size = getKnownOrFixedSize(ctx, key, index, data, resolved); -+ if (size !== void 0) { -+ setSize(ctx, key, size, notifyTotalSize); -+ return size; -+ } -+ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; -+ if (useAverageSize && !scrollingTo) { -+ const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; -+ if (averageSizeForType !== void 0) { -+ size = roundSize(averageSizeForType); -+ } -+ } -+ if (size === void 0 && renderedSize !== void 0) { -+ return renderedSize; -+ } -+ if (size === void 0 && useAverageSize && scrollingTo) { -+ const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; -+ if (averageSizeForType !== void 0) { -+ size = roundSize(averageSizeForType); -+ } -+ } -+ if (size === void 0) { -+ size = estimatedItemSize + ctx.scrollAxisGap; -+ } -+ setSize(ctx, key, size, notifyTotalSize); -+ return size; -+} -+function getItemSizeAtIndex(ctx, index) { -+ if (index === void 0 || index < 0) { -+ return void 0; -+ } -+ const targetId = getId(ctx.state, index); -+ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+} -+ -+// src/core/calculateOffsetWithOffsetPosition.ts -+function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -+ var _a3; -+ const state = ctx.state; -+ const { index, viewOffset, viewPosition } = params; -+ let offset = offsetParam; -+ if (viewOffset) { -+ offset -= viewOffset; -+ } -+ if (index !== void 0) { -+ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -+ if (startOffsetAdjustment) { -+ offset += startOffsetAdjustment; -+ } -+ } -+ if (viewPosition !== void 0 && index !== void 0) { -+ const dataLength = state.props.data.length; -+ if (dataLength === 0) { -+ return offset; -+ } -+ const isOutOfBounds = index < 0 || index >= dataLength; -+ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -+ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -+ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -+ const trailingInset = getContentInsetEnd(ctx); -+ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -+ if (!isOutOfBounds && index === state.props.data.length - 1) { -+ const footerSize = peek$(ctx, "footerSize") || 0; -+ offset += footerSize; -+ } -+ } -+ return offset; -+} -+ -+// src/core/clampScrollOffset.ts -+function clampScrollOffset(ctx, offset, scrollTarget) { -+ const state = ctx.state; -+ const contentSize = getContentSize(ctx); -+ let clampedOffset = offset; -+ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { -+ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); -+ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; -+ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; -+ const maxOffset = baseMaxOffset + extraEndOffset; -+ clampedOffset = Math.min(offset, maxOffset); -+ } -+ clampedOffset = Math.max(0, clampedOffset); -+ return clampedOffset; -+} -+ -+// src/utils/checkThreshold.ts -+var HYSTERESIS_MULTIPLIER = 1.3; -+function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { -+ const absDistance = Math.abs(distance); -+ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; - } - var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { - const absDistance = Math.abs(distance); -@@ -794,642 +882,346 @@ function recalculateSettledScroll(ctx) { - } - checkThresholds(ctx); - } --var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; --var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; --var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; --function clearAdaptiveRenderExitTimeout(ctx) { -- ctx.state.scheduledWork.cancel("adaptiveRender"); -} --function scheduleAdaptiveRenderExit(ctx, exitDelay) { +-var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { +- const absDistance = Math.abs(distance); +- const within = atThreshold || threshold > 0 && absDistance <= threshold; +- const updateSnapshot = () => { +- setSnapshot({ +- atThreshold, +- contentSize: context.contentSize, +- dataLength: context.dataLength, +- scrollPosition: context.scrollPosition +- }); +- }; +- if (!wasReached) { +- if (!within) { +- return false; +- } +- onReached(distance); +- updateSnapshot(); +- return true; +- } +- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); +- if (reset) { +- setSnapshot(void 0); +- return false; +- } +- if (within) { +- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; +- if (changed) { +- updateSnapshot(); +- } +- } +- return true; +-}; +- +-// src/utils/edgeReachedGate.ts +-function resetEdgeLatch(ctx, edge) { - const state = ctx.state; -- clearAdaptiveRenderExitTimeout(ctx); -- if (exitDelay <= 0) { -- setAdaptiveRender(ctx, "normal", "scroll"); +- if (edge === "start") { +- state.isStartReached = false; +- state.startReachedSnapshot = void 0; - } else { -- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); +- state.isEndReached = false; +- state.endReachedSnapshot = void 0; - } -} --function setAdaptiveRender(ctx, mode, reason) { -- var _a3, _b; -- const previousMode = peek$(ctx, "adaptiveRender"); -- if (previousMode !== mode) { -- set$(ctx, "adaptiveRender", mode); -- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); +-function resetSharedEdgeGateIfOutsideHysteresis(ctx) { +- const state = ctx.state; +- if (!state.edgeReachedGate) { +- return; - } --} --function resetAdaptiveRender(ctx) { -+ -+// src/core/finishScrollTo.ts -+function finishScrollTo(ctx) { - var _a3, _b; -- clearAdaptiveRenderExitTimeout(ctx); -- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; -- if (peek$(ctx, "adaptiveRender") !== mode) { -- setAdaptiveRender(ctx, mode, "initial"); +- const contentSize = getContentSize(ctx); +- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); +- const isContentLess = contentSize < state.scrollLength; +- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; +- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; +- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); +- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); +- if (isOutsideStart && isOutsideEnd) { +- state.edgeReachedGate = void 0; - } -} --function updateAdaptiveRender(ctx, scrollVelocity, options) { -- var _a3, _b, _c; - const state = ctx.state; -- const adaptiveRender = state.props.adaptiveRender; -- const currentMode = peek$(ctx, "adaptiveRender"); -- if (peek$(ctx, "readyToRender")) { -- if (adaptiveRender) { -- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; -- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; -- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; -- const threshold = currentMode === "light" ? exitVelocity : enterVelocity; -- const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; -- const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; -- if (nextMode !== previousMode) { -- if (nextMode === "light") { -- setAdaptiveRender(ctx, "light", "scroll"); -- scheduleAdaptiveRenderExit(ctx, exitDelay); -- } else if (currentMode === "light") { -- scheduleAdaptiveRenderExit(ctx, exitDelay); -- } -- } -- } else { -- resetAdaptiveRender(ctx); -+ if (state == null ? void 0 : state.scrollingTo) { -+ cancelScrollCompletionChecks(state); -+ const resolvePendingScroll = state.pendingScrollResolve; -+ state.pendingScrollResolve = void 0; -+ const scrollingTo = state.scrollingTo; -+ state.scrollHistory.length = 0; -+ state.scrollingTo = void 0; -+ state.scrollTargetPinnedRange = void 0; -+ if (state.pendingTotalSize !== void 0) { -+ addTotalSize(ctx, null, state.pendingTotalSize); -+ } -+ { -+ state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); -+ } -+ if (scrollingTo.isInitialScroll || state.initialScroll) { -+ const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; -+ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; -+ finishInitialScroll(ctx, { -+ onFinished: () => { -+ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); -+ }, -+ preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, -+ recalculateItems: true, -+ schedulePreservedTargetClear: shouldPreserveResizeTarget, -+ syncObservedOffset: isOffsetSession, -+ waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame -+ }); -+ return; - } -+ recalculateSettledScroll(ctx); -+ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); - } - } - --// src/utils/getEffectiveDrawDistance.ts --var INITIAL_DRAW_DISTANCE = 50; --function getEffectiveDrawDistance(ctx, mode) { -- var _a3; -- const drawDistance = ctx.state.props.drawDistance; -- const initialScroll = ctx.state.initialScroll; -- const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; -- const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; -- return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; +-function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { +- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; -} --function scheduleFullDrawDistancePrewarm(ctx) { -- const { state } = ctx; -- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { -+// src/core/doScrollTo.ts -+var SCROLL_END_IDLE_MS = 80; -+var SCROLL_END_MAX_MS = 1500; -+var SMOOTH_SCROLL_DURATION_MS = 320; -+var SCROLL_END_TARGET_EPSILON = 1; -+function doScrollTo(ctx, params) { -+ var _a3, _b; -+ const state = ctx.state; -+ const { animated, horizontal, offset } = params; -+ state.scheduledWork.cancel("platformScrollCompletion"); -+ const scroller = state.refScroller.current; -+ const node = scroller == null ? void 0 : scroller.getScrollableNode(); -+ if (!scroller || !node) { - return; - } +-function markReachedEdge(ctx) { +- ctx.state.edgeReachedGate = "closed"; +-} +-function prepareReachedEdgeForNextUserScroll(ctx) { +- if (ctx.state.edgeReachedGate) { +- ctx.state.edgeReachedGate = "prepared"; +- } +-} +-function beginReachedEdgeUserScroll(ctx, scrollDelta) { +- const state = ctx.state; +- if (state.edgeReachedGate !== "prepared") { +- return void 0; +- } +- const allowedEdge = scrollDelta < 0 ? "start" : "end"; +- state.edgeReachedGate = "closed"; +- resetEdgeLatch(ctx, allowedEdge); +- return allowedEdge; +-} +- +-// src/utils/hasActiveInitialScroll.ts +-function hasActiveInitialScroll(state) { +- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; +-} +- +-// src/utils/checkAtBottom.ts +-function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { +- var _a3; +- const state = ctx.state; +- if (!state) { +- return; +- } +- const { +- queuedInitialLayout, +- scrollLength, +- scroll, +- maintainingScrollAtEnd, +- props: { maintainScrollAtEndThreshold, onEndReachedThreshold } +- } = state; +- const contentSize = getContentSize(ctx); +- resetSharedEdgeGateIfOutsideHysteresis(ctx); +- if (contentSize > 0 && queuedInitialLayout) { +- const insetEnd = getContentInsetEnd(ctx); +- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; +- const isContentLess = contentSize < scrollLength; +- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); +- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); +- set$( +- ctx, +- "isWithinMaintainScrollAtEndThreshold", +- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength +- ); +- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; +- if (!shouldSkipThresholdChecks) { +- state.isEndReached = checkThreshold( +- distanceFromEnd, +- isContentLess, +- onEndReachedThreshold * scrollLength, +- state.isEndReached, +- state.endReachedSnapshot, +- { +- contentSize, +- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, +- scrollPosition: scroll +- }, +- (distance) => { +- var _a4, _b; +- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { +- markReachedEdge(ctx); +- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); +- } +- }, +- (snapshot) => { +- state.endReachedSnapshot = snapshot; +- } +- ); +- } +- } +-} +- +-// src/utils/checkAtTop.ts +-function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { +- const state = ctx == null ? void 0 : ctx.state; +- if (!state) { +- return; +- } +- const { +- isStartReached, +- props: { data, onStartReachedThreshold }, +- scroll, +- scrollLength, +- startReachedSnapshot, +- totalSize +- } = state; +- const dataLength = data.length; +- const threshold = onStartReachedThreshold * scrollLength; +- resetSharedEdgeGateIfOutsideHysteresis(ctx); +- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { +- state.isStartReached = false; +- state.startReachedSnapshot = void 0; +- } +- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); +- set$(ctx, "isNearStart", scroll <= threshold); +- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; +- if (!shouldSkipThresholdChecks) { +- state.isStartReached = checkThreshold( +- scroll, +- false, +- threshold, +- state.isStartReached, +- startReachedSnapshot, +- { +- contentSize: totalSize, +- dataLength, +- scrollPosition: scroll +- }, +- (distance) => { +- var _a3, _b; +- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { +- markReachedEdge(ctx); +- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); +- } +- }, +- (snapshot) => { +- state.startReachedSnapshot = snapshot; +- } +- ); +- } +-} +- +-// src/utils/checkThresholds.ts +-function checkThresholds(ctx, allowedEdge) { +- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; +- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); +- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); +-} +- +-// src/core/recalculateSettledScroll.ts +-function recalculateSettledScroll(ctx) { +- var _a3, _b; +- const state = ctx.state; +- if ((_a3 = state.props) == null ? void 0 : _a3.data) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); +- } +- checkThresholds(ctx); +-} +-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; +-function clearAdaptiveRenderExitTimeout(ctx) { +- ctx.state.scheduledWork.cancel("adaptiveRender"); +-} +-function scheduleAdaptiveRenderExit(ctx, exitDelay) { +- const state = ctx.state; +- clearAdaptiveRenderExitTimeout(ctx); +- if (exitDelay <= 0) { +- setAdaptiveRender(ctx, "normal", "scroll"); +- } else { +- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); +- } +-} +-function setAdaptiveRender(ctx, mode, reason) { +- var _a3, _b; +- const previousMode = peek$(ctx, "adaptiveRender"); +- if (previousMode !== mode) { +- set$(ctx, "adaptiveRender", mode); +- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); +- } +-} +-function resetAdaptiveRender(ctx) { +- var _a3, _b; +- clearAdaptiveRenderExitTimeout(ctx); +- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; +- if (peek$(ctx, "adaptiveRender") !== mode) { +- setAdaptiveRender(ctx, mode, "initial"); +- } +-} +-function updateAdaptiveRender(ctx, scrollVelocity, options) { +- var _a3, _b, _c; +- const state = ctx.state; +- const adaptiveRender = state.props.adaptiveRender; +- const currentMode = peek$(ctx, "adaptiveRender"); +- if (peek$(ctx, "readyToRender")) { +- if (adaptiveRender) { +- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; +- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; +- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; +- const threshold = currentMode === "light" ? exitVelocity : enterVelocity; +- const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; +- const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; +- if (nextMode !== previousMode) { +- if (nextMode === "light") { +- setAdaptiveRender(ctx, "light", "scroll"); +- scheduleAdaptiveRenderExit(ctx, exitDelay); +- } else if (currentMode === "light") { +- scheduleAdaptiveRenderExit(ctx, exitDelay); +- } +- } +- } else { +- resetAdaptiveRender(ctx); +- } +- } +-} +- +-// src/utils/getEffectiveDrawDistance.ts +-var INITIAL_DRAW_DISTANCE = 50; +-function getEffectiveDrawDistance(ctx, mode) { +- var _a3; +- const drawDistance = ctx.state.props.drawDistance; +- const initialScroll = ctx.state.initialScroll; +- const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; +- const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; +- return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; +-} +-function scheduleFullDrawDistancePrewarm(ctx) { +- const { state } = ctx; +- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { +- return; +- } - state.scheduledWork.frame(() => { - var _a3; - return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); @@ -6313,35 +5774,10 @@ index 95465f2..9ac6d3f 100644 - } - if (resetInitialScroll) { - state.didFinishInitialScroll = false; -+ const isAnimated = !!animated; -+ const isHorizontal = !!horizontal; -+ const contentSize = isHorizontal ? getContentSize(ctx) : void 0; -+ const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; -+ const top = isHorizontal ? 0 : offset; -+ scroller.scrollTo({ animated: isAnimated, x: left, y: top }); -+ if (isAnimated) { -+ const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; -+ listenForScrollEnd(ctx, { -+ readOffset: () => scroller.getCurrentScrollOffset(), -+ target, -+ targetOffset: offset -+ }); -+ } else { -+ state.scroll = offset; -+ const targetToken = state.scrollingTo; -+ state.scheduledWork.timeout( -+ () => { -+ if (targetToken === state.scrollingTo) { -+ finishScrollTo(ctx); -+ } -+ }, -+ 100, -+ "platformScrollCompletion" -+ ); - } +- } - set$(ctx, "readyToRender", false); - resetAdaptiveRender(ctx); - } +-} -function setInitialRenderState(ctx, { - didLayout, - didInitialScroll @@ -6370,12 +5806,7 @@ index 95465f2..9ac6d3f 100644 - onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); - } - } -+function listenForScrollEnd(ctx, params) { -+ const { readOffset, target, targetOffset } = params; -+ if (!target) { -+ finishScrollTo(ctx); -+ return; - } +- } -} - -// src/core/finishInitialScroll.ts @@ -6401,23 +5832,12 @@ index 95465f2..9ac6d3f 100644 - if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { - if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { - cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -+ const supportsScrollEnd = "onscrollend" in target; -+ let idleTimeout; -+ let settled = false; -+ const { scheduledWork, scrollingTo: targetToken } = ctx.state; -+ const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); -+ const cleanup = () => { -+ target.removeEventListener("scroll", onScroll2); -+ if (supportsScrollEnd) { -+ target.removeEventListener("scrollend", onScrollEnd); - } +- } - if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { - cancelScrollCompletionChecks(state); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; -+ if (idleTimeout !== void 0) { -+ clearTimeout(idleTimeout); - } +- } - initialScrollCompletion.resetFlags(state); - setInitialScrollSession(state, { bootstrap: null }); - finishInitialScroll(ctx); @@ -6432,13 +5852,7 @@ index 95465f2..9ac6d3f 100644 - const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); - if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { - syncInitialScrollOffset(state, observedOffset); -+ clearTimeout(maxTimeout); -+ }; -+ const cancel = () => { -+ if (!settled) { -+ settled = true; -+ cleanup(); - } +- } - } - const complete = () => { - var _a4, _b2, _c2, _d, _e; @@ -6464,422 +5878,290 @@ index 95465f2..9ac6d3f 100644 - } - } else { - clearPreservedInitialScrollTarget(state); -+ }; -+ const finish = (reason) => { -+ if (settled) return; -+ if (targetToken !== ctx.state.scrollingTo) { -+ scheduledWork.cancel("platformScrollCompletion"); -+ return; - } +- } - if (options == null ? void 0 : options.recalculateItems) { - recalculateSettledScroll(ctx); -+ const currentOffset = readOffset(); -+ const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; -+ if (reason === "scrollend" && !isNearTarget) { -+ return; - } +- } - setInitialRenderState(ctx, { didInitialScroll: true }); - if (shouldReleaseDeferredPublicOnScroll) { - releaseDeferredPublicOnScroll(ctx, finalScrollOffset); -+ scheduledWork.cancel("platformScrollCompletion"); -+ finishScrollTo(ctx); -+ }; -+ const onScroll2 = () => { -+ if (idleTimeout !== void 0) { -+ clearTimeout(idleTimeout); - } +- } - (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); -+ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); - }; +- }; - if (options == null ? void 0 : options.waitForCompletionFrame) { - requestAnimationFrame(complete); - return; -+ const onScrollEnd = () => finish("scrollend"); -+ target.addEventListener("scroll", onScroll2); -+ if (supportsScrollEnd) { -+ target.addEventListener("scrollend", onScrollEnd); -+ } else { -+ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); - } +- } - complete(); -+ scheduledWork.register("platformScrollCompletion", cancel); - } - +-} +- -// src/core/calculateOffsetForIndex.ts -function calculateOffsetForIndex(ctx, index) { - const state = ctx.state; - return index !== void 0 ? state.positions[index] || 0 : 0; -+// src/core/initialScrollSession.ts -+var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -+function hasInitialScrollSessionCompletion(completion) { -+ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); - } -- --// src/core/getStartOffsetAdjustment.ts --function getStartOffsetAdjustment(ctx) { -- const { state } = ctx; -- const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; -- return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); -+function clearInitialScrollSession(state) { -+ state.initialScrollSession = void 0; -+ return void 0; - } -- --// src/utils/getId.ts --function getId(state, index) { -- const { data, keyExtractor } = state.props; -- if (!data) { -- return ""; -- } -- const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; -- const id = ret; -- state.idCache[index] = id; -- return id; -+function createInitialScrollSession(options) { -+ const { bootstrap, completion, kind, previousDataLength } = options; -+ return kind === "offset" ? { -+ completion, -+ kind, -+ previousDataLength -+ } : { -+ bootstrap, -+ completion, -+ kind, -+ previousDataLength -+ }; - } -- --// src/core/updateContentMetricsState.ts --function getRawContentLength(ctx) { -- var _a3, _b, _c; -- const { state, values } = ctx; -- return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); -+function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { -+ var _a4, _b2; -+ if (!state.initialScrollSession) { -+ state.initialScrollSession = createInitialScrollSession({ -+ completion: {}, -+ kind, -+ previousDataLength: 0 -+ }); -+ } else if (state.initialScrollSession.kind !== kind) { -+ state.initialScrollSession = createInitialScrollSession({ -+ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, -+ completion: state.initialScrollSession.completion, -+ kind, -+ previousDataLength: state.initialScrollSession.previousDataLength -+ }); -+ } -+ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; -+ return state.initialScrollSession.completion; - } --function getAlignItemsAtEndPadding(ctx) { -- const { state } = ctx; -- const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; -- return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; --} --function updateContentMetricsState(ctx) { -- const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; -- const nextPadding = getAlignItemsAtEndPadding(ctx); -- if (previousPadding !== nextPadding) { -- set$(ctx, "alignItemsAtEndPadding", nextPadding); -- } --} -- --// src/core/addTotalSize.ts --function addTotalSize(ctx, key, add, notifyTotalSize = true) { -- const state = ctx.state; -- const prevTotalSize = state.totalSize; -- let totalSize = state.totalSize; -- if (key === null) { -- totalSize = add; -- if (state.timeoutSetPaddingTop) { -- clearTimeout(state.timeoutSetPaddingTop); -- state.timeoutSetPaddingTop = void 0; -+var initialScrollCompletion = { -+ didDispatchNativeScroll(state) { -+ var _a3, _b; -+ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); -+ }, -+ didRetrySilentInitialScroll(state) { -+ var _a3, _b; -+ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); -+ }, -+ markInitialScrollNativeDispatch(state) { -+ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; -+ }, -+ markSilentInitialScrollRetry(state) { -+ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; -+ }, -+ resetFlags(state) { -+ if (!state.initialScrollSession) { -+ return; - } -- } else { -- totalSize += add; -+ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); -+ completion.didDispatchNativeScroll = void 0; -+ completion.didRetrySilentInitialScroll = void 0; - } -- if (prevTotalSize !== totalSize) { -- { -- state.pendingTotalSize = void 0; -- state.totalSize = totalSize; -- if (notifyTotalSize) { -- set$(ctx, "totalSize", totalSize); -- } -- updateContentMetricsState(ctx); -+}; -+var initialScrollWatchdog = { -+ clear(state) { -+ initialScrollWatchdog.set(state, void 0); -+ }, -+ didReachTarget(newScroll, watchdog) { -+ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); -+ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ get(state) { -+ var _a3, _b; -+ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; -+ }, -+ hasNonZeroTargetOffset(targetOffset) { -+ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ isAtZeroTargetOffset(targetOffset) { -+ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ set(state, watchdog) { -+ var _a3, _b; -+ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { -+ return; - } -- } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { -- set$(ctx, "totalSize", totalSize); -+ const completion = ensureInitialScrollSessionCompletion(state); -+ completion.watchdog = watchdog ? { -+ startScroll: watchdog.startScroll, -+ targetOffset: watchdog.targetOffset -+ } : void 0; - } --} -- --// src/core/setSize.ts --function setSize(ctx, itemKey, size, notifyTotalSize = true) { -- const state = ctx.state; -- const { sizes } = state; -- const previousSize = sizes.get(itemKey); -- const diff = previousSize !== void 0 ? size - previousSize : size; -- if (diff !== 0) { -- addTotalSize(ctx, itemKey, diff, notifyTotalSize); -+}; -+function setInitialScrollSession(state, options = {}) { -+ var _a3, _b, _c, _d; -+ const existingSession = state.initialScrollSession; -+ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; -+ const completion = existingSession == null ? void 0 : existingSession.completion; -+ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; -+ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; -+ if (!kind) { -+ return clearInitialScrollSession(state); - } -- sizes.set(itemKey, size); -} - --// src/utils/helpers.ts --function isFunction(obj) { -- return typeof obj === "function"; --} --function isArray(obj) { -- return Array.isArray(obj); --} --var warned = /* @__PURE__ */ new Set(); --function warnDevOnce(id, text) { -- if (IS_DEV && !warned.has(id)) { -- warned.add(id); -- console.warn(`[legend-list] ${text}`); -+ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { -+ return clearInitialScrollSession(state); + // src/core/getStartOffsetAdjustment.ts + function getStartOffsetAdjustment(ctx) { + const { state } = ctx; +@@ -1213,58 +614,273 @@ function getItemSizeAtIndex(ctx, index) { + if (index === void 0 || index < 0) { + return void 0; } -+ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; -+ state.initialScrollSession = createInitialScrollSession({ -+ bootstrap, -+ completion, -+ kind, -+ previousDataLength -+ }); -+ return state.initialScrollSession; - } --function roundSize(size) { -- return Math.floor(size * 8) / 8; --} --function isNullOrUndefined(value) { -- return value === null || value === void 0; --} --function getPadding(s, type) { -- var _a3, _b, _c; -- const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; -- return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; -+var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; -+var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; -+var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; -+function clearAdaptiveRenderExitTimeout(ctx) { -+ ctx.state.scheduledWork.cancel("adaptiveRender"); - } --function extractPadding(style, contentContainerStyle, type) { -- return getPadding(style, type) + getPadding(contentContainerStyle, type); -+function scheduleAdaptiveRenderExit(ctx, exitDelay) { +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; + const state = ctx.state; -+ clearAdaptiveRenderExitTimeout(ctx); -+ if (exitDelay <= 0) { -+ setAdaptiveRender(ctx, "normal", "scroll"); -+ } else { -+ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; + } - } --function findContainerId(ctx, key) { -+function setAdaptiveRender(ctx, mode, reason) { - var _a3, _b; -- const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); -- if (directMatch !== void 0) { -- return directMatch; -- } -- const numContainers = peek$(ctx, "numContainers"); -- for (let i = 0; i < numContainers; i++) { -- const itemKey = peek$(ctx, `containerItemKey${i}`); -- if (itemKey === key) { -- return i; -- } -+ const previousMode = peek$(ctx, "adaptiveRender"); -+ if (previousMode !== mode) { -+ set$(ctx, "adaptiveRender", mode); -+ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); - } -- return -1; - } -- --// src/utils/getItemSize.ts --function getKnownOrFixedSize(ctx, key, index, data, resolved) { -+function resetAdaptiveRender(ctx) { - var _a3, _b; -- const state = ctx.state; -- const { getFixedItemSize, getItemType } = state.props; -- let size = key ? state.sizesKnown.get(key) : void 0; -- if (size === void 0 && key && getFixedItemSize) { -- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; -- const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); -- if (fixedSize !== void 0) { -- size = fixedSize + ctx.scrollAxisGap; -- state.sizesKnown.set(key, size); -- } -+ clearAdaptiveRenderExitTimeout(ctx); -+ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; -+ if (peek$(ctx, "adaptiveRender") !== mode) { -+ setAdaptiveRender(ctx, mode, "initial"); - } -- return size; --} --function getKnownOrFixedItemSize(ctx, index) { -- const key = getId(ctx.state, index); -- return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); - } --function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { -- for (let index = startIndex; index <= endIndex; index++) { -- if (getKnownOrFixedItemSize(ctx, index) === void 0) { -- return false; -+function updateAdaptiveRender(ctx, scrollVelocity, options) { -+ var _a3, _b, _c; ++ if (index !== void 0) { ++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); ++ if (startOffsetAdjustment) { ++ offset += startOffsetAdjustment; ++ } ++ } ++ if (viewPosition !== void 0 && index !== void 0) { ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return offset; ++ } ++ const isOutOfBounds = index < 0 || index >= dataLength; ++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; ++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); ++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); ++ const trailingInset = getContentInsetEnd(ctx); ++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); ++ if (!isOutOfBounds && index === state.props.data.length - 1) { ++ const footerSize = peek$(ctx, "footerSize") || 0; ++ offset += footerSize; ++ } ++ } ++ return offset; ++} ++ ++// src/core/clampScrollOffset.ts ++function clampScrollOffset(ctx, offset, scrollTarget) { + const state = ctx.state; -+ const adaptiveRender = state.props.adaptiveRender; -+ const currentMode = peek$(ctx, "adaptiveRender"); -+ if (peek$(ctx, "readyToRender")) { -+ if (adaptiveRender) { -+ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; -+ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; -+ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; -+ const threshold = currentMode === "light" ? exitVelocity : enterVelocity; -+ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; -+ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; -+ if (nextMode !== previousMode) { -+ if (nextMode === "light") { -+ setAdaptiveRender(ctx, "light", "scroll"); -+ scheduleAdaptiveRenderExit(ctx, exitDelay); -+ } else if (currentMode === "light") { -+ scheduleAdaptiveRenderExit(ctx, exitDelay); -+ } -+ } -+ } else { -+ resetAdaptiveRender(ctx); - } - } -- return true; - } --function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { -- var _a3, _b, _c, _d; ++ const contentSize = getContentSize(ctx); ++ let clampedOffset = offset; ++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { ++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); ++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; ++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; ++ const maxOffset = baseMaxOffset + extraEndOffset; ++ clampedOffset = Math.min(offset, maxOffset); ++ } ++ clampedOffset = Math.max(0, clampedOffset); ++ return clampedOffset; ++} + -+// src/core/doMaintainScrollAtEnd.ts -+function doMaintainScrollAtEnd(ctx) { - const state = ctx.state; - const { -- sizes, -- averageSizes, -- props: { estimatedItemSize, getItemType }, -- scrollingTo -+ didContainersLayout, -+ pendingNativeMVCPAdjust, -+ refScroller, -+ props: { maintainScrollAtEnd } - } = state; -- const sizeKnown = state.sizesKnown.get(key); -- if (sizeKnown !== void 0) { -- return sizeKnown; -+ const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); -+ const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout); -+ if (pendingNativeMVCPAdjust) { -+ state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd; ++// src/utils/checkThreshold.ts ++var HYSTERESIS_MULTIPLIER = 1.3; ++function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { ++ const absDistance = Math.abs(distance); ++ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; ++} ++var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { ++ const absDistance = Math.abs(distance); ++ const within = atThreshold || threshold > 0 && absDistance <= threshold; ++ const updateSnapshot = () => { ++ setSnapshot({ ++ atThreshold, ++ contentSize: context.contentSize, ++ dataLength: context.dataLength, ++ scrollPosition: context.scrollPosition ++ }); ++ }; ++ if (!wasReached) { ++ if (!within) { ++ return false; ++ } ++ onReached(distance); ++ updateSnapshot(); ++ return true; ++ } ++ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); ++ if (reset) { ++ setSnapshot(void 0); + return false; - } -- let size; -- const renderedSize = sizes.get(key); -- if (preferCachedSize) { -- if (renderedSize !== void 0) { -- return renderedSize; -- } -- } -- size = getKnownOrFixedSize(ctx, key, index, data, resolved); -- if (size !== void 0) { -- setSize(ctx, key, size, notifyTotalSize); -- return size; -- } -- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; -- if (useAverageSize && !scrollingTo) { -- const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; -- if (averageSizeForType !== void 0) { -- size = roundSize(averageSizeForType); -- } -- } -- if (size === void 0 && renderedSize !== void 0) { -- return renderedSize; -- } -- if (size === void 0 && useAverageSize && scrollingTo) { -- const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; -- if (averageSizeForType !== void 0) { -- size = roundSize(averageSizeForType); -- } -- } -- if (size === void 0) { -- size = estimatedItemSize + ctx.scrollAxisGap; -- } -- setSize(ctx, key, size, notifyTotalSize); -- return size; --} --function getItemSizeAtIndex(ctx, index) { -- if (index === void 0 || index < 0) { -- return void 0; -- } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); --} -- ++ } ++ if (within) { ++ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; ++ if (changed) { ++ updateSnapshot(); ++ } ++ } ++ return true; ++}; ++ ++// src/utils/edgeReachedGate.ts ++function resetEdgeLatch(ctx, edge) { ++ const state = ctx.state; ++ if (edge === "start") { ++ state.isStartReached = false; ++ state.startReachedSnapshot = void 0; ++ } else { ++ state.isEndReached = false; ++ state.endReachedSnapshot = void 0; ++ } ++} ++function resetSharedEdgeGateIfOutsideHysteresis(ctx) { ++ const state = ctx.state; ++ if (!state.edgeReachedGate) { ++ return; ++ } ++ const contentSize = getContentSize(ctx); ++ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); ++ const isContentLess = contentSize < state.scrollLength; ++ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; ++ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; ++ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); ++ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); ++ if (isOutsideStart && isOutsideEnd) { ++ state.edgeReachedGate = void 0; ++ } ++} ++function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { ++ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; ++} ++function markReachedEdge(ctx) { ++ ctx.state.edgeReachedGate = "closed"; ++} ++function prepareReachedEdgeForNextUserScroll(ctx) { ++ if (ctx.state.edgeReachedGate) { ++ ctx.state.edgeReachedGate = "prepared"; ++ } ++} ++function beginReachedEdgeUserScroll(ctx, scrollDelta) { ++ const state = ctx.state; ++ if (state.edgeReachedGate !== "prepared") { ++ return void 0; ++ } ++ const allowedEdge = scrollDelta < 0 ? "start" : "end"; ++ state.edgeReachedGate = "closed"; ++ resetEdgeLatch(ctx, allowedEdge); ++ return allowedEdge; ++} ++ ++// src/utils/hasActiveInitialScroll.ts ++function hasActiveInitialScroll(state) { ++ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; ++} ++ ++// src/utils/checkAtBottom.ts ++function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { ++ var _a3; ++ const state = ctx.state; ++ if (!state) { ++ return; ++ } ++ const { ++ queuedInitialLayout, ++ scrollLength, ++ scroll, ++ maintainingScrollAtEnd, ++ props: { maintainScrollAtEndThreshold, onEndReachedThreshold } ++ } = state; ++ const contentSize = getContentSize(ctx); ++ resetSharedEdgeGateIfOutsideHysteresis(ctx); ++ if (contentSize > 0 && queuedInitialLayout) { ++ const insetEnd = getContentInsetEnd(ctx); ++ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; ++ const isContentLess = contentSize < scrollLength; ++ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); ++ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); ++ set$( ++ ctx, ++ "isWithinMaintainScrollAtEndThreshold", ++ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength ++ ); ++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; ++ if (!shouldSkipThresholdChecks) { ++ state.isEndReached = checkThreshold( ++ distanceFromEnd, ++ isContentLess, ++ onEndReachedThreshold * scrollLength, ++ state.isEndReached, ++ state.endReachedSnapshot, ++ { ++ contentSize, ++ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, ++ scrollPosition: scroll ++ }, ++ (distance) => { ++ var _a4, _b; ++ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { ++ markReachedEdge(ctx); ++ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); ++ } ++ }, ++ (snapshot) => { ++ state.endReachedSnapshot = snapshot; ++ } ++ ); ++ } ++ } ++} ++ ++// src/utils/checkAtTop.ts ++function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { ++ const state = ctx == null ? void 0 : ctx.state; ++ if (!state) { ++ return; ++ } ++ const { ++ isStartReached, ++ props: { data, onStartReachedThreshold }, ++ scroll, ++ scrollLength, ++ startReachedSnapshot, ++ totalSize ++ } = state; ++ const dataLength = data.length; ++ const threshold = onStartReachedThreshold * scrollLength; ++ resetSharedEdgeGateIfOutsideHysteresis(ctx); ++ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { ++ state.isStartReached = false; ++ state.startReachedSnapshot = void 0; ++ } ++ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); ++ set$(ctx, "isNearStart", scroll <= threshold); ++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; ++ if (!shouldSkipThresholdChecks) { ++ state.isStartReached = checkThreshold( ++ scroll, ++ false, ++ threshold, ++ state.isStartReached, ++ startReachedSnapshot, ++ { ++ contentSize: totalSize, ++ dataLength, ++ scrollPosition: scroll ++ }, ++ (distance) => { ++ var _a3, _b; ++ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { ++ markReachedEdge(ctx); ++ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); ++ } ++ }, ++ (snapshot) => { ++ state.startReachedSnapshot = snapshot; ++ } ++ ); ++ } + } + -// src/core/calculateOffsetWithOffsetPosition.ts -function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { - var _a3; @@ -6912,11 +6194,19 @@ index 95465f2..9ac6d3f 100644 - } - } - return offset; --} -- ++// src/utils/checkThresholds.ts ++function checkThresholds(ctx, allowedEdge) { ++ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; ++ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); ++ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + } + -// src/core/clampScrollOffset.ts -function clampScrollOffset(ctx, offset, scrollTarget) { -- const state = ctx.state; ++// src/core/recalculateSettledScroll.ts ++function recalculateSettledScroll(ctx) { ++ var _a3, _b; + const state = ctx.state; - const contentSize = getContentSize(ctx); - let clampedOffset = offset; - if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { @@ -6925,176 +6215,200 @@ index 95465f2..9ac6d3f 100644 - const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; - const maxOffset = baseMaxOffset + extraEndOffset; - clampedOffset = Math.min(offset, maxOffset); -- } ++ if ((_a3 = state.props) == null ? void 0 : _a3.data) { ++ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); + } - clampedOffset = Math.max(0, clampedOffset); - return clampedOffset; --} -- --// src/core/finishScrollTo.ts --function finishScrollTo(ctx) { -- var _a3, _b; -- const state = ctx.state; -- if (state == null ? void 0 : state.scrollingTo) { -- cancelScrollCompletionChecks(state); -- const resolvePendingScroll = state.pendingScrollResolve; -- state.pendingScrollResolve = void 0; -- const scrollingTo = state.scrollingTo; -- state.scrollHistory.length = 0; -- state.scrollingTo = void 0; -- state.scrollTargetPinnedRange = void 0; -- if (state.pendingTotalSize !== void 0) { -- addTotalSize(ctx, null, state.pendingTotalSize); -- } -- { -- state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); -- } -- if (scrollingTo.isInitialScroll || state.initialScroll) { -- const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; -- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; -- finishInitialScroll(ctx, { -- onFinished: () => { -- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); -- }, -- preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, -- recalculateItems: true, -- schedulePreservedTargetClear: shouldPreserveResizeTarget, -- syncObservedOffset: isOffsetSession, -- waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame -- }); -- return; -- } -- recalculateSettledScroll(ctx); -- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); -- } --} -- --// src/core/doScrollTo.ts --var SCROLL_END_IDLE_MS = 80; --var SCROLL_END_MAX_MS = 1500; --var SMOOTH_SCROLL_DURATION_MS = 320; --var SCROLL_END_TARGET_EPSILON = 1; --function doScrollTo(ctx, params) { -- var _a3, _b; -- const state = ctx.state; -- const { animated, horizontal, offset } = params; -- state.scheduledWork.cancel("platformScrollCompletion"); -- const scroller = state.refScroller.current; -- const node = scroller == null ? void 0 : scroller.getScrollableNode(); -- if (!scroller || !node) { -- return; -- } -- const isAnimated = !!animated; -- const isHorizontal = !!horizontal; -- const contentSize = isHorizontal ? getContentSize(ctx) : void 0; -- const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; -- const top = isHorizontal ? 0 : offset; -- scroller.scrollTo({ animated: isAnimated, x: left, y: top }); -- if (isAnimated) { -- const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; -- listenForScrollEnd(ctx, { -- readOffset: () => scroller.getCurrentScrollOffset(), -- target, -- targetOffset: offset -- }); -- } else { -- state.scroll = offset; -- const targetToken = state.scrollingTo; -- state.scheduledWork.timeout( -- () => { -- if (targetToken === state.scrollingTo) { -- finishScrollTo(ctx); -- } -- }, -- 100, -- "platformScrollCompletion" -- ); -- } --} --function listenForScrollEnd(ctx, params) { -- const { readOffset, target, targetOffset } = params; -- if (!target) { -- finishScrollTo(ctx); -- return; -- } -- const supportsScrollEnd = "onscrollend" in target; -- let idleTimeout; -- let settled = false; -- const { scheduledWork, scrollingTo: targetToken } = ctx.state; -- const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); -- const cleanup = () => { -- target.removeEventListener("scroll", onScroll2); -- if (supportsScrollEnd) { -- target.removeEventListener("scrollend", onScrollEnd); -- } -- if (idleTimeout !== void 0) { -- clearTimeout(idleTimeout); -- } -- clearTimeout(maxTimeout); -- }; -- const cancel = () => { -- if (!settled) { -- settled = true; -- cleanup(); -- } -- }; -- const finish = (reason) => { -- if (settled) return; -- if (targetToken !== ctx.state.scrollingTo) { -- scheduledWork.cancel("platformScrollCompletion"); -- return; -- } -- const currentOffset = readOffset(); -- const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; -- if (reason === "scrollend" && !isNearTarget) { -- return; -- } -- scheduledWork.cancel("platformScrollCompletion"); -- finishScrollTo(ctx); -- }; -- const onScroll2 = () => { -- if (idleTimeout !== void 0) { -- clearTimeout(idleTimeout); -- } -- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); -- }; -- const onScrollEnd = () => finish("scrollend"); -- target.addEventListener("scroll", onScroll2); -- if (supportsScrollEnd) { -- target.addEventListener("scrollend", onScrollEnd); -- } else { -- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); -- } ++ checkThresholds(ctx); + } + + // src/core/finishScrollTo.ts +@@ -1401,7 +1017,182 @@ function listenForScrollEnd(ctx, params) { + } else { + idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); + } - scheduledWork.register("platformScrollCompletion", cancel); --} -- --// src/core/doMaintainScrollAtEnd.ts --function doMaintainScrollAtEnd(ctx) { -- const state = ctx.state; -- const { -- didContainersLayout, -- pendingNativeMVCPAdjust, -- refScroller, -- props: { maintainScrollAtEnd } -- } = state; -- const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); -- const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout); -- if (pendingNativeMVCPAdjust) { -- state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd; -- return false; -- } -- if (shouldMaintainScrollAtEnd) { -- state.pendingMaintainScrollAtEnd = false; -- const contentSize = getContentSize(ctx); -- if (contentSize < state.scrollLength) { -- state.scroll = 0; -+ if (shouldMaintainScrollAtEnd) { -+ state.pendingMaintainScrollAtEnd = false; -+ const contentSize = getContentSize(ctx); -+ if (contentSize < state.scrollLength) { -+ state.scroll = 0; - } - if (!state.maintainingScrollAtEnd) { - const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; ++ scheduledWork.register("platformScrollCompletion", cancel); ++} ++ ++// src/core/initialScrollSession.ts ++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; ++function hasInitialScrollSessionCompletion(completion) { ++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); ++} ++function clearInitialScrollSession(state) { ++ state.initialScrollSession = void 0; ++ return void 0; ++} ++function createInitialScrollSession(options) { ++ const { bootstrap, completion, kind, previousDataLength } = options; ++ return kind === "offset" ? { ++ completion, ++ kind, ++ previousDataLength ++ } : { ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }; ++} ++function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { ++ var _a4, _b2; ++ if (!state.initialScrollSession) { ++ state.initialScrollSession = createInitialScrollSession({ ++ completion: {}, ++ kind, ++ previousDataLength: 0 ++ }); ++ } else if (state.initialScrollSession.kind !== kind) { ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, ++ completion: state.initialScrollSession.completion, ++ kind, ++ previousDataLength: state.initialScrollSession.previousDataLength ++ }); ++ } ++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; ++ return state.initialScrollSession.completion; ++} ++var initialScrollCompletion = { ++ didDispatchNativeScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); ++ }, ++ didRetrySilentInitialScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); ++ }, ++ markInitialScrollNativeDispatch(state) { ++ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; ++ }, ++ markSilentInitialScrollRetry(state) { ++ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; ++ }, ++ resetFlags(state) { ++ if (!state.initialScrollSession) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); ++ completion.didDispatchNativeScroll = void 0; ++ completion.didRetrySilentInitialScroll = void 0; ++ } ++}; ++var initialScrollWatchdog = { ++ clear(state) { ++ initialScrollWatchdog.set(state, void 0); ++ }, ++ didReachTarget(newScroll, watchdog) { ++ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); ++ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ get(state) { ++ var _a3, _b; ++ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; ++ }, ++ hasNonZeroTargetOffset(targetOffset) { ++ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ isAtZeroTargetOffset(targetOffset) { ++ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ set(state, watchdog) { ++ var _a3, _b; ++ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state); ++ completion.watchdog = watchdog ? { ++ startScroll: watchdog.startScroll, ++ targetOffset: watchdog.targetOffset ++ } : void 0; ++ } ++}; ++function setInitialScrollSession(state, options = {}) { ++ var _a3, _b, _c, _d; ++ const existingSession = state.initialScrollSession; ++ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; ++ const completion = existingSession == null ? void 0 : existingSession.completion; ++ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; ++ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; ++ if (!kind) { ++ return clearInitialScrollSession(state); ++ } ++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { ++ return clearInitialScrollSession(state); ++ } ++ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }); ++ return state.initialScrollSession; ++} ++var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; ++function clearAdaptiveRenderExitTimeout(ctx) { ++ ctx.state.scheduledWork.cancel("adaptiveRender"); ++} ++function scheduleAdaptiveRenderExit(ctx, exitDelay) { ++ const state = ctx.state; ++ clearAdaptiveRenderExitTimeout(ctx); ++ if (exitDelay <= 0) { ++ setAdaptiveRender(ctx, "normal", "scroll"); ++ } else { ++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); ++ } ++} ++function setAdaptiveRender(ctx, mode, reason) { ++ var _a3, _b; ++ const previousMode = peek$(ctx, "adaptiveRender"); ++ if (previousMode !== mode) { ++ set$(ctx, "adaptiveRender", mode); ++ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); ++ } ++} ++function resetAdaptiveRender(ctx) { ++ var _a3, _b; ++ clearAdaptiveRenderExitTimeout(ctx); ++ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; ++ if (peek$(ctx, "adaptiveRender") !== mode) { ++ setAdaptiveRender(ctx, mode, "initial"); ++ } ++} ++function updateAdaptiveRender(ctx, scrollVelocity, options) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ const adaptiveRender = state.props.adaptiveRender; ++ const currentMode = peek$(ctx, "adaptiveRender"); ++ if (peek$(ctx, "readyToRender")) { ++ if (adaptiveRender) { ++ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; ++ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; ++ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; ++ const threshold = currentMode === "light" ? exitVelocity : enterVelocity; ++ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; ++ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; ++ if (nextMode !== previousMode) { ++ if (nextMode === "light") { ++ setAdaptiveRender(ctx, "light", "scroll"); ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } else if (currentMode === "light") { ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } ++ } ++ } else { ++ resetAdaptiveRender(ctx); ++ } ++ } + } + + // src/core/doMaintainScrollAtEnd.ts +@@ -1430,6 +1221,7 @@ function doMaintainScrollAtEnd(ctx) { const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; state.maintainingScrollAtEnd = pendingState; @@ -7102,27 +6416,10 @@ index 95465f2..9ac6d3f 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1788,23 +1580,44 @@ function prepareMVCP(ctx, dataChanged) { +@@ -1788,6 +1580,27 @@ function prepareMVCP(ctx, dataChanged) { } } --// src/utils/getScrollVelocity.ts --var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; --var SCROLL_VELOCITY_HALF_LIFE_MS = 200; --var getScrollVelocity = (state) => { -- const { scrollHistory } = state; -- const newestIndex = scrollHistory.length - 1; -- if (newestIndex < 1) { -- return 0; -- } -- const newest = scrollHistory[newestIndex]; -- if (Date.now() - newest.time > MAX_SCROLL_VELOCITY_WINDOW_MS) { -- return 0; -- } -- let direction = 0; -- let weightedVelocity = 0; -- let totalWeight = 0; -- for (let i = newestIndex; i > 0; i--) { +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { @@ -7144,269 +6441,39 @@ index 95465f2..9ac6d3f 100644 + }, "fullDrawDistancePrewarm"); +} + -+// src/utils/getScrollVelocity.ts -+var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; -+var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -+var getScrollVelocity = (state) => { -+ const { scrollHistory } = state; -+ const newestIndex = scrollHistory.length - 1; -+ if (newestIndex < 1) { -+ return 0; -+ } -+ const newest = scrollHistory[newestIndex]; -+ if (Date.now() - newest.time > MAX_SCROLL_VELOCITY_WINDOW_MS) { -+ return 0; -+ } -+ let direction = 0; -+ let weightedVelocity = 0; -+ let totalWeight = 0; -+ for (let i = newestIndex; i > 0; i--) { - const current = scrollHistory[i]; - const previous = scrollHistory[i - 1]; - const scrollDiff = current.scroll - previous.scroll; -@@ -1823,281 +1636,604 @@ var getScrollVelocity = (state) => { - if (scrollDiff === 0 || timeDiff <= 0) { - continue; + // src/utils/getScrollVelocity.ts + var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; + var SCROLL_VELOCITY_HALF_LIFE_MS = 200; +@@ -2034,70 +1847,395 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { + if (nextTop === void 0 || nextTop > viewportEnd) { + break; } -- const age = newest.time - current.time; -- const weight = Math.exp(-age / SCROLL_VELOCITY_HALF_LIFE_MS); -- weightedVelocity += scrollDiff / timeDiff * weight; -- totalWeight += weight; +- end++; - } -- return totalWeight > 0 ? weightedVelocity / totalWeight : 0; --}; -- --// src/utils/hasActiveMVCPAnchorLock.ts --function hasActiveMVCPAnchorLock(state) { -- const lock = state.mvcpAnchorLock; -- if (!lock) { -- return false; -+ const age = newest.time - current.time; -+ const weight = Math.exp(-age / SCROLL_VELOCITY_HALF_LIFE_MS); -+ weightedVelocity += scrollDiff / timeDiff * weight; -+ totalWeight += weight; -+ } -+ return totalWeight > 0 ? weightedVelocity / totalWeight : 0; -+}; -+ -+// src/utils/hasActiveMVCPAnchorLock.ts -+function hasActiveMVCPAnchorLock(state) { -+ const lock = state.mvcpAnchorLock; -+ if (!lock) { -+ return false; -+ } -+ if (Date.now() > lock.expiresAt) { -+ state.mvcpAnchorLock = void 0; -+ return false; +- return { end, start }; +-} +-function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); +- if (range) { +- ctx.state.scrollTargetPinnedRange = range; +- ctx.state.scrollForNextCalculateItemsInView = void 0; +- } else { +- ctx.state.scrollTargetPinnedRange = void 0; ++ end++; + } -+ return true; ++ return { end, start }; +} -+ -+// src/utils/isInMVCPActiveMode.ts -+function isInMVCPActiveMode(state) { -+ return state.dataChangeNeedsScrollUpdate || hasActiveMVCPAnchorLock(state); ++function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { ++ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); ++ if (range) { ++ ctx.state.scrollTargetPinnedRange = range; ++ ctx.state.scrollForNextCalculateItemsInView = void 0; ++ } else { ++ ctx.state.scrollTargetPinnedRange = void 0; ++ } +} -+ -+// src/core/updateScroll.ts -+function updateScroll(ctx, newScroll, forceUpdate, options) { -+ var _a3; -+ const state = ctx.state; -+ const { ignoreScrollFromMVCP, lastScrollAdjustForHistory, scrollAdjustHandler, scrollHistory, scrollingTo } = state; -+ const prevScroll = state.scroll; -+ if ((options == null ? void 0 : options.markHasScrolled) !== false) { -+ state.hasScrolled = true; -+ } -+ const currentTime = Date.now(); -+ state.lastBatchingAction = currentTime; -+ const adjust = scrollAdjustHandler.getAdjust(); -+ const adjustChanged = lastScrollAdjustForHistory !== void 0 && Math.abs(adjust - lastScrollAdjustForHistory) > 0.1; -+ if (adjustChanged) { -+ scrollHistory.length = 0; -+ } -+ state.lastScrollAdjustForHistory = adjust; -+ if (scrollingTo === void 0 && !(scrollHistory.length === 0 && newScroll === state.scroll)) { -+ if (!adjustChanged) { -+ scrollHistory.push({ scroll: newScroll, time: currentTime }); -+ } -+ } -+ if (scrollHistory.length > 5) { -+ scrollHistory.shift(); -+ } -+ if (ignoreScrollFromMVCP && !scrollingTo) { -+ const { lt, gt } = ignoreScrollFromMVCP; -+ if (lt && newScroll < lt || gt && newScroll > gt) { -+ state.ignoreScrollFromMVCPIgnored = true; -+ return; -+ } -+ } -+ state.scrollPrev = prevScroll; -+ state.scrollPrevTime = state.scrollTime; -+ state.scroll = newScroll; -+ state.scrollTime = currentTime; -+ const scrollDelta = Math.abs(newScroll - prevScroll); -+ const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; -+ const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; -+ const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); -+ const scrollLength = state.scrollLength; -+ const isLargeUserScrollJump = scrollLength > 0 && scrollingTo === void 0 && scrollDelta > scrollLength && !state.pendingNativeMVCPAdjust; -+ const scrollVelocity = getScrollVelocity(state); -+ updateAdaptiveRender(ctx, scrollVelocity, { forceLight: isLargeUserScrollJump }); -+ const lastCalculated = state.scrollLastCalculate; -+ const useAggressiveItemRecalculation = isInMVCPActiveMode(state); -+ const shouldUpdate = useAggressiveItemRecalculation || didResolvePendingNativeMVCPAdjust || allowedEdge !== void 0 || forceUpdate || lastCalculated === void 0 || Math.abs(state.scroll - lastCalculated) > 2; -+ if (shouldUpdate) { -+ state.scrollLastCalculate = state.scroll; -+ state.ignoreScrollFromMVCPIgnored = false; -+ state.lastScrollDelta = scrollDelta; -+ const runCalculateItems = () => { -+ var _a4; -+ const calculateItemsParams = { -+ doMVCP: scrollingTo !== void 0, -+ scrollVelocity -+ }; -+ if (isLargeUserScrollJump) { -+ calculateItemsParams.drawDistanceMode = "visible-first"; -+ } -+ (_a4 = state.triggerCalculateItemsInView) == null ? void 0 : _a4.call(state, calculateItemsParams); -+ checkThresholds(ctx, allowedEdge); -+ }; -+ if (isLargeUserScrollJump) { -+ state.mvcpAnchorLock = void 0; -+ state.pendingNativeMVCPAdjust = void 0; -+ state.userScrollAnchorReset = { keys: /* @__PURE__ */ new Set() }; -+ state.scheduledWork.cancel("mvcpRecalculate"); -+ flushSync(runCalculateItems); -+ scheduleFullDrawDistancePrewarm(ctx); -+ } else { -+ runCalculateItems(); -+ } -+ const shouldMaintainScrollAtEndAfterPendingSettle = !!state.pendingMaintainScrollAtEnd || !!((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onDataChange); -+ if (didResolvePendingNativeMVCPAdjust && shouldMaintainScrollAtEndAfterPendingSettle) { -+ state.pendingMaintainScrollAtEnd = false; -+ doMaintainScrollAtEnd(ctx); -+ } -+ state.dataChangeNeedsScrollUpdate = false; -+ state.lastScrollDelta = 0; -+ } -+} -+ -+// src/core/scrollTo.ts -+function getAverageSizeSnapshot(state) { -+ if (Object.keys(state.averageSizes).length === 0) { -+ return void 0; -+ } -+ const snapshot = {}; -+ for (const itemType in state.averageSizes) { -+ const averages = state.averageSizes[itemType]; -+ snapshot[itemType] = averages.avg; -+ } -+ return snapshot; -+} -+function syncInitialScrollNativeWatchdog(state, options) { -+ var _a3; -+ const { isInitialScroll, requestedOffset, targetOffset } = options; -+ const existingWatchdog = initialScrollWatchdog.get(state); -+ const shouldWatchInitialNativeScroll = !state.didFinishInitialScroll && (isInitialScroll || !!existingWatchdog) && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); -+ const shouldClearInitialNativeScrollWatchdog = !state.didFinishInitialScroll && !!existingWatchdog && initialScrollWatchdog.isAtZeroTargetOffset(requestedOffset); -+ if (shouldWatchInitialNativeScroll) { -+ state.hasScrolled = false; -+ initialScrollWatchdog.set(state, { -+ startScroll: (_a3 = existingWatchdog == null ? void 0 : existingWatchdog.startScroll) != null ? _a3 : state.scroll, -+ targetOffset -+ }); -+ return; -+ } -+ if (shouldClearInitialNativeScrollWatchdog) { -+ initialScrollWatchdog.clear(state); -+ } -+} -+function findPositionIndexAtOrBeforeOffset(ctx, offset) { -+ const state = ctx.state; -+ const dataLength = state.props.data.length; -+ let low = 0; -+ let high = dataLength - 1; -+ let match; -+ while (low <= high) { -+ const mid = Math.floor((low + high) / 2); -+ const top = state.positions[mid]; -+ if (top === void 0) { -+ high = mid - 1; -+ } else { -+ if (top <= offset) { -+ match = mid; -+ low = mid + 1; -+ } else { -+ high = mid - 1; -+ } -+ } -+ } -+ return match; -+} -+function getItemBottom(ctx, index) { -+ var _a3; -+ const top = ctx.state.positions[index]; -+ if (top === void 0) { -+ return void 0; -+ } -+ const itemSize = (_a3 = getItemSizeAtIndex(ctx, index)) != null ? _a3 : 0; -+ return top + (Number.isFinite(itemSize) ? itemSize : 0); -+} -+function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { -+ const state = ctx.state; -+ const dataLength = state.props.data.length; -+ if (dataLength === 0) { -+ return void 0; -+ } -+ const viewportStart = Math.max(0, targetOffset); -+ const viewportEnd = Math.max(viewportStart, targetOffset + state.scrollLength); -+ let start = targetIndex !== void 0 ? Math.max(0, Math.min(dataLength - 1, targetIndex)) : findPositionIndexAtOrBeforeOffset(ctx, viewportStart); -+ if (start === void 0) { -+ return void 0; -+ } -+ if (targetIndex !== void 0 && state.positions[start] === void 0) { -+ return { end: start, start }; -+ } -+ if (targetIndex === void 0) { -+ const startBottom = getItemBottom(ctx, start); -+ if (startBottom === void 0 || startBottom <= viewportStart) { -+ return void 0; -+ } -+ } -+ while (start > 0) { -+ const top = state.positions[start]; -+ if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { -+ break; -+ } -+ start--; -+ } -+ while (start > 0) { -+ const previousBottom = getItemBottom(ctx, start - 1); -+ if (previousBottom === void 0 || previousBottom <= viewportStart) { -+ break; -+ } -+ start--; -+ } -+ let end = start; -+ while (end + 1 < dataLength) { -+ const nextTop = state.positions[end + 1]; -+ if (nextTop === void 0 || nextTop > viewportEnd) { -+ break; -+ } -+ end++; -+ } -+ return { end, start }; -+} -+function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -+ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -+ if (range) { -+ ctx.state.scrollTargetPinnedRange = range; -+ ctx.state.scrollForNextCalculateItemsInView = void 0; -+ } else { -+ ctx.state.scrollTargetPinnedRange = void 0; -+ } -+} -+function scrollTo(ctx, params) { -+ var _a3, _b, _c; ++function scrollTo(ctx, params) { ++ var _a3, _b, _c; + const state = ctx.state; + const { noScrollingTo, forceScroll, ...scrollTarget } = params; + const { @@ -7459,30 +6526,23 @@ index 95465f2..9ac6d3f 100644 + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); + } - } -- if (Date.now() > lock.expiresAt) { -- state.mvcpAnchorLock = void 0; -- return false; ++ } + if (forceScroll || !isInitialScroll || Platform.OS === "android") { + doScrollTo(ctx, { animated, horizontal, offset }); + } else { + state.scroll = offset; - } -- return true; - } - --// src/utils/isInMVCPActiveMode.ts --function isInMVCPActiveMode(state) { -- return state.dataChangeNeedsScrollUpdate || hasActiveMVCPAnchorLock(state); ++ } ++} ++ +// src/core/scrollTargetSettle.ts -+var SETTLE_POSITION_EPSILON = 0.5; ++var SETTLE_POSITION_EPSILON = 1; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; +var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; -+function releaseScrollTargetForUserInteraction(ctx) { -+ if (ctx.state.scrollTargetSettle) { -+ clearScrollTargetSettle(ctx.state); ++function releaseScrollTargetForUserInteraction(state) { ++ if (state.scrollTargetSettle) { ++ clearScrollTargetSettle(state); + } +} +function clearScrollTargetSettle(state) { @@ -7499,6 +6559,7 @@ index 95465f2..9ac6d3f 100644 + clearScrollTargetSettle(state); + return; + } ++ clearScrollTargetSettle(state); + const now = Date.now(); + state.scrollTargetSettle = { + corrections: 0, @@ -7511,48 +6572,28 @@ index 95465f2..9ac6d3f 100644 + viewPosition + }; + state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline"); - } -- --// src/core/updateScroll.ts --function updateScroll(ctx, newScroll, forceUpdate, options) { ++} +function getSettleTargetOffset(ctx, settle, index, position) { + const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; + return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} +function applyScrollTargetCorrection(ctx, id) { - var _a3; - const state = ctx.state; -- const { ignoreScrollFromMVCP, lastScrollAdjustForHistory, scrollAdjustHandler, scrollHistory, scrollingTo } = state; -- const prevScroll = state.scroll; -- if ((options == null ? void 0 : options.markHasScrolled) !== false) { -- state.hasScrolled = true; ++ var _a3; ++ const state = ctx.state; + const settle = state.scrollTargetSettle; + if (!settle || settle.id !== id) { + return; - } -- const currentTime = Date.now(); -- state.lastBatchingAction = currentTime; -- const adjust = scrollAdjustHandler.getAdjust(); -- const adjustChanged = lastScrollAdjustForHistory !== void 0 && Math.abs(adjust - lastScrollAdjustForHistory) > 0.1; -- if (adjustChanged) { -- scrollHistory.length = 0; ++ } + const index = state.indexByKey.get(id); + const position = index === void 0 ? void 0 : state.positions[index]; + if (index === void 0 || position === void 0 || Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return; - } -- state.lastScrollAdjustForHistory = adjust; -- if (scrollingTo === void 0 && !(scrollHistory.length === 0 && newScroll === state.scroll)) { -- if (!adjustChanged) { -- scrollHistory.push({ scroll: newScroll, time: currentTime }); -- } ++ } + if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { + clearScrollTargetSettle(state); + return; - } -- if (scrollHistory.length > 5) { -- scrollHistory.shift(); ++ } + settle.corrections++; + settle.measuredIndex = void 0; + scrollTo(ctx, { @@ -7575,65 +6616,21 @@ index 95465f2..9ac6d3f 100644 + const settle = state.scrollTargetSettle; + if (!settle) { + return false; - } -- if (ignoreScrollFromMVCP && !scrollingTo) { -- const { lt, gt } = ignoreScrollFromMVCP; -- if (lt && newScroll < lt || gt && newScroll > gt) { -- state.ignoreScrollFromMVCPIgnored = true; -- return; ++ } + const measured = options == null ? void 0 : options.minIndexSizeChanged; + if (measured !== void 0) { + settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); ++ settle.expiresAt = Date.now() + SETTLE_TTL_MS; + } + if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return false; - } ++ } + settle.quietPasses = 0; + settle.expiresAt = Date.now() + SETTLE_TTL_MS; + return false; - } -- state.scrollPrev = prevScroll; -- state.scrollPrevTime = state.scrollTime; -- state.scroll = newScroll; -- state.scrollTime = currentTime; -- const scrollDelta = Math.abs(newScroll - prevScroll); -- const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; -- const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; -- const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); -- const scrollLength = state.scrollLength; -- const isLargeUserScrollJump = scrollLength > 0 && scrollingTo === void 0 && scrollDelta > scrollLength && !state.pendingNativeMVCPAdjust; -- const scrollVelocity = getScrollVelocity(state); -- updateAdaptiveRender(ctx, scrollVelocity, { forceLight: isLargeUserScrollJump }); -- const lastCalculated = state.scrollLastCalculate; -- const useAggressiveItemRecalculation = isInMVCPActiveMode(state); -- const shouldUpdate = useAggressiveItemRecalculation || didResolvePendingNativeMVCPAdjust || allowedEdge !== void 0 || forceUpdate || lastCalculated === void 0 || Math.abs(state.scroll - lastCalculated) > 2; -- if (shouldUpdate) { -- state.scrollLastCalculate = state.scroll; -- state.ignoreScrollFromMVCPIgnored = false; -- state.lastScrollDelta = scrollDelta; -- const runCalculateItems = () => { -- var _a4; -- const calculateItemsParams = { -- doMVCP: scrollingTo !== void 0, -- scrollVelocity -- }; -- if (isLargeUserScrollJump) { -- calculateItemsParams.drawDistanceMode = "visible-first"; -- } -- (_a4 = state.triggerCalculateItemsInView) == null ? void 0 : _a4.call(state, calculateItemsParams); -- checkThresholds(ctx, allowedEdge); -- }; -- if (isLargeUserScrollJump) { -- state.mvcpAnchorLock = void 0; -- state.pendingNativeMVCPAdjust = void 0; -- state.userScrollAnchorReset = { keys: /* @__PURE__ */ new Set() }; -- state.scheduledWork.cancel("mvcpRecalculate"); -- flushSync(runCalculateItems); -- scheduleFullDrawDistancePrewarm(ctx); -- } else { -- runCalculateItems(); ++ } + const index = state.indexByKey.get(settle.id); + const position = index === void 0 ? void 0 : state.positions[index]; + if (index === void 0 || position === void 0) { @@ -7655,11 +6652,7 @@ index 95465f2..9ac6d3f 100644 + settle.quietPasses++; + if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { + clearScrollTargetSettle(state); - } -- const shouldMaintainScrollAtEndAfterPendingSettle = !!state.pendingMaintainScrollAtEnd || !!((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onDataChange); -- if (didResolvePendingNativeMVCPAdjust && shouldMaintainScrollAtEndAfterPendingSettle) { -- state.pendingMaintainScrollAtEnd = false; -- doMaintainScrollAtEnd(ctx); ++ } + return false; + } + settle.quietPasses = 0; @@ -7698,9 +6691,7 @@ index 95465f2..9ac6d3f 100644 + nativeEvent: { + ...event.nativeEvent, + contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } - } -- state.dataChangeNeedsScrollUpdate = false; -- state.lastScrollDelta = 0; ++ } + }; +} +function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { @@ -7717,13 +6708,9 @@ index 95465f2..9ac6d3f 100644 + (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 + ) + ); - } - } - --// src/core/scrollTo.ts --function getAverageSizeSnapshot(state) { -- if (Object.keys(state.averageSizes).length === 0) { -- return void 0; ++ } ++} ++ +// src/utils/setInitialRenderState.ts +function resetInitialRenderState(ctx, { + resetLayout, @@ -7733,31 +6720,13 @@ index 95465f2..9ac6d3f 100644 + if (resetLayout) { + state.didContainersLayout = false; + state.queuedInitialLayout = false; - } -- const snapshot = {}; -- for (const itemType in state.averageSizes) { -- const averages = state.averageSizes[itemType]; -- snapshot[itemType] = averages.avg; ++ } + if (resetInitialScroll) { + state.didFinishInitialScroll = false; - } -- return snapshot; ++ } + set$(ctx, "readyToRender", false); + resetAdaptiveRender(ctx); - } --function syncInitialScrollNativeWatchdog(state, options) { -- var _a3; -- const { isInitialScroll, requestedOffset, targetOffset } = options; -- const existingWatchdog = initialScrollWatchdog.get(state); -- const shouldWatchInitialNativeScroll = !state.didFinishInitialScroll && (isInitialScroll || !!existingWatchdog) && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); -- const shouldClearInitialNativeScrollWatchdog = !state.didFinishInitialScroll && !!existingWatchdog && initialScrollWatchdog.isAtZeroTargetOffset(requestedOffset); -- if (shouldWatchInitialNativeScroll) { -- state.hasScrolled = false; -- initialScrollWatchdog.set(state, { -- startScroll: (_a3 = existingWatchdog == null ? void 0 : existingWatchdog.startScroll) != null ? _a3 : state.scroll, -- targetOffset -- }); -- return; ++} +function setInitialRenderState(ctx, { + didLayout, + didInitialScroll @@ -7769,30 +6738,10 @@ index 95465f2..9ac6d3f 100644 + } = state; + if (didLayout) { + state.didContainersLayout = true; - } -- if (shouldClearInitialNativeScrollWatchdog) { -- initialScrollWatchdog.clear(state); ++ } + if (didInitialScroll) { + state.didFinishInitialScroll = true; - } --} --function findPositionIndexAtOrBeforeOffset(ctx, offset) { -- const state = ctx.state; -- const dataLength = state.props.data.length; -- let low = 0; -- let high = dataLength - 1; -- let match; -- while (low <= high) { -- const mid = Math.floor((low + high) / 2); -- const top = state.positions[mid]; -- if (top === void 0) { -- high = mid - 1; -- } else { -- if (top <= offset) { -- match = mid; -- low = mid + 1; -- } else { -- high = mid - 1; ++ } + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); + if (isReadyToRender && !peek$(ctx, "readyToRender")) { + set$(ctx, "readyToRender", true); @@ -7804,19 +6753,10 @@ index 95465f2..9ac6d3f 100644 + state.didLoad = true; + if (onLoad) { + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); - } - } - } -- return match; - } --function getItemBottom(ctx, index) { -- var _a3; -- const top = ctx.state.positions[index]; -- if (top === void 0) { -- return void 0; -- } -- const itemSize = (_a3 = getItemSizeAtIndex(ctx, index)) != null ? _a3 : 0; -- return top + (Number.isFinite(itemSize) ? itemSize : 0); ++ } ++ } ++ } ++} + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -7824,8 +6764,7 @@ index 95465f2..9ac6d3f 100644 + state.scroll = offset; + state.scrollPending = offset; + state.scrollPrev = offset; - } --function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { ++} +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -7837,65 +6776,17 @@ index 95465f2..9ac6d3f 100644 +} +function supersedeInitialScroll(ctx) { + var _a3, _b, _c; - const state = ctx.state; -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return void 0; -- } -- const viewportStart = Math.max(0, targetOffset); -- const viewportEnd = Math.max(viewportStart, targetOffset + state.scrollLength); -- let start = targetIndex !== void 0 ? Math.max(0, Math.min(dataLength - 1, targetIndex)) : findPositionIndexAtOrBeforeOffset(ctx, viewportStart); -- if (start === void 0) { -- return void 0; -- } -- if (targetIndex !== void 0 && state.positions[start] === void 0) { -- return { end: start, start }; -- } -- if (targetIndex === void 0) { -- const startBottom = getItemBottom(ctx, start); -- if (startBottom === void 0 || startBottom <= viewportStart) { -- return void 0; -- } -- } -- while (start > 0) { -- const top = state.positions[start]; -- if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { -- break; -- } -- start--; -- } -- while (start > 0) { -- const previousBottom = getItemBottom(ctx, start - 1); -- if (previousBottom === void 0 || previousBottom <= viewportStart) { -- break; ++ const state = ctx.state; + const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; + if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { + if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { + cancelAnimationFrame(bootstrapInitialScroll.frameHandle); - } -- start--; -- } -- let end = start; -- while (end + 1 < dataLength) { -- const nextTop = state.positions[end + 1]; -- if (nextTop === void 0 || nextTop > viewportEnd) { -- break; ++ } + if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { + cancelScrollCompletionChecks(state); + state.scrollingTo = void 0; + state.scrollTargetPinnedRange = void 0; - } -- end++; -- } -- return { end, start }; --} --function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -- if (range) { -- ctx.state.scrollTargetPinnedRange = range; -- ctx.state.scrollForNextCalculateItemsInView = void 0; -- } else { -- ctx.state.scrollTargetPinnedRange = void 0; ++ } + initialScrollCompletion.resetFlags(state); + setInitialScrollSession(state, { bootstrap: null }); + finishInitialScroll(ctx); @@ -8004,15 +6895,15 @@ index 95465f2..9ac6d3f 100644 } // src/core/scrollToIndex.ts -@@ -4329,6 +4465,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4329,6 +4467,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); -+ const minIndexSizeChangedThisPass = dataChanged ? 0 : minIndexSizeChanged; ++ const minIndexSizeChangedThisPass = minIndexSizeChanged; if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4346,8 +4483,17 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4346,8 +4485,17 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -8032,11 +6923,33 @@ index 95465f2..9ac6d3f 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5894,6 +6040,119 @@ function useRafCoalescer(callback) { - return coalescer; +@@ -5906,6 +6054,21 @@ function getDocumentScrollerNode() { + } + return document.scrollingElement || document.documentElement || document.body; + } ++function getScrollAxis(horizontal) { ++ return horizontal ? { ++ contentSizeKey: "scrollWidth", ++ paddingEndProp: "paddingRight", ++ viewportSizeKey: "clientWidth", ++ x: 1, ++ y: 0 ++ } : { ++ contentSizeKey: "scrollHeight", ++ paddingEndProp: "paddingBottom", ++ viewportSizeKey: "clientHeight", ++ x: 0, ++ y: 1 ++ }; ++} + function getWindowScrollPosition() { + var _a3, _b, _c, _d; + if (typeof window === "undefined") { +@@ -5982,9 +6145,175 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll + }; } -+// src/components/temporaryEndPadding.ts ++// src/components/webTemporaryEndPadding.ts +var entriesByNode = /* @__PURE__ */ new WeakMap(); +var nextRequestId = 1; +function readResolvedPadding(node, prop) { @@ -8057,6 +6970,20 @@ index 95465f2..9ac6d3f 100644 + node.style[prop] = total === 0 ? entry.baseline : `${entry.baselineSize + total}px`; + entry.lastApplied = node.style[prop]; +} ++function drainPendingReleases(entry) { ++ if (entry.resetHandle !== void 0) { ++ cancelAnimationFrame(entry.resetHandle); ++ entry.resetHandle = void 0; ++ } ++ if (entry.pendingReleases.size === 0) { ++ return; ++ } ++ const releases = [...entry.pendingReleases]; ++ entry.pendingReleases.clear(); ++ for (const pending of releases) { ++ pending(); ++ } ++} +function releaseEntry(node, prop, requestId) { + const entries = entriesByNode.get(node); + const entry = entries == null ? void 0 : entries[prop]; @@ -8067,10 +6994,8 @@ index 95465f2..9ac6d3f 100644 + applyPadding(node, prop, entry); + } + if (entry.requests.size === 0) { -+ if (entry.resetHandle !== void 0) { -+ cancelAnimationFrame(entry.resetHandle); -+ } + entries == null ? true : delete entries[prop]; ++ drainPendingReleases(entry); + } +} +function addTemporaryEndPadding(node, prop, extraSize) { @@ -8123,7 +7048,10 @@ index 95465f2..9ac6d3f 100644 +} +function getTemporaryEndPadding(node, prop) { + var _a3; -+ const entry = node ? (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop] : void 0; ++ if (!node) { ++ return 0; ++ } ++ const entry = (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop]; + if (!entry || !isOwnedByUs(node, prop, entry)) { + return 0; + } @@ -8139,43 +7067,77 @@ index 95465f2..9ac6d3f 100644 + if (!entry) { + continue; + } -+ if (entry.resetHandle !== void 0) { -+ cancelAnimationFrame(entry.resetHandle); -+ } + if (isOwnedByUs(node, prop, entry)) { + node.style[prop] = entry.baseline; + } + delete entries[prop]; ++ entry.requests.clear(); ++ drainPendingReleases(entry); + } +} + - // src/components/webConstants.ts - var LEGEND_LIST_CONTENT_CONTAINER_CLASS = "legend-list-content-container"; - var LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS = "legend-list-scrollbar-x-hidden"; -@@ -5985,6 +6244,10 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; +var SMOOTH_SCROLL_MAX_MS = 2e3; ++var MAX_BORROW_VIEWPORTS = 3; ++var BORROW_WATCH_FRAME_INTERVAL = 3; +var BORROW_MEASURE_ATTEMPTS = 2; -+var USER_INTERACTION_EVENTS = ["wheel", "pointerdown", "touchstart", "keydown"]; ++var USER_DRAG_SLOP = 8; ++var WHEEL_MOMENTUM_GRACE_MS = 150; ++var SCROLL_KEYS = /* @__PURE__ */ new Set([ ++ " ", ++ "ArrowDown", ++ "ArrowLeft", ++ "ArrowRight", ++ "ArrowUp", ++ "End", ++ "Home", ++ "PageDown", ++ "PageUp" ++]); ++function isTextEntryTarget(target) { ++ var _a3; ++ const element = target; ++ const tag = (_a3 = element == null ? void 0 : element.tagName) == null ? void 0 : _a3.toLowerCase(); ++ return tag === "input" || tag === "textarea" || tag === "select" || !!(element == null ? void 0 : element.isContentEditable); ++} ++function pointerPosition(event) { ++ var _a3; ++ const touch = (_a3 = event.touches) == null ? void 0 : _a3[0]; ++ if (touch) { ++ return { x: touch.clientX, y: touch.clientY }; ++ } ++ const pointer = event; ++ return typeof pointer.clientX === "number" ? { x: pointer.clientX, y: pointer.clientY } : void 0; ++} ++function isScrollKey(event) { ++ if (event.altKey || event.ctrlKey || event.metaKey) { ++ return false; ++ } ++ return SCROLL_KEYS.has(event.key) && !isTextEntryTarget(event.target); ++} var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6053,6 +6316,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6053,6 +6382,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); -+ const paddingEndProp = horizontal ? isHorizontalRTL(ctx.state) ? "paddingLeft" : "paddingRight" : "paddingBottom"; ++ const paddingEndProp = getScrollAxis(horizontal).paddingEndProp; + const getCommittedMaxScrollOffset = useCallback( + (maxOffset) => Math.max(0, maxOffset - getTemporaryEndPadding(contentRef.current, paddingEndProp)), + [paddingEndProp] + ); ++ const getViewportExtent = useCallback(() => { ++ const layout = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); ++ return horizontal ? layout.width : layout.height; ++ }, [horizontal, isWindowScroll]); const getMaxScrollOffset = useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6073,6 +6341,95 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,8 +6411,153 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -8190,7 +7152,13 @@ index 95465f2..9ac6d3f 100644 + const contentNode = contentRef.current; + (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); + const committedMaxOffset = getCommittedMaxScrollOffset(getMaxScrollOffset()); -+ if (!contentNode || offset <= committedMaxOffset + SCROLL_EXTENT_EPSILON) { ++ const viewportExtent = getViewportExtent(); ++ const reachLimit = viewportExtent > 0 ? committedMaxOffset + MAX_BORROW_VIEWPORTS * viewportExtent : Number.POSITIVE_INFINITY; ++ if (!contentNode || !Number.isFinite(offset) || offset > reachLimit) { ++ run(clampOffset(offset, maxOffset)); ++ return; ++ } ++ if (offset <= committedMaxOffset + SCROLL_EXTENT_EPSILON) { + run(clampOffset(offset, maxOffset)); + return; + } @@ -8218,12 +7186,13 @@ index 95465f2..9ac6d3f 100644 + } + const scrollTarget = getScrollTarget(); + const supportsScrollEnd = !!scrollTarget && "onscrollend" in scrollTarget; ++ const timers = {}; + const finish = () => { + if (animatedPaddingReleaseRef.current !== finish) { + return; + } + animatedPaddingReleaseRef.current = void 0; -+ clearTimeout(settleTimeout); ++ clearTimeout(timers.settle); + scrollTarget == null ? void 0 : scrollTarget.removeEventListener("scrollend", finishIfArrived); + cancelAnimationFrame(borrowWatchRef.current); + release(); @@ -8233,25 +7202,36 @@ index 95465f2..9ac6d3f 100644 + finish(); + } + }; ++ let framesUntilCheck = 0; + const releaseWhenContentCommits = () => { + if (animatedPaddingReleaseRef.current !== finish) { + return; + } -+ if (getCommittedMaxScrollOffset(getMaxScrollOffset()) + SCROLL_EXTENT_EPSILON >= offset) { -+ finish(); -+ return; ++ if (framesUntilCheck-- <= 0) { ++ framesUntilCheck = BORROW_WATCH_FRAME_INTERVAL; ++ if (getCommittedMaxScrollOffset(getMaxScrollOffset()) + SCROLL_EXTENT_EPSILON >= offset) { ++ finish(); ++ return; ++ } + } + borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); + }; + animatedPaddingReleaseRef.current = finish; + cancelAnimationFrame(borrowWatchRef.current); + borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); -+ const settleTimeout = setTimeout(finish, SMOOTH_SCROLL_MAX_MS); ++ timers.settle = setTimeout(finish, SMOOTH_SCROLL_MAX_MS); + if (supportsScrollEnd) { + scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finishIfArrived); + } + }, -+ [getCommittedMaxScrollOffset, getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, paddingEndProp] ++ [ ++ getCommittedMaxScrollOffset, ++ getCurrentScrollOffset, ++ getMaxScrollOffset, ++ getScrollTarget, ++ getViewportExtent, ++ paddingEndProp ++ ] + ); + useEffect( + () => () => { @@ -8265,13 +7245,53 @@ index 95465f2..9ac6d3f 100644 + }, + [] + ); ++ const interactionArmedAtRef = useRef(0); ++ const dragOriginRef = useRef(void 0); + const reportUserInteraction = useCallback(() => { -+ releaseScrollTargetForUserInteraction(ctx); ++ dragOriginRef.current = void 0; ++ releaseScrollTargetForUserInteraction(ctx.state); + }, [ctx]); ++ const onWheel = useCallback( ++ (event) => { ++ if (event.timeStamp - interactionArmedAtRef.current < WHEEL_MOMENTUM_GRACE_MS) { ++ return; ++ } ++ reportUserInteraction(); ++ }, ++ [reportUserInteraction] ++ ); ++ const onPointerDown = useCallback((event) => { ++ const point = pointerPosition(event); ++ dragOriginRef.current = point; ++ }, []); ++ const onPointerMove = useCallback( ++ (event) => { ++ const origin = dragOriginRef.current; ++ const point = origin && pointerPosition(event); ++ if (!origin || !point) { ++ return; ++ } ++ if (Math.abs(point.x - origin.x) > USER_DRAG_SLOP || Math.abs(point.y - origin.y) > USER_DRAG_SLOP) { ++ reportUserInteraction(); ++ } ++ }, ++ [reportUserInteraction] ++ ); ++ const onKeyDown = useCallback( ++ (event) => { ++ if (isScrollKey(event)) { ++ reportUserInteraction(); ++ } ++ }, ++ [reportUserInteraction] ++ ); const scrollToLocalOffset = useCallback( (offset, animated) => { ++ interactionArmedAtRef.current = typeof performance !== "undefined" ? performance.now() : 0; const scrollElement = scrollRef.current; -@@ -6095,14 +6452,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + const target = getScrollTarget(); + if (!target || typeof target.scrollTo !== "function") { +@@ -6095,14 +6578,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); options.left = left; options.top = top; @@ -8298,7 +7318,7 @@ index 95465f2..9ac6d3f 100644 ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6492,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6128,8 +6618,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -8308,7 +7328,7 @@ index 95465f2..9ac6d3f 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6142,7 +6505,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6142,7 +6631,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ if (!onScroll2 || !scrollRef.current) { return; } @@ -8325,25 +7345,74 @@ index 95465f2..9ac6d3f 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6200,11 +6571,17 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6200,11 +6697,26 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ const target = getScrollTarget(); if (!target) return; target.addEventListener("scroll", handleScroll, { passive: true }); -+ for (const type of USER_INTERACTION_EVENTS) { -+ target.addEventListener(type, reportUserInteraction, { passive: true }); -+ } ++ const listenerOptions = { capture: true, passive: true }; ++ const removeOptions = { capture: true }; ++ const interactionTarget = scrollRef.current; ++ target.addEventListener("wheel", onWheel, listenerOptions); ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener("pointerdown", onPointerDown, listenerOptions); ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener("pointermove", onPointerMove, listenerOptions); ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener("touchstart", onPointerDown, listenerOptions); ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener("touchmove", onPointerMove, listenerOptions); ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener("keydown", onKeyDown, listenerOptions); if ("onscrollend" in target) { target.addEventListener("scrollend", emitScrollEnd); } return () => { target.removeEventListener("scroll", handleScroll); -+ for (const type of USER_INTERACTION_EVENTS) { -+ target.removeEventListener(type, reportUserInteraction); -+ } ++ target.removeEventListener("wheel", onWheel, removeOptions); ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("pointerdown", onPointerDown, removeOptions); ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("pointermove", onPointerMove, removeOptions); ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("touchstart", onPointerDown, removeOptions); ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("touchmove", onPointerMove, removeOptions); ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("keydown", onKeyDown, removeOptions); if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6390,8 +6767,6 @@ function ScrollAdjust() { +@@ -6215,7 +6727,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + } + scrollEventCoalescer.cancel(); + }; +- }, [emitScrollEnd, getScrollTarget, handleScroll, scrollEventCoalescer]); ++ }, [ ++ emitScrollEnd, ++ getScrollTarget, ++ handleScroll, ++ onKeyDown, ++ onPointerDown, ++ onPointerMove, ++ onWheel, ++ scrollEventCoalescer ++ ]); + useEffect(() => { + const doScroll = () => { + if (contentOffset) { +@@ -6358,21 +6879,6 @@ function useValueListener$(key, callback) { + } + + // src/components/ScrollAdjust.tsx +-function getScrollAdjustAxis(horizontal) { +- return horizontal ? { +- contentSizeKey: "scrollWidth", +- paddingEndProp: "paddingRight", +- viewportSizeKey: "clientWidth", +- x: 1, +- y: 0 +- } : { +- contentSizeKey: "scrollHeight", +- paddingEndProp: "paddingBottom", +- viewportSizeKey: "clientHeight", +- x: 0, +- y: 1 +- }; +-} + function getScrollAdjustTarget(ctx, contentNode) { + var _a3, _b, _c; + const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; +@@ -6390,8 +6896,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3.useRef(0); const lastScrollAdjustUserOffsetRef = React3.useRef(0); @@ -8352,7 +7421,16 @@ index 95465f2..9ac6d3f 100644 const contentNodeRef = React3.useRef(null); const callback = React3.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6417,29 +6792,10 @@ function ScrollAdjust() { +@@ -6402,7 +6906,7 @@ function ScrollAdjust() { + const target = getScrollAdjustTarget(ctx, contentNodeRef.current); + if (target) { + const horizontal = !!ctx.state.props.horizontal; +- const axis = getScrollAdjustAxis(horizontal); ++ const axis = getScrollAxis(horizontal); + const { contentNode, scrollElement: el } = target; + const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; + const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; +@@ -6417,29 +6921,10 @@ function ScrollAdjust() { const nextScroll = currentScroll + scrollDelta; const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; if (needsTemporaryPadding) { @@ -8384,19 +7462,19 @@ index 95465f2..9ac6d3f 100644 } else { scrollBy(); } -@@ -8267,6 +8623,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8267,6 +8752,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); -+ clearScrollTargetSettle(state); ++ releaseScrollTargetForUserInteraction(state); (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js -index 914d2da..c3ed202 100644 +index 914d2da..f03ba1e 100644 --- a/node_modules/@legendapp/list/react.js +++ b/node_modules/@legendapp/list/react.js -@@ -434,178 +434,266 @@ var EDGE_POSITION_EPSILON = 1; +@@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; var ENABLE_DEVMODE = IS_DEV && false; var ENABLE_DEBUG_VIEW = IS_DEV && false; @@ -8406,56 +7484,22 @@ index 914d2da..c3ed202 100644 - scheduledWork.cancel("checkFinishedScrollRetryFrame"); - scheduledWork.cancel("checkFinishedScrollFallback"); - scheduledWork.cancel("platformScrollCompletion"); -+// src/core/getStartOffsetAdjustment.ts -+function getStartOffsetAdjustment(ctx) { -+ const { state } = ctx; -+ const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; -+ return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); - } +-} -function settlePendingImperativeScroll(state) { - var _a3, _b; - const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; - state.pendingScrollResolve = void 0; - state.pendingScrollToEnd = void 0; - resolvePendingScroll == null ? void 0 : resolvePendingScroll(); -+ -+// src/utils/getId.ts -+function getId(state, index) { -+ const { data, keyExtractor } = state.props; -+ if (!data) { -+ return ""; -+ } -+ const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; -+ const id = ret; -+ state.idCache[index] = id; -+ return id; - } +-} -function cancelImperativeScroll(state) { - cancelScrollCompletionChecks(state); - state.scheduledWork.cancel("imperativeScrollReady"); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; - settlePendingImperativeScroll(state); -+ -+// src/core/updateContentMetricsState.ts -+function getRawContentLength(ctx) { -+ var _a3, _b, _c; -+ const { state, values } = ctx; -+ return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); -+} -+function getAlignItemsAtEndPadding(ctx) { -+ const { state } = ctx; -+ const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; -+ return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; -+} -+function updateContentMetricsState(ctx) { -+ const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; -+ const nextPadding = getAlignItemsAtEndPadding(ctx); -+ if (previousPadding !== nextPadding) { -+ set$(ctx, "alignItemsAtEndPadding", nextPadding); -+ } - } - +-} +- -// src/core/deferredPublicOnScroll.ts -function withResolvedContentOffset(state, event, resolvedOffset) { - return { @@ -8463,40 +7507,12 @@ index 914d2da..c3ed202 100644 - nativeEvent: { - ...event.nativeEvent, - contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -+// src/core/addTotalSize.ts -+function addTotalSize(ctx, key, add, notifyTotalSize = true) { -+ const state = ctx.state; -+ const prevTotalSize = state.totalSize; -+ let totalSize = state.totalSize; -+ if (key === null) { -+ totalSize = add; -+ if (state.timeoutSetPaddingTop) { -+ clearTimeout(state.timeoutSetPaddingTop); -+ state.timeoutSetPaddingTop = void 0; - } +- } - }; -+ } else { -+ totalSize += add; -+ } -+ if (prevTotalSize !== totalSize) { -+ { -+ state.pendingTotalSize = void 0; -+ state.totalSize = totalSize; -+ if (notifyTotalSize) { -+ set$(ctx, "totalSize", totalSize); -+ } -+ updateContentMetricsState(ctx); -+ } -+ } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { -+ set$(ctx, "totalSize", totalSize); -+ } - } +-} -function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { - var _a3, _b, _c, _d; -+ -+// src/core/setSize.ts -+function setSize(ctx, itemKey, size, notifyTotalSize = true) { - const state = ctx.state; +- const state = ctx.state; - const deferredEvent = state.deferredPublicOnScrollEvent; - state.deferredPublicOnScrollEvent = void 0; - if (deferredEvent) { @@ -8508,15 +7524,9 @@ index 914d2da..c3ed202 100644 - (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 - ) - ); -+ const { sizes } = state; -+ const previousSize = sizes.get(itemKey); -+ const diff = previousSize !== void 0 ? size - previousSize : size; -+ if (diff !== 0) { -+ addTotalSize(ctx, itemKey, diff, notifyTotalSize); - } -+ sizes.set(itemKey, size); - } - +- } +-} +- -// src/core/initialScrollSession.ts -var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -function hasInitialScrollSessionCompletion(completion) { @@ -8525,10 +7535,7 @@ index 914d2da..c3ed202 100644 -function clearInitialScrollSession(state) { - state.initialScrollSession = void 0; - return void 0; -+// src/utils/helpers.ts -+function isFunction(obj) { -+ return typeof obj === "function"; - } +-} -function createInitialScrollSession(options) { - const { bootstrap, completion, kind, previousDataLength } = options; - return kind === "offset" ? { @@ -8541,9 +7548,7 @@ index 914d2da..c3ed202 100644 - kind, - previousDataLength - }; -+function isArray(obj) { -+ return Array.isArray(obj); - } +-} -function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { - var _a4, _b2; - if (!state.initialScrollSession) { @@ -8559,15 +7564,10 @@ index 914d2da..c3ed202 100644 - kind, - previousDataLength: state.initialScrollSession.previousDataLength - }); -+var warned = /* @__PURE__ */ new Set(); -+function warnDevOnce(id, text) { -+ if (IS_DEV && !warned.has(id)) { -+ warned.add(id); -+ console.warn(`[legend-list] ${text}`); - } +- } - (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; - return state.initialScrollSession.completion; - } +-} -var initialScrollCompletion = { - didDispatchNativeScroll(state) { - var _a3, _b; @@ -8590,26 +7590,7 @@ index 914d2da..c3ed202 100644 - const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); - completion.didDispatchNativeScroll = void 0; - completion.didRetrySilentInitialScroll = void 0; -+function roundSize(size) { -+ return Math.floor(size * 8) / 8; -+} -+function isNullOrUndefined(value) { -+ return value === null || value === void 0; -+} -+function getPadding(s, type) { -+ var _a3, _b, _c; -+ const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; -+ return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; -+} -+function extractPadding(style, contentContainerStyle, type) { -+ return getPadding(style, type) + getPadding(contentContainerStyle, type); -+} -+function findContainerId(ctx, key) { -+ var _a3, _b; -+ const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); -+ if (directMatch !== void 0) { -+ return directMatch; - } +- } -}; -var initialScrollWatchdog = { - clear(state) { @@ -8633,12 +7614,7 @@ index 914d2da..c3ed202 100644 - var _a3, _b; - if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { - return; -+ const numContainers = peek$(ctx, "numContainers"); -+ for (let i = 0; i < numContainers; i++) { -+ const itemKey = peek$(ctx, `containerItemKey${i}`); -+ if (itemKey === key) { -+ return i; - } +- } - const completion = ensureInitialScrollSessionCompletion(state); - completion.watchdog = watchdog ? { - startScroll: watchdog.startScroll, @@ -8658,7 +7634,7 @@ index 914d2da..c3ed202 100644 - } - if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { - return clearInitialScrollSession(state); - } +- } - const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; - state.initialScrollSession = createInitialScrollSession({ - bootstrap, @@ -8667,282 +7643,299 @@ index 914d2da..c3ed202 100644 - previousDataLength - }); - return state.initialScrollSession; -+ return -1; - } - +-} +- -// src/utils/checkThreshold.ts -var HYSTERESIS_MULTIPLIER = 1.3; -function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { - const absDistance = Math.abs(distance); - return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; -+// src/utils/getItemSize.ts -+function getKnownOrFixedSize(ctx, key, index, data, resolved) { -+ var _a3, _b; -+ const state = ctx.state; -+ const { getFixedItemSize, getItemType } = state.props; -+ let size = key ? state.sizesKnown.get(key) : void 0; -+ if (size === void 0 && key && getFixedItemSize) { -+ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; -+ const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); -+ if (fixedSize !== void 0) { -+ size = fixedSize + ctx.scrollAxisGap; -+ state.sizesKnown.set(key, size); -+ } -+ } -+ return size; -+} -+function getKnownOrFixedItemSize(ctx, index) { -+ const key = getId(ctx.state, index); -+ return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); -+} -+function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { -+ for (let index = startIndex; index <= endIndex; index++) { -+ if (getKnownOrFixedItemSize(ctx, index) === void 0) { -+ return false; -+ } -+ } -+ return true; -+} -+function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { -+ var _a3, _b, _c, _d; -+ const state = ctx.state; -+ const { -+ sizes, -+ averageSizes, -+ props: { estimatedItemSize, getItemType }, -+ scrollingTo -+ } = state; -+ const sizeKnown = state.sizesKnown.get(key); -+ if (sizeKnown !== void 0) { -+ return sizeKnown; -+ } -+ let size; -+ const renderedSize = sizes.get(key); -+ if (preferCachedSize) { -+ if (renderedSize !== void 0) { -+ return renderedSize; -+ } -+ } -+ size = getKnownOrFixedSize(ctx, key, index, data, resolved); -+ if (size !== void 0) { -+ setSize(ctx, key, size, notifyTotalSize); -+ return size; -+ } -+ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; -+ if (useAverageSize && !scrollingTo) { -+ const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; -+ if (averageSizeForType !== void 0) { -+ size = roundSize(averageSizeForType); -+ } -+ } -+ if (size === void 0 && renderedSize !== void 0) { -+ return renderedSize; -+ } -+ if (size === void 0 && useAverageSize && scrollingTo) { -+ const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; -+ if (averageSizeForType !== void 0) { -+ size = roundSize(averageSizeForType); -+ } -+ } -+ if (size === void 0) { -+ size = estimatedItemSize + ctx.scrollAxisGap; -+ } -+ setSize(ctx, key, size, notifyTotalSize); -+ return size; -+} -+function getItemSizeAtIndex(ctx, index) { -+ if (index === void 0 || index < 0) { -+ return void 0; -+ } -+ const targetId = getId(ctx.state, index); -+ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+} -+ -+// src/core/calculateOffsetWithOffsetPosition.ts -+function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -+ var _a3; -+ const state = ctx.state; -+ const { index, viewOffset, viewPosition } = params; -+ let offset = offsetParam; -+ if (viewOffset) { -+ offset -= viewOffset; -+ } -+ if (index !== void 0) { -+ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -+ if (startOffsetAdjustment) { -+ offset += startOffsetAdjustment; -+ } -+ } -+ if (viewPosition !== void 0 && index !== void 0) { -+ const dataLength = state.props.data.length; -+ if (dataLength === 0) { -+ return offset; -+ } -+ const isOutOfBounds = index < 0 || index >= dataLength; -+ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -+ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -+ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -+ const trailingInset = getContentInsetEnd(ctx); -+ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -+ if (!isOutOfBounds && index === state.props.data.length - 1) { -+ const footerSize = peek$(ctx, "footerSize") || 0; -+ offset += footerSize; -+ } -+ } -+ return offset; -+} -+ -+// src/core/clampScrollOffset.ts -+function clampScrollOffset(ctx, offset, scrollTarget) { -+ const state = ctx.state; -+ const contentSize = getContentSize(ctx); -+ let clampedOffset = offset; -+ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { -+ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); -+ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; -+ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; -+ const maxOffset = baseMaxOffset + extraEndOffset; -+ clampedOffset = Math.min(offset, maxOffset); -+ } -+ clampedOffset = Math.max(0, clampedOffset); -+ return clampedOffset; -+} -+ -+// src/utils/checkThreshold.ts -+var HYSTERESIS_MULTIPLIER = 1.3; -+function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { -+ const absDistance = Math.abs(distance); -+ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; - } - var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { - const absDistance = Math.abs(distance); -@@ -815,642 +903,346 @@ function recalculateSettledScroll(ctx) { - } - checkThresholds(ctx); - } --var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; --var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; --var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; --function clearAdaptiveRenderExitTimeout(ctx) { -- ctx.state.scheduledWork.cancel("adaptiveRender"); -} --function scheduleAdaptiveRenderExit(ctx, exitDelay) { +-var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { +- const absDistance = Math.abs(distance); +- const within = atThreshold || threshold > 0 && absDistance <= threshold; +- const updateSnapshot = () => { +- setSnapshot({ +- atThreshold, +- contentSize: context.contentSize, +- dataLength: context.dataLength, +- scrollPosition: context.scrollPosition +- }); +- }; +- if (!wasReached) { +- if (!within) { +- return false; +- } +- onReached(distance); +- updateSnapshot(); +- return true; +- } +- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); +- if (reset) { +- setSnapshot(void 0); +- return false; +- } +- if (within) { +- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; +- if (changed) { +- updateSnapshot(); +- } +- } +- return true; +-}; +- +-// src/utils/edgeReachedGate.ts +-function resetEdgeLatch(ctx, edge) { - const state = ctx.state; -- clearAdaptiveRenderExitTimeout(ctx); -- if (exitDelay <= 0) { -- setAdaptiveRender(ctx, "normal", "scroll"); +- if (edge === "start") { +- state.isStartReached = false; +- state.startReachedSnapshot = void 0; - } else { -- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); +- state.isEndReached = false; +- state.endReachedSnapshot = void 0; - } -} --function setAdaptiveRender(ctx, mode, reason) { -- var _a3, _b; -- const previousMode = peek$(ctx, "adaptiveRender"); -- if (previousMode !== mode) { -- set$(ctx, "adaptiveRender", mode); -- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); +-function resetSharedEdgeGateIfOutsideHysteresis(ctx) { +- const state = ctx.state; +- if (!state.edgeReachedGate) { +- return; +- } +- const contentSize = getContentSize(ctx); +- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); +- const isContentLess = contentSize < state.scrollLength; +- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; +- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; +- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); +- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); +- if (isOutsideStart && isOutsideEnd) { +- state.edgeReachedGate = void 0; - } -} --function resetAdaptiveRender(ctx) { -+ -+// src/core/finishScrollTo.ts -+function finishScrollTo(ctx) { - var _a3, _b; -- clearAdaptiveRenderExitTimeout(ctx); -- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; -- if (peek$(ctx, "adaptiveRender") !== mode) { -- setAdaptiveRender(ctx, mode, "initial"); +-function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { +- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; +-} +-function markReachedEdge(ctx) { +- ctx.state.edgeReachedGate = "closed"; +-} +-function prepareReachedEdgeForNextUserScroll(ctx) { +- if (ctx.state.edgeReachedGate) { +- ctx.state.edgeReachedGate = "prepared"; - } -} --function updateAdaptiveRender(ctx, scrollVelocity, options) { -- var _a3, _b, _c; - const state = ctx.state; -- const adaptiveRender = state.props.adaptiveRender; -- const currentMode = peek$(ctx, "adaptiveRender"); -- if (peek$(ctx, "readyToRender")) { -- if (adaptiveRender) { -- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; -- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; -- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; -- const threshold = currentMode === "light" ? exitVelocity : enterVelocity; -- const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; -- const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; -- if (nextMode !== previousMode) { -- if (nextMode === "light") { -- setAdaptiveRender(ctx, "light", "scroll"); -- scheduleAdaptiveRenderExit(ctx, exitDelay); -- } else if (currentMode === "light") { -- scheduleAdaptiveRenderExit(ctx, exitDelay); -- } -- } -- } else { -- resetAdaptiveRender(ctx); -+ if (state == null ? void 0 : state.scrollingTo) { -+ cancelScrollCompletionChecks(state); -+ const resolvePendingScroll = state.pendingScrollResolve; -+ state.pendingScrollResolve = void 0; -+ const scrollingTo = state.scrollingTo; -+ state.scrollHistory.length = 0; -+ state.scrollingTo = void 0; -+ state.scrollTargetPinnedRange = void 0; -+ if (state.pendingTotalSize !== void 0) { -+ addTotalSize(ctx, null, state.pendingTotalSize); -+ } -+ { -+ state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); -+ } -+ if (scrollingTo.isInitialScroll || state.initialScroll) { -+ const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; -+ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; -+ finishInitialScroll(ctx, { -+ onFinished: () => { -+ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); -+ }, -+ preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, -+ recalculateItems: true, -+ schedulePreservedTargetClear: shouldPreserveResizeTarget, -+ syncObservedOffset: isOffsetSession, -+ waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame -+ }); -+ return; - } -+ recalculateSettledScroll(ctx); -+ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); - } - } - --// src/utils/getEffectiveDrawDistance.ts --var INITIAL_DRAW_DISTANCE = 50; --function getEffectiveDrawDistance(ctx, mode) { -- var _a3; -- const drawDistance = ctx.state.props.drawDistance; -- const initialScroll = ctx.state.initialScroll; -- const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; -- const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; -- return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; +-function beginReachedEdgeUserScroll(ctx, scrollDelta) { +- const state = ctx.state; +- if (state.edgeReachedGate !== "prepared") { +- return void 0; +- } +- const allowedEdge = scrollDelta < 0 ? "start" : "end"; +- state.edgeReachedGate = "closed"; +- resetEdgeLatch(ctx, allowedEdge); +- return allowedEdge; -} --function scheduleFullDrawDistancePrewarm(ctx) { -- const { state } = ctx; -- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { -+// src/core/doScrollTo.ts -+var SCROLL_END_IDLE_MS = 80; -+var SCROLL_END_MAX_MS = 1500; -+var SMOOTH_SCROLL_DURATION_MS = 320; -+var SCROLL_END_TARGET_EPSILON = 1; -+function doScrollTo(ctx, params) { -+ var _a3, _b; -+ const state = ctx.state; -+ const { animated, horizontal, offset } = params; -+ state.scheduledWork.cancel("platformScrollCompletion"); -+ const scroller = state.refScroller.current; -+ const node = scroller == null ? void 0 : scroller.getScrollableNode(); -+ if (!scroller || !node) { - return; - } -- state.scheduledWork.frame(() => { -- var _a3; -- return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); -- }, "fullDrawDistancePrewarm"); +- +-// src/utils/hasActiveInitialScroll.ts +-function hasActiveInitialScroll(state) { +- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; +-} +- +-// src/utils/checkAtBottom.ts +-function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { +- var _a3; +- const state = ctx.state; +- if (!state) { +- return; +- } +- const { +- queuedInitialLayout, +- scrollLength, +- scroll, +- maintainingScrollAtEnd, +- props: { maintainScrollAtEndThreshold, onEndReachedThreshold } +- } = state; +- const contentSize = getContentSize(ctx); +- resetSharedEdgeGateIfOutsideHysteresis(ctx); +- if (contentSize > 0 && queuedInitialLayout) { +- const insetEnd = getContentInsetEnd(ctx); +- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; +- const isContentLess = contentSize < scrollLength; +- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); +- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); +- set$( +- ctx, +- "isWithinMaintainScrollAtEndThreshold", +- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength +- ); +- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; +- if (!shouldSkipThresholdChecks) { +- state.isEndReached = checkThreshold( +- distanceFromEnd, +- isContentLess, +- onEndReachedThreshold * scrollLength, +- state.isEndReached, +- state.endReachedSnapshot, +- { +- contentSize, +- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, +- scrollPosition: scroll +- }, +- (distance) => { +- var _a4, _b; +- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { +- markReachedEdge(ctx); +- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); +- } +- }, +- (snapshot) => { +- state.endReachedSnapshot = snapshot; +- } +- ); +- } +- } +-} +- +-// src/utils/checkAtTop.ts +-function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { +- const state = ctx == null ? void 0 : ctx.state; +- if (!state) { +- return; +- } +- const { +- isStartReached, +- props: { data, onStartReachedThreshold }, +- scroll, +- scrollLength, +- startReachedSnapshot, +- totalSize +- } = state; +- const dataLength = data.length; +- const threshold = onStartReachedThreshold * scrollLength; +- resetSharedEdgeGateIfOutsideHysteresis(ctx); +- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { +- state.isStartReached = false; +- state.startReachedSnapshot = void 0; +- } +- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); +- set$(ctx, "isNearStart", scroll <= threshold); +- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; +- if (!shouldSkipThresholdChecks) { +- state.isStartReached = checkThreshold( +- scroll, +- false, +- threshold, +- state.isStartReached, +- startReachedSnapshot, +- { +- contentSize: totalSize, +- dataLength, +- scrollPosition: scroll +- }, +- (distance) => { +- var _a3, _b; +- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { +- markReachedEdge(ctx); +- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); +- } +- }, +- (snapshot) => { +- state.startReachedSnapshot = snapshot; +- } +- ); +- } +-} +- +-// src/utils/checkThresholds.ts +-function checkThresholds(ctx, allowedEdge) { +- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; +- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); +- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); +-} +- +-// src/core/recalculateSettledScroll.ts +-function recalculateSettledScroll(ctx) { +- var _a3, _b; +- const state = ctx.state; +- if ((_a3 = state.props) == null ? void 0 : _a3.data) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); +- } +- checkThresholds(ctx); +-} +-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; +-function clearAdaptiveRenderExitTimeout(ctx) { +- ctx.state.scheduledWork.cancel("adaptiveRender"); +-} +-function scheduleAdaptiveRenderExit(ctx, exitDelay) { +- const state = ctx.state; +- clearAdaptiveRenderExitTimeout(ctx); +- if (exitDelay <= 0) { +- setAdaptiveRender(ctx, "normal", "scroll"); +- } else { +- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); +- } +-} +-function setAdaptiveRender(ctx, mode, reason) { +- var _a3, _b; +- const previousMode = peek$(ctx, "adaptiveRender"); +- if (previousMode !== mode) { +- set$(ctx, "adaptiveRender", mode); +- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); +- } +-} +-function resetAdaptiveRender(ctx) { +- var _a3, _b; +- clearAdaptiveRenderExitTimeout(ctx); +- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; +- if (peek$(ctx, "adaptiveRender") !== mode) { +- setAdaptiveRender(ctx, mode, "initial"); +- } +-} +-function updateAdaptiveRender(ctx, scrollVelocity, options) { +- var _a3, _b, _c; +- const state = ctx.state; +- const adaptiveRender = state.props.adaptiveRender; +- const currentMode = peek$(ctx, "adaptiveRender"); +- if (peek$(ctx, "readyToRender")) { +- if (adaptiveRender) { +- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; +- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; +- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; +- const threshold = currentMode === "light" ? exitVelocity : enterVelocity; +- const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; +- const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; +- if (nextMode !== previousMode) { +- if (nextMode === "light") { +- setAdaptiveRender(ctx, "light", "scroll"); +- scheduleAdaptiveRenderExit(ctx, exitDelay); +- } else if (currentMode === "light") { +- scheduleAdaptiveRenderExit(ctx, exitDelay); +- } +- } +- } else { +- resetAdaptiveRender(ctx); +- } +- } +-} +- +-// src/utils/getEffectiveDrawDistance.ts +-var INITIAL_DRAW_DISTANCE = 50; +-function getEffectiveDrawDistance(ctx, mode) { +- var _a3; +- const drawDistance = ctx.state.props.drawDistance; +- const initialScroll = ctx.state.initialScroll; +- const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; +- const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; +- return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; +-} +-function scheduleFullDrawDistancePrewarm(ctx) { +- const { state } = ctx; +- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { +- return; +- } +- state.scheduledWork.frame(() => { +- var _a3; +- return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +- }, "fullDrawDistancePrewarm"); -} - -// src/utils/setInitialRenderState.ts @@ -8957,35 +7950,10 @@ index 914d2da..c3ed202 100644 - } - if (resetInitialScroll) { - state.didFinishInitialScroll = false; -+ const isAnimated = !!animated; -+ const isHorizontal = !!horizontal; -+ const contentSize = isHorizontal ? getContentSize(ctx) : void 0; -+ const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; -+ const top = isHorizontal ? 0 : offset; -+ scroller.scrollTo({ animated: isAnimated, x: left, y: top }); -+ if (isAnimated) { -+ const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; -+ listenForScrollEnd(ctx, { -+ readOffset: () => scroller.getCurrentScrollOffset(), -+ target, -+ targetOffset: offset -+ }); -+ } else { -+ state.scroll = offset; -+ const targetToken = state.scrollingTo; -+ state.scheduledWork.timeout( -+ () => { -+ if (targetToken === state.scrollingTo) { -+ finishScrollTo(ctx); -+ } -+ }, -+ 100, -+ "platformScrollCompletion" -+ ); - } +- } - set$(ctx, "readyToRender", false); - resetAdaptiveRender(ctx); - } +-} -function setInitialRenderState(ctx, { - didLayout, - didInitialScroll @@ -9014,12 +7982,7 @@ index 914d2da..c3ed202 100644 - onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); - } - } -+function listenForScrollEnd(ctx, params) { -+ const { readOffset, target, targetOffset } = params; -+ if (!target) { -+ finishScrollTo(ctx); -+ return; - } +- } -} - -// src/core/finishInitialScroll.ts @@ -9045,23 +8008,12 @@ index 914d2da..c3ed202 100644 - if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { - if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { - cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -+ const supportsScrollEnd = "onscrollend" in target; -+ let idleTimeout; -+ let settled = false; -+ const { scheduledWork, scrollingTo: targetToken } = ctx.state; -+ const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); -+ const cleanup = () => { -+ target.removeEventListener("scroll", onScroll2); -+ if (supportsScrollEnd) { -+ target.removeEventListener("scrollend", onScrollEnd); - } +- } - if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { - cancelScrollCompletionChecks(state); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; -+ if (idleTimeout !== void 0) { -+ clearTimeout(idleTimeout); - } +- } - initialScrollCompletion.resetFlags(state); - setInitialScrollSession(state, { bootstrap: null }); - finishInitialScroll(ctx); @@ -9076,13 +8028,7 @@ index 914d2da..c3ed202 100644 - const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); - if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { - syncInitialScrollOffset(state, observedOffset); -+ clearTimeout(maxTimeout); -+ }; -+ const cancel = () => { -+ if (!settled) { -+ settled = true; -+ cleanup(); - } +- } - } - const complete = () => { - var _a4, _b2, _c2, _d, _e; @@ -9108,422 +8054,290 @@ index 914d2da..c3ed202 100644 - } - } else { - clearPreservedInitialScrollTarget(state); -+ }; -+ const finish = (reason) => { -+ if (settled) return; -+ if (targetToken !== ctx.state.scrollingTo) { -+ scheduledWork.cancel("platformScrollCompletion"); -+ return; - } +- } - if (options == null ? void 0 : options.recalculateItems) { - recalculateSettledScroll(ctx); -+ const currentOffset = readOffset(); -+ const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; -+ if (reason === "scrollend" && !isNearTarget) { -+ return; - } +- } - setInitialRenderState(ctx, { didInitialScroll: true }); - if (shouldReleaseDeferredPublicOnScroll) { - releaseDeferredPublicOnScroll(ctx, finalScrollOffset); -+ scheduledWork.cancel("platformScrollCompletion"); -+ finishScrollTo(ctx); -+ }; -+ const onScroll2 = () => { -+ if (idleTimeout !== void 0) { -+ clearTimeout(idleTimeout); - } +- } - (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); -+ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); - }; +- }; - if (options == null ? void 0 : options.waitForCompletionFrame) { - requestAnimationFrame(complete); - return; -+ const onScrollEnd = () => finish("scrollend"); -+ target.addEventListener("scroll", onScroll2); -+ if (supportsScrollEnd) { -+ target.addEventListener("scrollend", onScrollEnd); -+ } else { -+ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); - } +- } - complete(); -+ scheduledWork.register("platformScrollCompletion", cancel); - } - +-} +- -// src/core/calculateOffsetForIndex.ts -function calculateOffsetForIndex(ctx, index) { - const state = ctx.state; - return index !== void 0 ? state.positions[index] || 0 : 0; -+// src/core/initialScrollSession.ts -+var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -+function hasInitialScrollSessionCompletion(completion) { -+ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); - } -- --// src/core/getStartOffsetAdjustment.ts --function getStartOffsetAdjustment(ctx) { -- const { state } = ctx; -- const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; -- return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); -+function clearInitialScrollSession(state) { -+ state.initialScrollSession = void 0; -+ return void 0; - } +-} - --// src/utils/getId.ts --function getId(state, index) { -- const { data, keyExtractor } = state.props; -- if (!data) { -- return ""; -- } -- const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; -- const id = ret; -- state.idCache[index] = id; -- return id; -+function createInitialScrollSession(options) { -+ const { bootstrap, completion, kind, previousDataLength } = options; -+ return kind === "offset" ? { -+ completion, -+ kind, -+ previousDataLength -+ } : { -+ bootstrap, -+ completion, -+ kind, -+ previousDataLength -+ }; - } -- --// src/core/updateContentMetricsState.ts --function getRawContentLength(ctx) { -- var _a3, _b, _c; -- const { state, values } = ctx; -- return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); -+function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { -+ var _a4, _b2; -+ if (!state.initialScrollSession) { -+ state.initialScrollSession = createInitialScrollSession({ -+ completion: {}, -+ kind, -+ previousDataLength: 0 -+ }); -+ } else if (state.initialScrollSession.kind !== kind) { -+ state.initialScrollSession = createInitialScrollSession({ -+ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, -+ completion: state.initialScrollSession.completion, -+ kind, -+ previousDataLength: state.initialScrollSession.previousDataLength + // src/core/getStartOffsetAdjustment.ts + function getStartOffsetAdjustment(ctx) { + const { state } = ctx; +@@ -1234,58 +635,273 @@ function getItemSizeAtIndex(ctx, index) { + if (index === void 0 || index < 0) { + return void 0; + } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; ++ } ++ if (index !== void 0) { ++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); ++ if (startOffsetAdjustment) { ++ offset += startOffsetAdjustment; ++ } ++ } ++ if (viewPosition !== void 0 && index !== void 0) { ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return offset; ++ } ++ const isOutOfBounds = index < 0 || index >= dataLength; ++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; ++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); ++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); ++ const trailingInset = getContentInsetEnd(ctx); ++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); ++ if (!isOutOfBounds && index === state.props.data.length - 1) { ++ const footerSize = peek$(ctx, "footerSize") || 0; ++ offset += footerSize; ++ } ++ } ++ return offset; ++} ++ ++// src/core/clampScrollOffset.ts ++function clampScrollOffset(ctx, offset, scrollTarget) { ++ const state = ctx.state; ++ const contentSize = getContentSize(ctx); ++ let clampedOffset = offset; ++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { ++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); ++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; ++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; ++ const maxOffset = baseMaxOffset + extraEndOffset; ++ clampedOffset = Math.min(offset, maxOffset); ++ } ++ clampedOffset = Math.max(0, clampedOffset); ++ return clampedOffset; ++} ++ ++// src/utils/checkThreshold.ts ++var HYSTERESIS_MULTIPLIER = 1.3; ++function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { ++ const absDistance = Math.abs(distance); ++ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; ++} ++var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { ++ const absDistance = Math.abs(distance); ++ const within = atThreshold || threshold > 0 && absDistance <= threshold; ++ const updateSnapshot = () => { ++ setSnapshot({ ++ atThreshold, ++ contentSize: context.contentSize, ++ dataLength: context.dataLength, ++ scrollPosition: context.scrollPosition + }); ++ }; ++ if (!wasReached) { ++ if (!within) { ++ return false; ++ } ++ onReached(distance); ++ updateSnapshot(); ++ return true; + } -+ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; -+ return state.initialScrollSession.completion; - } --function getAlignItemsAtEndPadding(ctx) { -- const { state } = ctx; -- const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; -- return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; --} --function updateContentMetricsState(ctx) { -- const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; -- const nextPadding = getAlignItemsAtEndPadding(ctx); -- if (previousPadding !== nextPadding) { -- set$(ctx, "alignItemsAtEndPadding", nextPadding); -- } --} -- --// src/core/addTotalSize.ts --function addTotalSize(ctx, key, add, notifyTotalSize = true) { -- const state = ctx.state; -- const prevTotalSize = state.totalSize; -- let totalSize = state.totalSize; -- if (key === null) { -- totalSize = add; -- if (state.timeoutSetPaddingTop) { -- clearTimeout(state.timeoutSetPaddingTop); -- state.timeoutSetPaddingTop = void 0; -+var initialScrollCompletion = { -+ didDispatchNativeScroll(state) { -+ var _a3, _b; -+ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); -+ }, -+ didRetrySilentInitialScroll(state) { -+ var _a3, _b; -+ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); -+ }, -+ markInitialScrollNativeDispatch(state) { -+ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; -+ }, -+ markSilentInitialScrollRetry(state) { -+ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; -+ }, -+ resetFlags(state) { -+ if (!state.initialScrollSession) { -+ return; - } -- } else { -- totalSize += add; -+ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); -+ completion.didDispatchNativeScroll = void 0; -+ completion.didRetrySilentInitialScroll = void 0; - } -- if (prevTotalSize !== totalSize) { -- { -- state.pendingTotalSize = void 0; -- state.totalSize = totalSize; -- if (notifyTotalSize) { -- set$(ctx, "totalSize", totalSize); -- } -- updateContentMetricsState(ctx); -+}; -+var initialScrollWatchdog = { -+ clear(state) { -+ initialScrollWatchdog.set(state, void 0); -+ }, -+ didReachTarget(newScroll, watchdog) { -+ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); -+ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ get(state) { -+ var _a3, _b; -+ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; -+ }, -+ hasNonZeroTargetOffset(targetOffset) { -+ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ isAtZeroTargetOffset(targetOffset) { -+ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ set(state, watchdog) { -+ var _a3, _b; -+ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { -+ return; - } -- } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { -- set$(ctx, "totalSize", totalSize); -+ const completion = ensureInitialScrollSessionCompletion(state); -+ completion.watchdog = watchdog ? { -+ startScroll: watchdog.startScroll, -+ targetOffset: watchdog.targetOffset -+ } : void 0; - } --} -- --// src/core/setSize.ts --function setSize(ctx, itemKey, size, notifyTotalSize = true) { -- const state = ctx.state; -- const { sizes } = state; -- const previousSize = sizes.get(itemKey); -- const diff = previousSize !== void 0 ? size - previousSize : size; -- if (diff !== 0) { -- addTotalSize(ctx, itemKey, diff, notifyTotalSize); ++ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); ++ if (reset) { ++ setSnapshot(void 0); ++ return false; ++ } ++ if (within) { ++ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; ++ if (changed) { ++ updateSnapshot(); ++ } ++ } ++ return true; +}; -+function setInitialScrollSession(state, options = {}) { -+ var _a3, _b, _c, _d; -+ const existingSession = state.initialScrollSession; -+ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; -+ const completion = existingSession == null ? void 0 : existingSession.completion; -+ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; -+ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; -+ if (!kind) { -+ return clearInitialScrollSession(state); - } -- sizes.set(itemKey, size); --} -- --// src/utils/helpers.ts --function isFunction(obj) { -- return typeof obj === "function"; --} --function isArray(obj) { -- return Array.isArray(obj); --} --var warned = /* @__PURE__ */ new Set(); --function warnDevOnce(id, text) { -- if (IS_DEV && !warned.has(id)) { -- warned.add(id); -- console.warn(`[legend-list] ${text}`); -+ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { -+ return clearInitialScrollSession(state); - } -+ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; -+ state.initialScrollSession = createInitialScrollSession({ -+ bootstrap, -+ completion, -+ kind, -+ previousDataLength -+ }); -+ return state.initialScrollSession; - } --function roundSize(size) { -- return Math.floor(size * 8) / 8; --} --function isNullOrUndefined(value) { -- return value === null || value === void 0; --} --function getPadding(s, type) { -- var _a3, _b, _c; -- const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; -- return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; -+var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; -+var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; -+var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; -+function clearAdaptiveRenderExitTimeout(ctx) { -+ ctx.state.scheduledWork.cancel("adaptiveRender"); - } --function extractPadding(style, contentContainerStyle, type) { -- return getPadding(style, type) + getPadding(contentContainerStyle, type); -+function scheduleAdaptiveRenderExit(ctx, exitDelay) { ++ ++// src/utils/edgeReachedGate.ts ++function resetEdgeLatch(ctx, edge) { + const state = ctx.state; -+ clearAdaptiveRenderExitTimeout(ctx); -+ if (exitDelay <= 0) { -+ setAdaptiveRender(ctx, "normal", "scroll"); ++ if (edge === "start") { ++ state.isStartReached = false; ++ state.startReachedSnapshot = void 0; + } else { -+ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); ++ state.isEndReached = false; ++ state.endReachedSnapshot = void 0; + } - } --function findContainerId(ctx, key) { -+function setAdaptiveRender(ctx, mode, reason) { - var _a3, _b; -- const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); -- if (directMatch !== void 0) { -- return directMatch; -- } -- const numContainers = peek$(ctx, "numContainers"); -- for (let i = 0; i < numContainers; i++) { -- const itemKey = peek$(ctx, `containerItemKey${i}`); -- if (itemKey === key) { -- return i; -- } -+ const previousMode = peek$(ctx, "adaptiveRender"); -+ if (previousMode !== mode) { -+ set$(ctx, "adaptiveRender", mode); -+ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); - } -- return -1; - } -- --// src/utils/getItemSize.ts --function getKnownOrFixedSize(ctx, key, index, data, resolved) { -+function resetAdaptiveRender(ctx) { - var _a3, _b; -- const state = ctx.state; -- const { getFixedItemSize, getItemType } = state.props; -- let size = key ? state.sizesKnown.get(key) : void 0; -- if (size === void 0 && key && getFixedItemSize) { -- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; -- const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); -- if (fixedSize !== void 0) { -- size = fixedSize + ctx.scrollAxisGap; -- state.sizesKnown.set(key, size); -- } -+ clearAdaptiveRenderExitTimeout(ctx); -+ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; -+ if (peek$(ctx, "adaptiveRender") !== mode) { -+ setAdaptiveRender(ctx, mode, "initial"); - } -- return size; --} --function getKnownOrFixedItemSize(ctx, index) { -- const key = getId(ctx.state, index); -- return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); - } --function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { -- for (let index = startIndex; index <= endIndex; index++) { -- if (getKnownOrFixedItemSize(ctx, index) === void 0) { -- return false; -+function updateAdaptiveRender(ctx, scrollVelocity, options) { -+ var _a3, _b, _c; ++} ++function resetSharedEdgeGateIfOutsideHysteresis(ctx) { + const state = ctx.state; -+ const adaptiveRender = state.props.adaptiveRender; -+ const currentMode = peek$(ctx, "adaptiveRender"); -+ if (peek$(ctx, "readyToRender")) { -+ if (adaptiveRender) { -+ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; -+ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; -+ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; -+ const threshold = currentMode === "light" ? exitVelocity : enterVelocity; -+ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; -+ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; -+ if (nextMode !== previousMode) { -+ if (nextMode === "light") { -+ setAdaptiveRender(ctx, "light", "scroll"); -+ scheduleAdaptiveRenderExit(ctx, exitDelay); -+ } else if (currentMode === "light") { -+ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ if (!state.edgeReachedGate) { ++ return; ++ } ++ const contentSize = getContentSize(ctx); ++ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); ++ const isContentLess = contentSize < state.scrollLength; ++ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; ++ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; ++ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); ++ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); ++ if (isOutsideStart && isOutsideEnd) { ++ state.edgeReachedGate = void 0; ++ } ++} ++function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { ++ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; ++} ++function markReachedEdge(ctx) { ++ ctx.state.edgeReachedGate = "closed"; ++} ++function prepareReachedEdgeForNextUserScroll(ctx) { ++ if (ctx.state.edgeReachedGate) { ++ ctx.state.edgeReachedGate = "prepared"; ++ } ++} ++function beginReachedEdgeUserScroll(ctx, scrollDelta) { ++ const state = ctx.state; ++ if (state.edgeReachedGate !== "prepared") { ++ return void 0; ++ } ++ const allowedEdge = scrollDelta < 0 ? "start" : "end"; ++ state.edgeReachedGate = "closed"; ++ resetEdgeLatch(ctx, allowedEdge); ++ return allowedEdge; ++} ++ ++// src/utils/hasActiveInitialScroll.ts ++function hasActiveInitialScroll(state) { ++ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; ++} ++ ++// src/utils/checkAtBottom.ts ++function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { ++ var _a3; ++ const state = ctx.state; ++ if (!state) { ++ return; ++ } ++ const { ++ queuedInitialLayout, ++ scrollLength, ++ scroll, ++ maintainingScrollAtEnd, ++ props: { maintainScrollAtEndThreshold, onEndReachedThreshold } ++ } = state; ++ const contentSize = getContentSize(ctx); ++ resetSharedEdgeGateIfOutsideHysteresis(ctx); ++ if (contentSize > 0 && queuedInitialLayout) { ++ const insetEnd = getContentInsetEnd(ctx); ++ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; ++ const isContentLess = contentSize < scrollLength; ++ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); ++ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); ++ set$( ++ ctx, ++ "isWithinMaintainScrollAtEndThreshold", ++ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength ++ ); ++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; ++ if (!shouldSkipThresholdChecks) { ++ state.isEndReached = checkThreshold( ++ distanceFromEnd, ++ isContentLess, ++ onEndReachedThreshold * scrollLength, ++ state.isEndReached, ++ state.endReachedSnapshot, ++ { ++ contentSize, ++ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, ++ scrollPosition: scroll ++ }, ++ (distance) => { ++ var _a4, _b; ++ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { ++ markReachedEdge(ctx); ++ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); ++ } ++ }, ++ (snapshot) => { ++ state.endReachedSnapshot = snapshot; ++ } ++ ); ++ } ++ } ++} ++ ++// src/utils/checkAtTop.ts ++function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { ++ const state = ctx == null ? void 0 : ctx.state; ++ if (!state) { ++ return; ++ } ++ const { ++ isStartReached, ++ props: { data, onStartReachedThreshold }, ++ scroll, ++ scrollLength, ++ startReachedSnapshot, ++ totalSize ++ } = state; ++ const dataLength = data.length; ++ const threshold = onStartReachedThreshold * scrollLength; ++ resetSharedEdgeGateIfOutsideHysteresis(ctx); ++ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { ++ state.isStartReached = false; ++ state.startReachedSnapshot = void 0; ++ } ++ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); ++ set$(ctx, "isNearStart", scroll <= threshold); ++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; ++ if (!shouldSkipThresholdChecks) { ++ state.isStartReached = checkThreshold( ++ scroll, ++ false, ++ threshold, ++ state.isStartReached, ++ startReachedSnapshot, ++ { ++ contentSize: totalSize, ++ dataLength, ++ scrollPosition: scroll ++ }, ++ (distance) => { ++ var _a3, _b; ++ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { ++ markReachedEdge(ctx); ++ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); + } ++ }, ++ (snapshot) => { ++ state.startReachedSnapshot = snapshot; + } -+ } else { -+ resetAdaptiveRender(ctx); - } - } -- return true; ++ ); ++ } } --function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { -- var _a3, _b, _c, _d; -+ -+// src/core/doMaintainScrollAtEnd.ts -+function doMaintainScrollAtEnd(ctx) { - const state = ctx.state; - const { -- sizes, -- averageSizes, -- props: { estimatedItemSize, getItemType }, -- scrollingTo -+ didContainersLayout, -+ pendingNativeMVCPAdjust, -+ refScroller, -+ props: { maintainScrollAtEnd } - } = state; -- const sizeKnown = state.sizesKnown.get(key); -- if (sizeKnown !== void 0) { -- return sizeKnown; -+ const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); -+ const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout); -+ if (pendingNativeMVCPAdjust) { -+ state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd; -+ return false; - } -- let size; -- const renderedSize = sizes.get(key); -- if (preferCachedSize) { -- if (renderedSize !== void 0) { -- return renderedSize; -- } -- } -- size = getKnownOrFixedSize(ctx, key, index, data, resolved); -- if (size !== void 0) { -- setSize(ctx, key, size, notifyTotalSize); -- return size; -- } -- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; -- if (useAverageSize && !scrollingTo) { -- const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; -- if (averageSizeForType !== void 0) { -- size = roundSize(averageSizeForType); -- } -- } -- if (size === void 0 && renderedSize !== void 0) { -- return renderedSize; -- } -- if (size === void 0 && useAverageSize && scrollingTo) { -- const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; -- if (averageSizeForType !== void 0) { -- size = roundSize(averageSizeForType); -- } -- } -- if (size === void 0) { -- size = estimatedItemSize + ctx.scrollAxisGap; -- } -- setSize(ctx, key, size, notifyTotalSize); -- return size; --} --function getItemSizeAtIndex(ctx, index) { -- if (index === void 0 || index < 0) { -- return void 0; -- } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); --} -- + -// src/core/calculateOffsetWithOffsetPosition.ts -function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { - var _a3; @@ -9556,11 +8370,19 @@ index 914d2da..c3ed202 100644 - } - } - return offset; --} -- ++// src/utils/checkThresholds.ts ++function checkThresholds(ctx, allowedEdge) { ++ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; ++ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); ++ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + } + -// src/core/clampScrollOffset.ts -function clampScrollOffset(ctx, offset, scrollTarget) { -- const state = ctx.state; ++// src/core/recalculateSettledScroll.ts ++function recalculateSettledScroll(ctx) { ++ var _a3, _b; + const state = ctx.state; - const contentSize = getContentSize(ctx); - let clampedOffset = offset; - if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { @@ -9569,176 +8391,200 @@ index 914d2da..c3ed202 100644 - const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; - const maxOffset = baseMaxOffset + extraEndOffset; - clampedOffset = Math.min(offset, maxOffset); -- } ++ if ((_a3 = state.props) == null ? void 0 : _a3.data) { ++ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); + } - clampedOffset = Math.max(0, clampedOffset); - return clampedOffset; --} -- --// src/core/finishScrollTo.ts --function finishScrollTo(ctx) { -- var _a3, _b; -- const state = ctx.state; -- if (state == null ? void 0 : state.scrollingTo) { -- cancelScrollCompletionChecks(state); -- const resolvePendingScroll = state.pendingScrollResolve; -- state.pendingScrollResolve = void 0; -- const scrollingTo = state.scrollingTo; -- state.scrollHistory.length = 0; -- state.scrollingTo = void 0; -- state.scrollTargetPinnedRange = void 0; -- if (state.pendingTotalSize !== void 0) { -- addTotalSize(ctx, null, state.pendingTotalSize); -- } -- { -- state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); -- } -- if (scrollingTo.isInitialScroll || state.initialScroll) { -- const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; -- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; -- finishInitialScroll(ctx, { -- onFinished: () => { -- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); -- }, -- preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, -- recalculateItems: true, -- schedulePreservedTargetClear: shouldPreserveResizeTarget, -- syncObservedOffset: isOffsetSession, -- waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame -- }); -- return; -- } -- recalculateSettledScroll(ctx); -- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); -- } --} -- --// src/core/doScrollTo.ts --var SCROLL_END_IDLE_MS = 80; --var SCROLL_END_MAX_MS = 1500; --var SMOOTH_SCROLL_DURATION_MS = 320; --var SCROLL_END_TARGET_EPSILON = 1; --function doScrollTo(ctx, params) { -- var _a3, _b; -- const state = ctx.state; -- const { animated, horizontal, offset } = params; -- state.scheduledWork.cancel("platformScrollCompletion"); -- const scroller = state.refScroller.current; -- const node = scroller == null ? void 0 : scroller.getScrollableNode(); -- if (!scroller || !node) { -- return; -- } -- const isAnimated = !!animated; -- const isHorizontal = !!horizontal; -- const contentSize = isHorizontal ? getContentSize(ctx) : void 0; -- const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; -- const top = isHorizontal ? 0 : offset; -- scroller.scrollTo({ animated: isAnimated, x: left, y: top }); -- if (isAnimated) { -- const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; -- listenForScrollEnd(ctx, { -- readOffset: () => scroller.getCurrentScrollOffset(), -- target, -- targetOffset: offset -- }); -- } else { -- state.scroll = offset; -- const targetToken = state.scrollingTo; -- state.scheduledWork.timeout( -- () => { -- if (targetToken === state.scrollingTo) { -- finishScrollTo(ctx); -- } -- }, -- 100, -- "platformScrollCompletion" -- ); -- } --} --function listenForScrollEnd(ctx, params) { -- const { readOffset, target, targetOffset } = params; -- if (!target) { -- finishScrollTo(ctx); -- return; -- } -- const supportsScrollEnd = "onscrollend" in target; -- let idleTimeout; -- let settled = false; -- const { scheduledWork, scrollingTo: targetToken } = ctx.state; -- const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); -- const cleanup = () => { -- target.removeEventListener("scroll", onScroll2); -- if (supportsScrollEnd) { -- target.removeEventListener("scrollend", onScrollEnd); -- } -- if (idleTimeout !== void 0) { -- clearTimeout(idleTimeout); -- } -- clearTimeout(maxTimeout); -- }; -- const cancel = () => { -- if (!settled) { -- settled = true; -- cleanup(); -- } -- }; -- const finish = (reason) => { -- if (settled) return; -- if (targetToken !== ctx.state.scrollingTo) { -- scheduledWork.cancel("platformScrollCompletion"); -- return; -- } -- const currentOffset = readOffset(); -- const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; -- if (reason === "scrollend" && !isNearTarget) { -- return; -- } -- scheduledWork.cancel("platformScrollCompletion"); -- finishScrollTo(ctx); -- }; -- const onScroll2 = () => { -- if (idleTimeout !== void 0) { -- clearTimeout(idleTimeout); -- } -- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); -- }; -- const onScrollEnd = () => finish("scrollend"); -- target.addEventListener("scroll", onScroll2); -- if (supportsScrollEnd) { -- target.addEventListener("scrollend", onScrollEnd); -- } else { -- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); -- } ++ checkThresholds(ctx); + } + + // src/core/finishScrollTo.ts +@@ -1422,7 +1038,182 @@ function listenForScrollEnd(ctx, params) { + } else { + idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); + } - scheduledWork.register("platformScrollCompletion", cancel); --} -- --// src/core/doMaintainScrollAtEnd.ts --function doMaintainScrollAtEnd(ctx) { -- const state = ctx.state; -- const { -- didContainersLayout, -- pendingNativeMVCPAdjust, -- refScroller, -- props: { maintainScrollAtEnd } -- } = state; -- const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); -- const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout); -- if (pendingNativeMVCPAdjust) { -- state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd; -- return false; -- } -- if (shouldMaintainScrollAtEnd) { -- state.pendingMaintainScrollAtEnd = false; -- const contentSize = getContentSize(ctx); -- if (contentSize < state.scrollLength) { -- state.scroll = 0; -+ if (shouldMaintainScrollAtEnd) { -+ state.pendingMaintainScrollAtEnd = false; -+ const contentSize = getContentSize(ctx); -+ if (contentSize < state.scrollLength) { -+ state.scroll = 0; - } - if (!state.maintainingScrollAtEnd) { - const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; ++ scheduledWork.register("platformScrollCompletion", cancel); ++} ++ ++// src/core/initialScrollSession.ts ++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; ++function hasInitialScrollSessionCompletion(completion) { ++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); ++} ++function clearInitialScrollSession(state) { ++ state.initialScrollSession = void 0; ++ return void 0; ++} ++function createInitialScrollSession(options) { ++ const { bootstrap, completion, kind, previousDataLength } = options; ++ return kind === "offset" ? { ++ completion, ++ kind, ++ previousDataLength ++ } : { ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }; ++} ++function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { ++ var _a4, _b2; ++ if (!state.initialScrollSession) { ++ state.initialScrollSession = createInitialScrollSession({ ++ completion: {}, ++ kind, ++ previousDataLength: 0 ++ }); ++ } else if (state.initialScrollSession.kind !== kind) { ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, ++ completion: state.initialScrollSession.completion, ++ kind, ++ previousDataLength: state.initialScrollSession.previousDataLength ++ }); ++ } ++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; ++ return state.initialScrollSession.completion; ++} ++var initialScrollCompletion = { ++ didDispatchNativeScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); ++ }, ++ didRetrySilentInitialScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); ++ }, ++ markInitialScrollNativeDispatch(state) { ++ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; ++ }, ++ markSilentInitialScrollRetry(state) { ++ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; ++ }, ++ resetFlags(state) { ++ if (!state.initialScrollSession) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); ++ completion.didDispatchNativeScroll = void 0; ++ completion.didRetrySilentInitialScroll = void 0; ++ } ++}; ++var initialScrollWatchdog = { ++ clear(state) { ++ initialScrollWatchdog.set(state, void 0); ++ }, ++ didReachTarget(newScroll, watchdog) { ++ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); ++ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ get(state) { ++ var _a3, _b; ++ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; ++ }, ++ hasNonZeroTargetOffset(targetOffset) { ++ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ isAtZeroTargetOffset(targetOffset) { ++ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ set(state, watchdog) { ++ var _a3, _b; ++ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state); ++ completion.watchdog = watchdog ? { ++ startScroll: watchdog.startScroll, ++ targetOffset: watchdog.targetOffset ++ } : void 0; ++ } ++}; ++function setInitialScrollSession(state, options = {}) { ++ var _a3, _b, _c, _d; ++ const existingSession = state.initialScrollSession; ++ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; ++ const completion = existingSession == null ? void 0 : existingSession.completion; ++ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; ++ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; ++ if (!kind) { ++ return clearInitialScrollSession(state); ++ } ++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { ++ return clearInitialScrollSession(state); ++ } ++ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }); ++ return state.initialScrollSession; ++} ++var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; ++function clearAdaptiveRenderExitTimeout(ctx) { ++ ctx.state.scheduledWork.cancel("adaptiveRender"); ++} ++function scheduleAdaptiveRenderExit(ctx, exitDelay) { ++ const state = ctx.state; ++ clearAdaptiveRenderExitTimeout(ctx); ++ if (exitDelay <= 0) { ++ setAdaptiveRender(ctx, "normal", "scroll"); ++ } else { ++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); ++ } ++} ++function setAdaptiveRender(ctx, mode, reason) { ++ var _a3, _b; ++ const previousMode = peek$(ctx, "adaptiveRender"); ++ if (previousMode !== mode) { ++ set$(ctx, "adaptiveRender", mode); ++ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); ++ } ++} ++function resetAdaptiveRender(ctx) { ++ var _a3, _b; ++ clearAdaptiveRenderExitTimeout(ctx); ++ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; ++ if (peek$(ctx, "adaptiveRender") !== mode) { ++ setAdaptiveRender(ctx, mode, "initial"); ++ } ++} ++function updateAdaptiveRender(ctx, scrollVelocity, options) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ const adaptiveRender = state.props.adaptiveRender; ++ const currentMode = peek$(ctx, "adaptiveRender"); ++ if (peek$(ctx, "readyToRender")) { ++ if (adaptiveRender) { ++ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; ++ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; ++ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; ++ const threshold = currentMode === "light" ? exitVelocity : enterVelocity; ++ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; ++ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; ++ if (nextMode !== previousMode) { ++ if (nextMode === "light") { ++ setAdaptiveRender(ctx, "light", "scroll"); ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } else if (currentMode === "light") { ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } ++ } ++ } else { ++ resetAdaptiveRender(ctx); ++ } ++ } + } + + // src/core/doMaintainScrollAtEnd.ts +@@ -1451,6 +1242,7 @@ function doMaintainScrollAtEnd(ctx) { const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; state.maintainingScrollAtEnd = pendingState; @@ -9746,27 +8592,10 @@ index 914d2da..c3ed202 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1809,23 +1601,44 @@ function prepareMVCP(ctx, dataChanged) { +@@ -1809,6 +1601,27 @@ function prepareMVCP(ctx, dataChanged) { } } --// src/utils/getScrollVelocity.ts --var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; --var SCROLL_VELOCITY_HALF_LIFE_MS = 200; --var getScrollVelocity = (state) => { -- const { scrollHistory } = state; -- const newestIndex = scrollHistory.length - 1; -- if (newestIndex < 1) { -- return 0; -- } -- const newest = scrollHistory[newestIndex]; -- if (Date.now() - newest.time > MAX_SCROLL_VELOCITY_WINDOW_MS) { -- return 0; -- } -- let direction = 0; -- let weightedVelocity = 0; -- let totalWeight = 0; -- for (let i = newestIndex; i > 0; i--) { +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { @@ -9788,269 +8617,39 @@ index 914d2da..c3ed202 100644 + }, "fullDrawDistancePrewarm"); +} + -+// src/utils/getScrollVelocity.ts -+var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; -+var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -+var getScrollVelocity = (state) => { -+ const { scrollHistory } = state; -+ const newestIndex = scrollHistory.length - 1; -+ if (newestIndex < 1) { -+ return 0; -+ } -+ const newest = scrollHistory[newestIndex]; -+ if (Date.now() - newest.time > MAX_SCROLL_VELOCITY_WINDOW_MS) { -+ return 0; -+ } -+ let direction = 0; -+ let weightedVelocity = 0; -+ let totalWeight = 0; -+ for (let i = newestIndex; i > 0; i--) { - const current = scrollHistory[i]; - const previous = scrollHistory[i - 1]; - const scrollDiff = current.scroll - previous.scroll; -@@ -1844,281 +1657,604 @@ var getScrollVelocity = (state) => { - if (scrollDiff === 0 || timeDiff <= 0) { - continue; + // src/utils/getScrollVelocity.ts + var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; + var SCROLL_VELOCITY_HALF_LIFE_MS = 200; +@@ -2055,70 +1868,395 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { + if (nextTop === void 0 || nextTop > viewportEnd) { + break; } -- const age = newest.time - current.time; -- const weight = Math.exp(-age / SCROLL_VELOCITY_HALF_LIFE_MS); -- weightedVelocity += scrollDiff / timeDiff * weight; -- totalWeight += weight; +- end++; - } -- return totalWeight > 0 ? weightedVelocity / totalWeight : 0; --}; -- --// src/utils/hasActiveMVCPAnchorLock.ts --function hasActiveMVCPAnchorLock(state) { -- const lock = state.mvcpAnchorLock; -- if (!lock) { -- return false; -+ const age = newest.time - current.time; -+ const weight = Math.exp(-age / SCROLL_VELOCITY_HALF_LIFE_MS); -+ weightedVelocity += scrollDiff / timeDiff * weight; -+ totalWeight += weight; -+ } -+ return totalWeight > 0 ? weightedVelocity / totalWeight : 0; -+}; -+ -+// src/utils/hasActiveMVCPAnchorLock.ts -+function hasActiveMVCPAnchorLock(state) { -+ const lock = state.mvcpAnchorLock; -+ if (!lock) { -+ return false; -+ } -+ if (Date.now() > lock.expiresAt) { -+ state.mvcpAnchorLock = void 0; -+ return false; -+ } -+ return true; -+} -+ -+// src/utils/isInMVCPActiveMode.ts -+function isInMVCPActiveMode(state) { -+ return state.dataChangeNeedsScrollUpdate || hasActiveMVCPAnchorLock(state); -+} -+ -+// src/core/updateScroll.ts -+function updateScroll(ctx, newScroll, forceUpdate, options) { -+ var _a3; -+ const state = ctx.state; -+ const { ignoreScrollFromMVCP, lastScrollAdjustForHistory, scrollAdjustHandler, scrollHistory, scrollingTo } = state; -+ const prevScroll = state.scroll; -+ if ((options == null ? void 0 : options.markHasScrolled) !== false) { -+ state.hasScrolled = true; -+ } -+ const currentTime = Date.now(); -+ state.lastBatchingAction = currentTime; -+ const adjust = scrollAdjustHandler.getAdjust(); -+ const adjustChanged = lastScrollAdjustForHistory !== void 0 && Math.abs(adjust - lastScrollAdjustForHistory) > 0.1; -+ if (adjustChanged) { -+ scrollHistory.length = 0; -+ } -+ state.lastScrollAdjustForHistory = adjust; -+ if (scrollingTo === void 0 && !(scrollHistory.length === 0 && newScroll === state.scroll)) { -+ if (!adjustChanged) { -+ scrollHistory.push({ scroll: newScroll, time: currentTime }); -+ } -+ } -+ if (scrollHistory.length > 5) { -+ scrollHistory.shift(); -+ } -+ if (ignoreScrollFromMVCP && !scrollingTo) { -+ const { lt, gt } = ignoreScrollFromMVCP; -+ if (lt && newScroll < lt || gt && newScroll > gt) { -+ state.ignoreScrollFromMVCPIgnored = true; -+ return; -+ } -+ } -+ state.scrollPrev = prevScroll; -+ state.scrollPrevTime = state.scrollTime; -+ state.scroll = newScroll; -+ state.scrollTime = currentTime; -+ const scrollDelta = Math.abs(newScroll - prevScroll); -+ const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; -+ const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; -+ const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); -+ const scrollLength = state.scrollLength; -+ const isLargeUserScrollJump = scrollLength > 0 && scrollingTo === void 0 && scrollDelta > scrollLength && !state.pendingNativeMVCPAdjust; -+ const scrollVelocity = getScrollVelocity(state); -+ updateAdaptiveRender(ctx, scrollVelocity, { forceLight: isLargeUserScrollJump }); -+ const lastCalculated = state.scrollLastCalculate; -+ const useAggressiveItemRecalculation = isInMVCPActiveMode(state); -+ const shouldUpdate = useAggressiveItemRecalculation || didResolvePendingNativeMVCPAdjust || allowedEdge !== void 0 || forceUpdate || lastCalculated === void 0 || Math.abs(state.scroll - lastCalculated) > 2; -+ if (shouldUpdate) { -+ state.scrollLastCalculate = state.scroll; -+ state.ignoreScrollFromMVCPIgnored = false; -+ state.lastScrollDelta = scrollDelta; -+ const runCalculateItems = () => { -+ var _a4; -+ const calculateItemsParams = { -+ doMVCP: scrollingTo !== void 0, -+ scrollVelocity -+ }; -+ if (isLargeUserScrollJump) { -+ calculateItemsParams.drawDistanceMode = "visible-first"; -+ } -+ (_a4 = state.triggerCalculateItemsInView) == null ? void 0 : _a4.call(state, calculateItemsParams); -+ checkThresholds(ctx, allowedEdge); -+ }; -+ if (isLargeUserScrollJump) { -+ state.mvcpAnchorLock = void 0; -+ state.pendingNativeMVCPAdjust = void 0; -+ state.userScrollAnchorReset = { keys: /* @__PURE__ */ new Set() }; -+ state.scheduledWork.cancel("mvcpRecalculate"); -+ ReactDOM.flushSync(runCalculateItems); -+ scheduleFullDrawDistancePrewarm(ctx); -+ } else { -+ runCalculateItems(); -+ } -+ const shouldMaintainScrollAtEndAfterPendingSettle = !!state.pendingMaintainScrollAtEnd || !!((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onDataChange); -+ if (didResolvePendingNativeMVCPAdjust && shouldMaintainScrollAtEndAfterPendingSettle) { -+ state.pendingMaintainScrollAtEnd = false; -+ doMaintainScrollAtEnd(ctx); -+ } -+ state.dataChangeNeedsScrollUpdate = false; -+ state.lastScrollDelta = 0; -+ } -+} -+ -+// src/core/scrollTo.ts -+function getAverageSizeSnapshot(state) { -+ if (Object.keys(state.averageSizes).length === 0) { -+ return void 0; -+ } -+ const snapshot = {}; -+ for (const itemType in state.averageSizes) { -+ const averages = state.averageSizes[itemType]; -+ snapshot[itemType] = averages.avg; +- return { end, start }; +-} +-function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); +- if (range) { +- ctx.state.scrollTargetPinnedRange = range; +- ctx.state.scrollForNextCalculateItemsInView = void 0; +- } else { +- ctx.state.scrollTargetPinnedRange = void 0; ++ end++; + } -+ return snapshot; ++ return { end, start }; +} -+function syncInitialScrollNativeWatchdog(state, options) { -+ var _a3; -+ const { isInitialScroll, requestedOffset, targetOffset } = options; -+ const existingWatchdog = initialScrollWatchdog.get(state); -+ const shouldWatchInitialNativeScroll = !state.didFinishInitialScroll && (isInitialScroll || !!existingWatchdog) && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); -+ const shouldClearInitialNativeScrollWatchdog = !state.didFinishInitialScroll && !!existingWatchdog && initialScrollWatchdog.isAtZeroTargetOffset(requestedOffset); -+ if (shouldWatchInitialNativeScroll) { -+ state.hasScrolled = false; -+ initialScrollWatchdog.set(state, { -+ startScroll: (_a3 = existingWatchdog == null ? void 0 : existingWatchdog.startScroll) != null ? _a3 : state.scroll, -+ targetOffset -+ }); -+ return; -+ } -+ if (shouldClearInitialNativeScrollWatchdog) { -+ initialScrollWatchdog.clear(state); ++function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { ++ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); ++ if (range) { ++ ctx.state.scrollTargetPinnedRange = range; ++ ctx.state.scrollForNextCalculateItemsInView = void 0; ++ } else { ++ ctx.state.scrollTargetPinnedRange = void 0; + } +} -+function findPositionIndexAtOrBeforeOffset(ctx, offset) { -+ const state = ctx.state; -+ const dataLength = state.props.data.length; -+ let low = 0; -+ let high = dataLength - 1; -+ let match; -+ while (low <= high) { -+ const mid = Math.floor((low + high) / 2); -+ const top = state.positions[mid]; -+ if (top === void 0) { -+ high = mid - 1; -+ } else { -+ if (top <= offset) { -+ match = mid; -+ low = mid + 1; -+ } else { -+ high = mid - 1; -+ } -+ } -+ } -+ return match; -+} -+function getItemBottom(ctx, index) { -+ var _a3; -+ const top = ctx.state.positions[index]; -+ if (top === void 0) { -+ return void 0; -+ } -+ const itemSize = (_a3 = getItemSizeAtIndex(ctx, index)) != null ? _a3 : 0; -+ return top + (Number.isFinite(itemSize) ? itemSize : 0); -+} -+function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { -+ const state = ctx.state; -+ const dataLength = state.props.data.length; -+ if (dataLength === 0) { -+ return void 0; -+ } -+ const viewportStart = Math.max(0, targetOffset); -+ const viewportEnd = Math.max(viewportStart, targetOffset + state.scrollLength); -+ let start = targetIndex !== void 0 ? Math.max(0, Math.min(dataLength - 1, targetIndex)) : findPositionIndexAtOrBeforeOffset(ctx, viewportStart); -+ if (start === void 0) { -+ return void 0; -+ } -+ if (targetIndex !== void 0 && state.positions[start] === void 0) { -+ return { end: start, start }; -+ } -+ if (targetIndex === void 0) { -+ const startBottom = getItemBottom(ctx, start); -+ if (startBottom === void 0 || startBottom <= viewportStart) { -+ return void 0; -+ } -+ } -+ while (start > 0) { -+ const top = state.positions[start]; -+ if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { -+ break; -+ } -+ start--; -+ } -+ while (start > 0) { -+ const previousBottom = getItemBottom(ctx, start - 1); -+ if (previousBottom === void 0 || previousBottom <= viewportStart) { -+ break; -+ } -+ start--; -+ } -+ let end = start; -+ while (end + 1 < dataLength) { -+ const nextTop = state.positions[end + 1]; -+ if (nextTop === void 0 || nextTop > viewportEnd) { -+ break; -+ } -+ end++; -+ } -+ return { end, start }; -+} -+function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -+ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -+ if (range) { -+ ctx.state.scrollTargetPinnedRange = range; -+ ctx.state.scrollForNextCalculateItemsInView = void 0; -+ } else { -+ ctx.state.scrollTargetPinnedRange = void 0; -+ } -+} -+function scrollTo(ctx, params) { -+ var _a3, _b, _c; ++function scrollTo(ctx, params) { ++ var _a3, _b, _c; + const state = ctx.state; + const { noScrollingTo, forceScroll, ...scrollTarget } = params; + const { @@ -10103,30 +8702,23 @@ index 914d2da..c3ed202 100644 + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); + } - } -- if (Date.now() > lock.expiresAt) { -- state.mvcpAnchorLock = void 0; -- return false; ++ } + if (forceScroll || !isInitialScroll || Platform.OS === "android") { + doScrollTo(ctx, { animated, horizontal, offset }); + } else { + state.scroll = offset; - } -- return true; - } - --// src/utils/isInMVCPActiveMode.ts --function isInMVCPActiveMode(state) { -- return state.dataChangeNeedsScrollUpdate || hasActiveMVCPAnchorLock(state); ++ } ++} ++ +// src/core/scrollTargetSettle.ts -+var SETTLE_POSITION_EPSILON = 0.5; ++var SETTLE_POSITION_EPSILON = 1; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; +var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; -+function releaseScrollTargetForUserInteraction(ctx) { -+ if (ctx.state.scrollTargetSettle) { -+ clearScrollTargetSettle(ctx.state); ++function releaseScrollTargetForUserInteraction(state) { ++ if (state.scrollTargetSettle) { ++ clearScrollTargetSettle(state); + } +} +function clearScrollTargetSettle(state) { @@ -10143,6 +8735,7 @@ index 914d2da..c3ed202 100644 + clearScrollTargetSettle(state); + return; + } ++ clearScrollTargetSettle(state); + const now = Date.now(); + state.scrollTargetSettle = { + corrections: 0, @@ -10155,48 +8748,28 @@ index 914d2da..c3ed202 100644 + viewPosition + }; + state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline"); - } -- --// src/core/updateScroll.ts --function updateScroll(ctx, newScroll, forceUpdate, options) { ++} +function getSettleTargetOffset(ctx, settle, index, position) { + const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; + return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} +function applyScrollTargetCorrection(ctx, id) { - var _a3; - const state = ctx.state; -- const { ignoreScrollFromMVCP, lastScrollAdjustForHistory, scrollAdjustHandler, scrollHistory, scrollingTo } = state; -- const prevScroll = state.scroll; -- if ((options == null ? void 0 : options.markHasScrolled) !== false) { -- state.hasScrolled = true; ++ var _a3; ++ const state = ctx.state; + const settle = state.scrollTargetSettle; + if (!settle || settle.id !== id) { + return; - } -- const currentTime = Date.now(); -- state.lastBatchingAction = currentTime; -- const adjust = scrollAdjustHandler.getAdjust(); -- const adjustChanged = lastScrollAdjustForHistory !== void 0 && Math.abs(adjust - lastScrollAdjustForHistory) > 0.1; -- if (adjustChanged) { -- scrollHistory.length = 0; ++ } + const index = state.indexByKey.get(id); + const position = index === void 0 ? void 0 : state.positions[index]; + if (index === void 0 || position === void 0 || Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return; - } -- state.lastScrollAdjustForHistory = adjust; -- if (scrollingTo === void 0 && !(scrollHistory.length === 0 && newScroll === state.scroll)) { -- if (!adjustChanged) { -- scrollHistory.push({ scroll: newScroll, time: currentTime }); -- } ++ } + if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { + clearScrollTargetSettle(state); + return; - } -- if (scrollHistory.length > 5) { -- scrollHistory.shift(); ++ } + settle.corrections++; + settle.measuredIndex = void 0; + scrollTo(ctx, { @@ -10219,65 +8792,21 @@ index 914d2da..c3ed202 100644 + const settle = state.scrollTargetSettle; + if (!settle) { + return false; - } -- if (ignoreScrollFromMVCP && !scrollingTo) { -- const { lt, gt } = ignoreScrollFromMVCP; -- if (lt && newScroll < lt || gt && newScroll > gt) { -- state.ignoreScrollFromMVCPIgnored = true; -- return; ++ } + const measured = options == null ? void 0 : options.minIndexSizeChanged; + if (measured !== void 0) { + settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); ++ settle.expiresAt = Date.now() + SETTLE_TTL_MS; + } + if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return false; - } ++ } + settle.quietPasses = 0; + settle.expiresAt = Date.now() + SETTLE_TTL_MS; + return false; - } -- state.scrollPrev = prevScroll; -- state.scrollPrevTime = state.scrollTime; -- state.scroll = newScroll; -- state.scrollTime = currentTime; -- const scrollDelta = Math.abs(newScroll - prevScroll); -- const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; -- const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; -- const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); -- const scrollLength = state.scrollLength; -- const isLargeUserScrollJump = scrollLength > 0 && scrollingTo === void 0 && scrollDelta > scrollLength && !state.pendingNativeMVCPAdjust; -- const scrollVelocity = getScrollVelocity(state); -- updateAdaptiveRender(ctx, scrollVelocity, { forceLight: isLargeUserScrollJump }); -- const lastCalculated = state.scrollLastCalculate; -- const useAggressiveItemRecalculation = isInMVCPActiveMode(state); -- const shouldUpdate = useAggressiveItemRecalculation || didResolvePendingNativeMVCPAdjust || allowedEdge !== void 0 || forceUpdate || lastCalculated === void 0 || Math.abs(state.scroll - lastCalculated) > 2; -- if (shouldUpdate) { -- state.scrollLastCalculate = state.scroll; -- state.ignoreScrollFromMVCPIgnored = false; -- state.lastScrollDelta = scrollDelta; -- const runCalculateItems = () => { -- var _a4; -- const calculateItemsParams = { -- doMVCP: scrollingTo !== void 0, -- scrollVelocity -- }; -- if (isLargeUserScrollJump) { -- calculateItemsParams.drawDistanceMode = "visible-first"; -- } -- (_a4 = state.triggerCalculateItemsInView) == null ? void 0 : _a4.call(state, calculateItemsParams); -- checkThresholds(ctx, allowedEdge); -- }; -- if (isLargeUserScrollJump) { -- state.mvcpAnchorLock = void 0; -- state.pendingNativeMVCPAdjust = void 0; -- state.userScrollAnchorReset = { keys: /* @__PURE__ */ new Set() }; -- state.scheduledWork.cancel("mvcpRecalculate"); -- ReactDOM.flushSync(runCalculateItems); -- scheduleFullDrawDistancePrewarm(ctx); -- } else { -- runCalculateItems(); ++ } + const index = state.indexByKey.get(settle.id); + const position = index === void 0 ? void 0 : state.positions[index]; + if (index === void 0 || position === void 0) { @@ -10299,11 +8828,7 @@ index 914d2da..c3ed202 100644 + settle.quietPasses++; + if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { + clearScrollTargetSettle(state); - } -- const shouldMaintainScrollAtEndAfterPendingSettle = !!state.pendingMaintainScrollAtEnd || !!((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onDataChange); -- if (didResolvePendingNativeMVCPAdjust && shouldMaintainScrollAtEndAfterPendingSettle) { -- state.pendingMaintainScrollAtEnd = false; -- doMaintainScrollAtEnd(ctx); ++ } + return false; + } + settle.quietPasses = 0; @@ -10342,9 +8867,7 @@ index 914d2da..c3ed202 100644 + nativeEvent: { + ...event.nativeEvent, + contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } - } -- state.dataChangeNeedsScrollUpdate = false; -- state.lastScrollDelta = 0; ++ } + }; +} +function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { @@ -10361,13 +8884,9 @@ index 914d2da..c3ed202 100644 + (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 + ) + ); - } - } - --// src/core/scrollTo.ts --function getAverageSizeSnapshot(state) { -- if (Object.keys(state.averageSizes).length === 0) { -- return void 0; ++ } ++} ++ +// src/utils/setInitialRenderState.ts +function resetInitialRenderState(ctx, { + resetLayout, @@ -10377,31 +8896,13 @@ index 914d2da..c3ed202 100644 + if (resetLayout) { + state.didContainersLayout = false; + state.queuedInitialLayout = false; - } -- const snapshot = {}; -- for (const itemType in state.averageSizes) { -- const averages = state.averageSizes[itemType]; -- snapshot[itemType] = averages.avg; ++ } + if (resetInitialScroll) { + state.didFinishInitialScroll = false; - } -- return snapshot; ++ } + set$(ctx, "readyToRender", false); + resetAdaptiveRender(ctx); - } --function syncInitialScrollNativeWatchdog(state, options) { -- var _a3; -- const { isInitialScroll, requestedOffset, targetOffset } = options; -- const existingWatchdog = initialScrollWatchdog.get(state); -- const shouldWatchInitialNativeScroll = !state.didFinishInitialScroll && (isInitialScroll || !!existingWatchdog) && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); -- const shouldClearInitialNativeScrollWatchdog = !state.didFinishInitialScroll && !!existingWatchdog && initialScrollWatchdog.isAtZeroTargetOffset(requestedOffset); -- if (shouldWatchInitialNativeScroll) { -- state.hasScrolled = false; -- initialScrollWatchdog.set(state, { -- startScroll: (_a3 = existingWatchdog == null ? void 0 : existingWatchdog.startScroll) != null ? _a3 : state.scroll, -- targetOffset -- }); -- return; ++} +function setInitialRenderState(ctx, { + didLayout, + didInitialScroll @@ -10413,30 +8914,10 @@ index 914d2da..c3ed202 100644 + } = state; + if (didLayout) { + state.didContainersLayout = true; - } -- if (shouldClearInitialNativeScrollWatchdog) { -- initialScrollWatchdog.clear(state); ++ } + if (didInitialScroll) { + state.didFinishInitialScroll = true; - } --} --function findPositionIndexAtOrBeforeOffset(ctx, offset) { -- const state = ctx.state; -- const dataLength = state.props.data.length; -- let low = 0; -- let high = dataLength - 1; -- let match; -- while (low <= high) { -- const mid = Math.floor((low + high) / 2); -- const top = state.positions[mid]; -- if (top === void 0) { -- high = mid - 1; -- } else { -- if (top <= offset) { -- match = mid; -- low = mid + 1; -- } else { -- high = mid - 1; ++ } + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); + if (isReadyToRender && !peek$(ctx, "readyToRender")) { + set$(ctx, "readyToRender", true); @@ -10448,19 +8929,10 @@ index 914d2da..c3ed202 100644 + state.didLoad = true; + if (onLoad) { + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); - } - } - } -- return match; - } --function getItemBottom(ctx, index) { -- var _a3; -- const top = ctx.state.positions[index]; -- if (top === void 0) { -- return void 0; -- } -- const itemSize = (_a3 = getItemSizeAtIndex(ctx, index)) != null ? _a3 : 0; -- return top + (Number.isFinite(itemSize) ? itemSize : 0); ++ } ++ } ++ } ++} + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -10468,8 +8940,7 @@ index 914d2da..c3ed202 100644 + state.scroll = offset; + state.scrollPending = offset; + state.scrollPrev = offset; - } --function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { ++} +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -10481,65 +8952,17 @@ index 914d2da..c3ed202 100644 +} +function supersedeInitialScroll(ctx) { + var _a3, _b, _c; - const state = ctx.state; -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return void 0; -- } -- const viewportStart = Math.max(0, targetOffset); -- const viewportEnd = Math.max(viewportStart, targetOffset + state.scrollLength); -- let start = targetIndex !== void 0 ? Math.max(0, Math.min(dataLength - 1, targetIndex)) : findPositionIndexAtOrBeforeOffset(ctx, viewportStart); -- if (start === void 0) { -- return void 0; -- } -- if (targetIndex !== void 0 && state.positions[start] === void 0) { -- return { end: start, start }; -- } -- if (targetIndex === void 0) { -- const startBottom = getItemBottom(ctx, start); -- if (startBottom === void 0 || startBottom <= viewportStart) { -- return void 0; -- } -- } -- while (start > 0) { -- const top = state.positions[start]; -- if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { -- break; -- } -- start--; -- } -- while (start > 0) { -- const previousBottom = getItemBottom(ctx, start - 1); -- if (previousBottom === void 0 || previousBottom <= viewportStart) { -- break; ++ const state = ctx.state; + const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; + if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { + if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { + cancelAnimationFrame(bootstrapInitialScroll.frameHandle); - } -- start--; -- } -- let end = start; -- while (end + 1 < dataLength) { -- const nextTop = state.positions[end + 1]; -- if (nextTop === void 0 || nextTop > viewportEnd) { -- break; ++ } + if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { + cancelScrollCompletionChecks(state); + state.scrollingTo = void 0; + state.scrollTargetPinnedRange = void 0; - } -- end++; -- } -- return { end, start }; --} --function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -- if (range) { -- ctx.state.scrollTargetPinnedRange = range; -- ctx.state.scrollForNextCalculateItemsInView = void 0; -- } else { -- ctx.state.scrollTargetPinnedRange = void 0; ++ } + initialScrollCompletion.resetFlags(state); + setInitialScrollSession(state, { bootstrap: null }); + finishInitialScroll(ctx); @@ -10648,15 +9071,15 @@ index 914d2da..c3ed202 100644 } // src/core/scrollToIndex.ts -@@ -4350,6 +4486,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4350,6 +4488,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); -+ const minIndexSizeChangedThisPass = dataChanged ? 0 : minIndexSizeChanged; ++ const minIndexSizeChangedThisPass = minIndexSizeChanged; if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4367,8 +4504,17 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4367,8 +4506,17 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -10676,11 +9099,33 @@ index 914d2da..c3ed202 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5915,6 +6061,119 @@ function useRafCoalescer(callback) { - return coalescer; +@@ -5927,6 +6075,21 @@ function getDocumentScrollerNode() { + } + return document.scrollingElement || document.documentElement || document.body; + } ++function getScrollAxis(horizontal) { ++ return horizontal ? { ++ contentSizeKey: "scrollWidth", ++ paddingEndProp: "paddingRight", ++ viewportSizeKey: "clientWidth", ++ x: 1, ++ y: 0 ++ } : { ++ contentSizeKey: "scrollHeight", ++ paddingEndProp: "paddingBottom", ++ viewportSizeKey: "clientHeight", ++ x: 0, ++ y: 1 ++ }; ++} + function getWindowScrollPosition() { + var _a3, _b, _c, _d; + if (typeof window === "undefined") { +@@ -6003,9 +6166,175 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll + }; } -+// src/components/temporaryEndPadding.ts ++// src/components/webTemporaryEndPadding.ts +var entriesByNode = /* @__PURE__ */ new WeakMap(); +var nextRequestId = 1; +function readResolvedPadding(node, prop) { @@ -10701,6 +9146,20 @@ index 914d2da..c3ed202 100644 + node.style[prop] = total === 0 ? entry.baseline : `${entry.baselineSize + total}px`; + entry.lastApplied = node.style[prop]; +} ++function drainPendingReleases(entry) { ++ if (entry.resetHandle !== void 0) { ++ cancelAnimationFrame(entry.resetHandle); ++ entry.resetHandle = void 0; ++ } ++ if (entry.pendingReleases.size === 0) { ++ return; ++ } ++ const releases = [...entry.pendingReleases]; ++ entry.pendingReleases.clear(); ++ for (const pending of releases) { ++ pending(); ++ } ++} +function releaseEntry(node, prop, requestId) { + const entries = entriesByNode.get(node); + const entry = entries == null ? void 0 : entries[prop]; @@ -10711,10 +9170,8 @@ index 914d2da..c3ed202 100644 + applyPadding(node, prop, entry); + } + if (entry.requests.size === 0) { -+ if (entry.resetHandle !== void 0) { -+ cancelAnimationFrame(entry.resetHandle); -+ } + entries == null ? true : delete entries[prop]; ++ drainPendingReleases(entry); + } +} +function addTemporaryEndPadding(node, prop, extraSize) { @@ -10767,7 +9224,10 @@ index 914d2da..c3ed202 100644 +} +function getTemporaryEndPadding(node, prop) { + var _a3; -+ const entry = node ? (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop] : void 0; ++ if (!node) { ++ return 0; ++ } ++ const entry = (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop]; + if (!entry || !isOwnedByUs(node, prop, entry)) { + return 0; + } @@ -10783,43 +9243,77 @@ index 914d2da..c3ed202 100644 + if (!entry) { + continue; + } -+ if (entry.resetHandle !== void 0) { -+ cancelAnimationFrame(entry.resetHandle); -+ } + if (isOwnedByUs(node, prop, entry)) { + node.style[prop] = entry.baseline; + } + delete entries[prop]; ++ entry.requests.clear(); ++ drainPendingReleases(entry); + } +} + - // src/components/webConstants.ts - var LEGEND_LIST_CONTENT_CONTAINER_CLASS = "legend-list-content-container"; - var LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS = "legend-list-scrollbar-x-hidden"; -@@ -6006,6 +6265,10 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; +var SMOOTH_SCROLL_MAX_MS = 2e3; ++var MAX_BORROW_VIEWPORTS = 3; ++var BORROW_WATCH_FRAME_INTERVAL = 3; +var BORROW_MEASURE_ATTEMPTS = 2; -+var USER_INTERACTION_EVENTS = ["wheel", "pointerdown", "touchstart", "keydown"]; ++var USER_DRAG_SLOP = 8; ++var WHEEL_MOMENTUM_GRACE_MS = 150; ++var SCROLL_KEYS = /* @__PURE__ */ new Set([ ++ " ", ++ "ArrowDown", ++ "ArrowLeft", ++ "ArrowRight", ++ "ArrowUp", ++ "End", ++ "Home", ++ "PageDown", ++ "PageUp" ++]); ++function isTextEntryTarget(target) { ++ var _a3; ++ const element = target; ++ const tag = (_a3 = element == null ? void 0 : element.tagName) == null ? void 0 : _a3.toLowerCase(); ++ return tag === "input" || tag === "textarea" || tag === "select" || !!(element == null ? void 0 : element.isContentEditable); ++} ++function pointerPosition(event) { ++ var _a3; ++ const touch = (_a3 = event.touches) == null ? void 0 : _a3[0]; ++ if (touch) { ++ return { x: touch.clientX, y: touch.clientY }; ++ } ++ const pointer = event; ++ return typeof pointer.clientX === "number" ? { x: pointer.clientX, y: pointer.clientY } : void 0; ++} ++function isScrollKey(event) { ++ if (event.altKey || event.ctrlKey || event.metaKey) { ++ return false; ++ } ++ return SCROLL_KEYS.has(event.key) && !isTextEntryTarget(event.target); ++} var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6074,6 +6337,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6074,6 +6403,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); -+ const paddingEndProp = horizontal ? isHorizontalRTL(ctx.state) ? "paddingLeft" : "paddingRight" : "paddingBottom"; ++ const paddingEndProp = getScrollAxis(horizontal).paddingEndProp; + const getCommittedMaxScrollOffset = React3.useCallback( + (maxOffset) => Math.max(0, maxOffset - getTemporaryEndPadding(contentRef.current, paddingEndProp)), + [paddingEndProp] + ); ++ const getViewportExtent = React3.useCallback(() => { ++ const layout = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); ++ return horizontal ? layout.width : layout.height; ++ }, [horizontal, isWindowScroll]); const getMaxScrollOffset = React3.useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6094,6 +6362,95 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,8 +6432,153 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -10834,7 +9328,13 @@ index 914d2da..c3ed202 100644 + const contentNode = contentRef.current; + (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); + const committedMaxOffset = getCommittedMaxScrollOffset(getMaxScrollOffset()); -+ if (!contentNode || offset <= committedMaxOffset + SCROLL_EXTENT_EPSILON) { ++ const viewportExtent = getViewportExtent(); ++ const reachLimit = viewportExtent > 0 ? committedMaxOffset + MAX_BORROW_VIEWPORTS * viewportExtent : Number.POSITIVE_INFINITY; ++ if (!contentNode || !Number.isFinite(offset) || offset > reachLimit) { ++ run(clampOffset(offset, maxOffset)); ++ return; ++ } ++ if (offset <= committedMaxOffset + SCROLL_EXTENT_EPSILON) { + run(clampOffset(offset, maxOffset)); + return; + } @@ -10862,12 +9362,13 @@ index 914d2da..c3ed202 100644 + } + const scrollTarget = getScrollTarget(); + const supportsScrollEnd = !!scrollTarget && "onscrollend" in scrollTarget; ++ const timers = {}; + const finish = () => { + if (animatedPaddingReleaseRef.current !== finish) { + return; + } + animatedPaddingReleaseRef.current = void 0; -+ clearTimeout(settleTimeout); ++ clearTimeout(timers.settle); + scrollTarget == null ? void 0 : scrollTarget.removeEventListener("scrollend", finishIfArrived); + cancelAnimationFrame(borrowWatchRef.current); + release(); @@ -10877,25 +9378,36 @@ index 914d2da..c3ed202 100644 + finish(); + } + }; ++ let framesUntilCheck = 0; + const releaseWhenContentCommits = () => { + if (animatedPaddingReleaseRef.current !== finish) { + return; + } -+ if (getCommittedMaxScrollOffset(getMaxScrollOffset()) + SCROLL_EXTENT_EPSILON >= offset) { -+ finish(); -+ return; ++ if (framesUntilCheck-- <= 0) { ++ framesUntilCheck = BORROW_WATCH_FRAME_INTERVAL; ++ if (getCommittedMaxScrollOffset(getMaxScrollOffset()) + SCROLL_EXTENT_EPSILON >= offset) { ++ finish(); ++ return; ++ } + } + borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); + }; + animatedPaddingReleaseRef.current = finish; + cancelAnimationFrame(borrowWatchRef.current); + borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); -+ const settleTimeout = setTimeout(finish, SMOOTH_SCROLL_MAX_MS); ++ timers.settle = setTimeout(finish, SMOOTH_SCROLL_MAX_MS); + if (supportsScrollEnd) { + scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finishIfArrived); + } + }, -+ [getCommittedMaxScrollOffset, getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, paddingEndProp] ++ [ ++ getCommittedMaxScrollOffset, ++ getCurrentScrollOffset, ++ getMaxScrollOffset, ++ getScrollTarget, ++ getViewportExtent, ++ paddingEndProp ++ ] + ); + React3.useEffect( + () => () => { @@ -10909,13 +9421,53 @@ index 914d2da..c3ed202 100644 + }, + [] + ); ++ const interactionArmedAtRef = React3.useRef(0); ++ const dragOriginRef = React3.useRef(void 0); + const reportUserInteraction = React3.useCallback(() => { -+ releaseScrollTargetForUserInteraction(ctx); ++ dragOriginRef.current = void 0; ++ releaseScrollTargetForUserInteraction(ctx.state); + }, [ctx]); ++ const onWheel = React3.useCallback( ++ (event) => { ++ if (event.timeStamp - interactionArmedAtRef.current < WHEEL_MOMENTUM_GRACE_MS) { ++ return; ++ } ++ reportUserInteraction(); ++ }, ++ [reportUserInteraction] ++ ); ++ const onPointerDown = React3.useCallback((event) => { ++ const point = pointerPosition(event); ++ dragOriginRef.current = point; ++ }, []); ++ const onPointerMove = React3.useCallback( ++ (event) => { ++ const origin = dragOriginRef.current; ++ const point = origin && pointerPosition(event); ++ if (!origin || !point) { ++ return; ++ } ++ if (Math.abs(point.x - origin.x) > USER_DRAG_SLOP || Math.abs(point.y - origin.y) > USER_DRAG_SLOP) { ++ reportUserInteraction(); ++ } ++ }, ++ [reportUserInteraction] ++ ); ++ const onKeyDown = React3.useCallback( ++ (event) => { ++ if (isScrollKey(event)) { ++ reportUserInteraction(); ++ } ++ }, ++ [reportUserInteraction] ++ ); const scrollToLocalOffset = React3.useCallback( (offset, animated) => { ++ interactionArmedAtRef.current = typeof performance !== "undefined" ? performance.now() : 0; const scrollElement = scrollRef.current; -@@ -6116,14 +6473,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + const target = getScrollTarget(); + if (!target || typeof target.scrollTo !== "function") { +@@ -6116,14 +6599,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); options.left = left; options.top = top; @@ -10942,7 +9494,7 @@ index 914d2da..c3ed202 100644 ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6513,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6149,8 +6639,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -10952,7 +9504,7 @@ index 914d2da..c3ed202 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6163,7 +6526,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6163,7 +6652,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView if (!onScroll2 || !scrollRef.current) { return; } @@ -10969,25 +9521,74 @@ index 914d2da..c3ed202 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6221,11 +6592,17 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6221,11 +6718,26 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView const target = getScrollTarget(); if (!target) return; target.addEventListener("scroll", handleScroll, { passive: true }); -+ for (const type of USER_INTERACTION_EVENTS) { -+ target.addEventListener(type, reportUserInteraction, { passive: true }); -+ } ++ const listenerOptions = { capture: true, passive: true }; ++ const removeOptions = { capture: true }; ++ const interactionTarget = scrollRef.current; ++ target.addEventListener("wheel", onWheel, listenerOptions); ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener("pointerdown", onPointerDown, listenerOptions); ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener("pointermove", onPointerMove, listenerOptions); ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener("touchstart", onPointerDown, listenerOptions); ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener("touchmove", onPointerMove, listenerOptions); ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener("keydown", onKeyDown, listenerOptions); if ("onscrollend" in target) { target.addEventListener("scrollend", emitScrollEnd); } return () => { target.removeEventListener("scroll", handleScroll); -+ for (const type of USER_INTERACTION_EVENTS) { -+ target.removeEventListener(type, reportUserInteraction); -+ } ++ target.removeEventListener("wheel", onWheel, removeOptions); ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("pointerdown", onPointerDown, removeOptions); ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("pointermove", onPointerMove, removeOptions); ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("touchstart", onPointerDown, removeOptions); ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("touchmove", onPointerMove, removeOptions); ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("keydown", onKeyDown, removeOptions); if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6411,8 +6788,6 @@ function ScrollAdjust() { +@@ -6236,7 +6748,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + } + scrollEventCoalescer.cancel(); + }; +- }, [emitScrollEnd, getScrollTarget, handleScroll, scrollEventCoalescer]); ++ }, [ ++ emitScrollEnd, ++ getScrollTarget, ++ handleScroll, ++ onKeyDown, ++ onPointerDown, ++ onPointerMove, ++ onWheel, ++ scrollEventCoalescer ++ ]); + React3.useEffect(() => { + const doScroll = () => { + if (contentOffset) { +@@ -6379,21 +6900,6 @@ function useValueListener$(key, callback) { + } + + // src/components/ScrollAdjust.tsx +-function getScrollAdjustAxis(horizontal) { +- return horizontal ? { +- contentSizeKey: "scrollWidth", +- paddingEndProp: "paddingRight", +- viewportSizeKey: "clientWidth", +- x: 1, +- y: 0 +- } : { +- contentSizeKey: "scrollHeight", +- paddingEndProp: "paddingBottom", +- viewportSizeKey: "clientHeight", +- x: 0, +- y: 1 +- }; +-} + function getScrollAdjustTarget(ctx, contentNode) { + var _a3, _b, _c; + const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; +@@ -6411,8 +6917,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3__namespace.useRef(0); const lastScrollAdjustUserOffsetRef = React3__namespace.useRef(0); @@ -10996,7 +9597,16 @@ index 914d2da..c3ed202 100644 const contentNodeRef = React3__namespace.useRef(null); const callback = React3__namespace.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6438,29 +6813,10 @@ function ScrollAdjust() { +@@ -6423,7 +6927,7 @@ function ScrollAdjust() { + const target = getScrollAdjustTarget(ctx, contentNodeRef.current); + if (target) { + const horizontal = !!ctx.state.props.horizontal; +- const axis = getScrollAdjustAxis(horizontal); ++ const axis = getScrollAxis(horizontal); + const { contentNode, scrollElement: el } = target; + const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; + const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; +@@ -6438,29 +6942,10 @@ function ScrollAdjust() { const nextScroll = currentScroll + scrollDelta; const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; if (needsTemporaryPadding) { @@ -11028,19 +9638,19 @@ index 914d2da..c3ed202 100644 } else { scrollBy(); } -@@ -8288,6 +8644,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8288,6 +8773,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); -+ clearScrollTargetSettle(state); ++ releaseScrollTargetForUserInteraction(state); (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs -index 95465f2..9ac6d3f 100644 +index 95465f2..ea937e4 100644 --- a/node_modules/@legendapp/list/react.mjs +++ b/node_modules/@legendapp/list/react.mjs -@@ -413,178 +413,266 @@ var EDGE_POSITION_EPSILON = 1; +@@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; var ENABLE_DEVMODE = IS_DEV && false; var ENABLE_DEBUG_VIEW = IS_DEV && false; @@ -11050,56 +9660,22 @@ index 95465f2..9ac6d3f 100644 - scheduledWork.cancel("checkFinishedScrollRetryFrame"); - scheduledWork.cancel("checkFinishedScrollFallback"); - scheduledWork.cancel("platformScrollCompletion"); -+// src/core/getStartOffsetAdjustment.ts -+function getStartOffsetAdjustment(ctx) { -+ const { state } = ctx; -+ const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; -+ return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); - } +-} -function settlePendingImperativeScroll(state) { - var _a3, _b; - const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; - state.pendingScrollResolve = void 0; - state.pendingScrollToEnd = void 0; - resolvePendingScroll == null ? void 0 : resolvePendingScroll(); -+ -+// src/utils/getId.ts -+function getId(state, index) { -+ const { data, keyExtractor } = state.props; -+ if (!data) { -+ return ""; -+ } -+ const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; -+ const id = ret; -+ state.idCache[index] = id; -+ return id; - } +-} -function cancelImperativeScroll(state) { - cancelScrollCompletionChecks(state); - state.scheduledWork.cancel("imperativeScrollReady"); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; - settlePendingImperativeScroll(state); -+ -+// src/core/updateContentMetricsState.ts -+function getRawContentLength(ctx) { -+ var _a3, _b, _c; -+ const { state, values } = ctx; -+ return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); -+} -+function getAlignItemsAtEndPadding(ctx) { -+ const { state } = ctx; -+ const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; -+ return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; -+} -+function updateContentMetricsState(ctx) { -+ const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; -+ const nextPadding = getAlignItemsAtEndPadding(ctx); -+ if (previousPadding !== nextPadding) { -+ set$(ctx, "alignItemsAtEndPadding", nextPadding); -+ } - } - +-} +- -// src/core/deferredPublicOnScroll.ts -function withResolvedContentOffset(state, event, resolvedOffset) { - return { @@ -11107,40 +9683,12 @@ index 95465f2..9ac6d3f 100644 - nativeEvent: { - ...event.nativeEvent, - contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -+// src/core/addTotalSize.ts -+function addTotalSize(ctx, key, add, notifyTotalSize = true) { -+ const state = ctx.state; -+ const prevTotalSize = state.totalSize; -+ let totalSize = state.totalSize; -+ if (key === null) { -+ totalSize = add; -+ if (state.timeoutSetPaddingTop) { -+ clearTimeout(state.timeoutSetPaddingTop); -+ state.timeoutSetPaddingTop = void 0; - } +- } - }; -+ } else { -+ totalSize += add; -+ } -+ if (prevTotalSize !== totalSize) { -+ { -+ state.pendingTotalSize = void 0; -+ state.totalSize = totalSize; -+ if (notifyTotalSize) { -+ set$(ctx, "totalSize", totalSize); -+ } -+ updateContentMetricsState(ctx); -+ } -+ } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { -+ set$(ctx, "totalSize", totalSize); -+ } - } +-} -function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { - var _a3, _b, _c, _d; -+ -+// src/core/setSize.ts -+function setSize(ctx, itemKey, size, notifyTotalSize = true) { - const state = ctx.state; +- const state = ctx.state; - const deferredEvent = state.deferredPublicOnScrollEvent; - state.deferredPublicOnScrollEvent = void 0; - if (deferredEvent) { @@ -11152,15 +9700,9 @@ index 95465f2..9ac6d3f 100644 - (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 - ) - ); -+ const { sizes } = state; -+ const previousSize = sizes.get(itemKey); -+ const diff = previousSize !== void 0 ? size - previousSize : size; -+ if (diff !== 0) { -+ addTotalSize(ctx, itemKey, diff, notifyTotalSize); - } -+ sizes.set(itemKey, size); - } - +- } +-} +- -// src/core/initialScrollSession.ts -var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -function hasInitialScrollSessionCompletion(completion) { @@ -11169,10 +9711,7 @@ index 95465f2..9ac6d3f 100644 -function clearInitialScrollSession(state) { - state.initialScrollSession = void 0; - return void 0; -+// src/utils/helpers.ts -+function isFunction(obj) { -+ return typeof obj === "function"; - } +-} -function createInitialScrollSession(options) { - const { bootstrap, completion, kind, previousDataLength } = options; - return kind === "offset" ? { @@ -11185,9 +9724,7 @@ index 95465f2..9ac6d3f 100644 - kind, - previousDataLength - }; -+function isArray(obj) { -+ return Array.isArray(obj); - } +-} -function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { - var _a4, _b2; - if (!state.initialScrollSession) { @@ -11203,15 +9740,10 @@ index 95465f2..9ac6d3f 100644 - kind, - previousDataLength: state.initialScrollSession.previousDataLength - }); -+var warned = /* @__PURE__ */ new Set(); -+function warnDevOnce(id, text) { -+ if (IS_DEV && !warned.has(id)) { -+ warned.add(id); -+ console.warn(`[legend-list] ${text}`); - } +- } - (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; - return state.initialScrollSession.completion; - } +-} -var initialScrollCompletion = { - didDispatchNativeScroll(state) { - var _a3, _b; @@ -11234,26 +9766,7 @@ index 95465f2..9ac6d3f 100644 - const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); - completion.didDispatchNativeScroll = void 0; - completion.didRetrySilentInitialScroll = void 0; -+function roundSize(size) { -+ return Math.floor(size * 8) / 8; -+} -+function isNullOrUndefined(value) { -+ return value === null || value === void 0; -+} -+function getPadding(s, type) { -+ var _a3, _b, _c; -+ const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; -+ return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; -+} -+function extractPadding(style, contentContainerStyle, type) { -+ return getPadding(style, type) + getPadding(contentContainerStyle, type); -+} -+function findContainerId(ctx, key) { -+ var _a3, _b; -+ const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); -+ if (directMatch !== void 0) { -+ return directMatch; - } +- } -}; -var initialScrollWatchdog = { - clear(state) { @@ -11277,12 +9790,7 @@ index 95465f2..9ac6d3f 100644 - var _a3, _b; - if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { - return; -+ const numContainers = peek$(ctx, "numContainers"); -+ for (let i = 0; i < numContainers; i++) { -+ const itemKey = peek$(ctx, `containerItemKey${i}`); -+ if (itemKey === key) { -+ return i; - } +- } - const completion = ensureInitialScrollSessionCompletion(state); - completion.watchdog = watchdog ? { - startScroll: watchdog.startScroll, @@ -11300,104 +9808,460 @@ index 95465f2..9ac6d3f 100644 - if (!kind) { - return clearInitialScrollSession(state); - } -- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { -- return clearInitialScrollSession(state); +- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { +- return clearInitialScrollSession(state); +- } +- const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; +- state.initialScrollSession = createInitialScrollSession({ +- bootstrap, +- completion, +- kind, +- previousDataLength +- }); +- return state.initialScrollSession; +-} +- +-// src/utils/checkThreshold.ts +-var HYSTERESIS_MULTIPLIER = 1.3; +-function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { +- const absDistance = Math.abs(distance); +- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; +-} +-var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { +- const absDistance = Math.abs(distance); +- const within = atThreshold || threshold > 0 && absDistance <= threshold; +- const updateSnapshot = () => { +- setSnapshot({ +- atThreshold, +- contentSize: context.contentSize, +- dataLength: context.dataLength, +- scrollPosition: context.scrollPosition +- }); +- }; +- if (!wasReached) { +- if (!within) { +- return false; +- } +- onReached(distance); +- updateSnapshot(); +- return true; +- } +- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); +- if (reset) { +- setSnapshot(void 0); +- return false; +- } +- if (within) { +- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; +- if (changed) { +- updateSnapshot(); +- } +- } +- return true; +-}; +- +-// src/utils/edgeReachedGate.ts +-function resetEdgeLatch(ctx, edge) { +- const state = ctx.state; +- if (edge === "start") { +- state.isStartReached = false; +- state.startReachedSnapshot = void 0; +- } else { +- state.isEndReached = false; +- state.endReachedSnapshot = void 0; +- } +-} +-function resetSharedEdgeGateIfOutsideHysteresis(ctx) { +- const state = ctx.state; +- if (!state.edgeReachedGate) { +- return; +- } +- const contentSize = getContentSize(ctx); +- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); +- const isContentLess = contentSize < state.scrollLength; +- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; +- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; +- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); +- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); +- if (isOutsideStart && isOutsideEnd) { +- state.edgeReachedGate = void 0; +- } +-} +-function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { +- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; +-} +-function markReachedEdge(ctx) { +- ctx.state.edgeReachedGate = "closed"; +-} +-function prepareReachedEdgeForNextUserScroll(ctx) { +- if (ctx.state.edgeReachedGate) { +- ctx.state.edgeReachedGate = "prepared"; +- } +-} +-function beginReachedEdgeUserScroll(ctx, scrollDelta) { +- const state = ctx.state; +- if (state.edgeReachedGate !== "prepared") { +- return void 0; +- } +- const allowedEdge = scrollDelta < 0 ? "start" : "end"; +- state.edgeReachedGate = "closed"; +- resetEdgeLatch(ctx, allowedEdge); +- return allowedEdge; +-} +- +-// src/utils/hasActiveInitialScroll.ts +-function hasActiveInitialScroll(state) { +- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; +-} +- +-// src/utils/checkAtBottom.ts +-function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { +- var _a3; +- const state = ctx.state; +- if (!state) { +- return; +- } +- const { +- queuedInitialLayout, +- scrollLength, +- scroll, +- maintainingScrollAtEnd, +- props: { maintainScrollAtEndThreshold, onEndReachedThreshold } +- } = state; +- const contentSize = getContentSize(ctx); +- resetSharedEdgeGateIfOutsideHysteresis(ctx); +- if (contentSize > 0 && queuedInitialLayout) { +- const insetEnd = getContentInsetEnd(ctx); +- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; +- const isContentLess = contentSize < scrollLength; +- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); +- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); +- set$( +- ctx, +- "isWithinMaintainScrollAtEndThreshold", +- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength +- ); +- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; +- if (!shouldSkipThresholdChecks) { +- state.isEndReached = checkThreshold( +- distanceFromEnd, +- isContentLess, +- onEndReachedThreshold * scrollLength, +- state.isEndReached, +- state.endReachedSnapshot, +- { +- contentSize, +- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, +- scrollPosition: scroll +- }, +- (distance) => { +- var _a4, _b; +- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { +- markReachedEdge(ctx); +- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); +- } +- }, +- (snapshot) => { +- state.endReachedSnapshot = snapshot; +- } +- ); +- } +- } +-} +- +-// src/utils/checkAtTop.ts +-function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { +- const state = ctx == null ? void 0 : ctx.state; +- if (!state) { +- return; +- } +- const { +- isStartReached, +- props: { data, onStartReachedThreshold }, +- scroll, +- scrollLength, +- startReachedSnapshot, +- totalSize +- } = state; +- const dataLength = data.length; +- const threshold = onStartReachedThreshold * scrollLength; +- resetSharedEdgeGateIfOutsideHysteresis(ctx); +- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { +- state.isStartReached = false; +- state.startReachedSnapshot = void 0; +- } +- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); +- set$(ctx, "isNearStart", scroll <= threshold); +- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; +- if (!shouldSkipThresholdChecks) { +- state.isStartReached = checkThreshold( +- scroll, +- false, +- threshold, +- state.isStartReached, +- startReachedSnapshot, +- { +- contentSize: totalSize, +- dataLength, +- scrollPosition: scroll +- }, +- (distance) => { +- var _a3, _b; +- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { +- markReachedEdge(ctx); +- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); +- } +- }, +- (snapshot) => { +- state.startReachedSnapshot = snapshot; +- } +- ); +- } +-} +- +-// src/utils/checkThresholds.ts +-function checkThresholds(ctx, allowedEdge) { +- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; +- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); +- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); +-} +- +-// src/core/recalculateSettledScroll.ts +-function recalculateSettledScroll(ctx) { +- var _a3, _b; +- const state = ctx.state; +- if ((_a3 = state.props) == null ? void 0 : _a3.data) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); +- } +- checkThresholds(ctx); +-} +-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; +-function clearAdaptiveRenderExitTimeout(ctx) { +- ctx.state.scheduledWork.cancel("adaptiveRender"); +-} +-function scheduleAdaptiveRenderExit(ctx, exitDelay) { +- const state = ctx.state; +- clearAdaptiveRenderExitTimeout(ctx); +- if (exitDelay <= 0) { +- setAdaptiveRender(ctx, "normal", "scroll"); +- } else { +- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); +- } +-} +-function setAdaptiveRender(ctx, mode, reason) { +- var _a3, _b; +- const previousMode = peek$(ctx, "adaptiveRender"); +- if (previousMode !== mode) { +- set$(ctx, "adaptiveRender", mode); +- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); +- } +-} +-function resetAdaptiveRender(ctx) { +- var _a3, _b; +- clearAdaptiveRenderExitTimeout(ctx); +- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; +- if (peek$(ctx, "adaptiveRender") !== mode) { +- setAdaptiveRender(ctx, mode, "initial"); +- } +-} +-function updateAdaptiveRender(ctx, scrollVelocity, options) { +- var _a3, _b, _c; +- const state = ctx.state; +- const adaptiveRender = state.props.adaptiveRender; +- const currentMode = peek$(ctx, "adaptiveRender"); +- if (peek$(ctx, "readyToRender")) { +- if (adaptiveRender) { +- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; +- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; +- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; +- const threshold = currentMode === "light" ? exitVelocity : enterVelocity; +- const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; +- const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; +- if (nextMode !== previousMode) { +- if (nextMode === "light") { +- setAdaptiveRender(ctx, "light", "scroll"); +- scheduleAdaptiveRenderExit(ctx, exitDelay); +- } else if (currentMode === "light") { +- scheduleAdaptiveRenderExit(ctx, exitDelay); +- } +- } +- } else { +- resetAdaptiveRender(ctx); +- } +- } +-} +- +-// src/utils/getEffectiveDrawDistance.ts +-var INITIAL_DRAW_DISTANCE = 50; +-function getEffectiveDrawDistance(ctx, mode) { +- var _a3; +- const drawDistance = ctx.state.props.drawDistance; +- const initialScroll = ctx.state.initialScroll; +- const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; +- const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; +- return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; +-} +-function scheduleFullDrawDistancePrewarm(ctx) { +- const { state } = ctx; +- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { +- return; +- } +- state.scheduledWork.frame(() => { +- var _a3; +- return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +- }, "fullDrawDistancePrewarm"); +-} +- +-// src/utils/setInitialRenderState.ts +-function resetInitialRenderState(ctx, { +- resetLayout, +- resetInitialScroll +-}) { +- const { state } = ctx; +- if (resetLayout) { +- state.didContainersLayout = false; +- state.queuedInitialLayout = false; +- } +- if (resetInitialScroll) { +- state.didFinishInitialScroll = false; +- } +- set$(ctx, "readyToRender", false); +- resetAdaptiveRender(ctx); +-} +-function setInitialRenderState(ctx, { +- didLayout, +- didInitialScroll +-}) { +- const { state } = ctx; +- const { +- loadStartTime, +- props: { onLoad } +- } = state; +- if (didLayout) { +- state.didContainersLayout = true; +- } +- if (didInitialScroll) { +- state.didFinishInitialScroll = true; +- } +- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); +- if (isReadyToRender && !peek$(ctx, "readyToRender")) { +- set$(ctx, "readyToRender", true); +- setAdaptiveRender(ctx, "normal", "ready"); +- if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { +- scheduleFullDrawDistancePrewarm(ctx); +- } +- if (!state.didLoad) { +- state.didLoad = true; +- if (onLoad) { +- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); +- } +- } +- } +-} +- +-// src/core/finishInitialScroll.ts +-var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; +-function syncInitialScrollOffset(state, offset) { +- state.scroll = offset; +- state.scrollPending = offset; +- state.scrollPrev = offset; +-} +-function clearPreservedInitialScrollTargetTimeout(state) { +- state.scheduledWork.cancel("preservedInitialScroll"); +-} +-function clearPreservedInitialScrollTarget(state) { +- clearPreservedInitialScrollTargetTimeout(state); +- state.clearPreservedInitialScrollOnNextFinish = void 0; +- state.initialScroll = void 0; +- setInitialScrollSession(state); +-} +-function supersedeInitialScroll(ctx) { +- var _a3, _b, _c; +- const state = ctx.state; +- const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; +- if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { +- if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { +- cancelAnimationFrame(bootstrapInitialScroll.frameHandle); +- } +- if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { +- cancelScrollCompletionChecks(state); +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- } +- initialScrollCompletion.resetFlags(state); +- setInitialScrollSession(state, { bootstrap: null }); +- finishInitialScroll(ctx); +- } +-} +-function finishInitialScroll(ctx, options) { +- var _a3, _b, _c; +- const state = ctx.state; +- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { +- syncInitialScrollOffset(state, options.resolvedOffset); +- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { +- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); +- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { +- syncInitialScrollOffset(state, observedOffset); +- } +- } +- const complete = () => { +- var _a4, _b2, _c2, _d, _e; +- const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; +- const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0; +- initialScrollWatchdog.clear(state); +- if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) { +- state.clearPreservedInitialScrollOnNextFinish = void 0; +- setInitialScrollSession(state); +- clearPreservedInitialScrollTargetTimeout(state); +- if (options == null ? void 0 : options.schedulePreservedTargetClear) { +- state.scheduledWork.timeout( +- () => { +- var _a5; +- if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) { +- return; +- } +- clearPreservedInitialScrollTarget(state); +- }, +- PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, +- "preservedInitialScroll" +- ); +- } +- } else { +- clearPreservedInitialScrollTarget(state); +- } +- if (options == null ? void 0 : options.recalculateItems) { +- recalculateSettledScroll(ctx); +- } +- setInitialRenderState(ctx, { didInitialScroll: true }); +- if (shouldReleaseDeferredPublicOnScroll) { +- releaseDeferredPublicOnScroll(ctx, finalScrollOffset); +- } +- (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); +- }; +- if (options == null ? void 0 : options.waitForCompletionFrame) { +- requestAnimationFrame(complete); +- return; +- } +- complete(); +-} +- +-// src/core/calculateOffsetForIndex.ts +-function calculateOffsetForIndex(ctx, index) { +- const state = ctx.state; +- return index !== void 0 ? state.positions[index] || 0 : 0; +-} +- + // src/core/getStartOffsetAdjustment.ts + function getStartOffsetAdjustment(ctx) { + const { state } = ctx; +@@ -1213,58 +614,273 @@ function getItemSizeAtIndex(ctx, index) { + if (index === void 0 || index < 0) { + return void 0; } -- const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; -- state.initialScrollSession = createInitialScrollSession({ -- bootstrap, -- completion, -- kind, -- previousDataLength -- }); -- return state.initialScrollSession; -+ return -1; - } - --// src/utils/checkThreshold.ts --var HYSTERESIS_MULTIPLIER = 1.3; --function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { -- const absDistance = Math.abs(distance); -- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; -+// src/utils/getItemSize.ts -+function getKnownOrFixedSize(ctx, key, index, data, resolved) { -+ var _a3, _b; -+ const state = ctx.state; -+ const { getFixedItemSize, getItemType } = state.props; -+ let size = key ? state.sizesKnown.get(key) : void 0; -+ if (size === void 0 && key && getFixedItemSize) { -+ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; -+ const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); -+ if (fixedSize !== void 0) { -+ size = fixedSize + ctx.scrollAxisGap; -+ state.sizesKnown.set(key, size); -+ } -+ } -+ return size; -+} -+function getKnownOrFixedItemSize(ctx, index) { -+ const key = getId(ctx.state, index); -+ return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); -+} -+function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { -+ for (let index = startIndex; index <= endIndex; index++) { -+ if (getKnownOrFixedItemSize(ctx, index) === void 0) { -+ return false; -+ } -+ } -+ return true; -+} -+function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { -+ var _a3, _b, _c, _d; -+ const state = ctx.state; -+ const { -+ sizes, -+ averageSizes, -+ props: { estimatedItemSize, getItemType }, -+ scrollingTo -+ } = state; -+ const sizeKnown = state.sizesKnown.get(key); -+ if (sizeKnown !== void 0) { -+ return sizeKnown; -+ } -+ let size; -+ const renderedSize = sizes.get(key); -+ if (preferCachedSize) { -+ if (renderedSize !== void 0) { -+ return renderedSize; -+ } -+ } -+ size = getKnownOrFixedSize(ctx, key, index, data, resolved); -+ if (size !== void 0) { -+ setSize(ctx, key, size, notifyTotalSize); -+ return size; -+ } -+ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; -+ if (useAverageSize && !scrollingTo) { -+ const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; -+ if (averageSizeForType !== void 0) { -+ size = roundSize(averageSizeForType); -+ } -+ } -+ if (size === void 0 && renderedSize !== void 0) { -+ return renderedSize; -+ } -+ if (size === void 0 && useAverageSize && scrollingTo) { -+ const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; -+ if (averageSizeForType !== void 0) { -+ size = roundSize(averageSizeForType); -+ } -+ } -+ if (size === void 0) { -+ size = estimatedItemSize + ctx.scrollAxisGap; -+ } -+ setSize(ctx, key, size, notifyTotalSize); -+ return size; -+} -+function getItemSizeAtIndex(ctx, index) { -+ if (index === void 0 || index < 0) { -+ return void 0; -+ } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); + const targetId = getId(ctx.state, index); + return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); +} @@ -11457,372 +10321,278 @@ index 95465f2..9ac6d3f 100644 +function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { + const absDistance = Math.abs(distance); + return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; - } - var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { - const absDistance = Math.abs(distance); -@@ -794,642 +882,346 @@ function recalculateSettledScroll(ctx) { - } - checkThresholds(ctx); - } --var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; --var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; --var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; --function clearAdaptiveRenderExitTimeout(ctx) { -- ctx.state.scheduledWork.cancel("adaptiveRender"); --} --function scheduleAdaptiveRenderExit(ctx, exitDelay) { -- const state = ctx.state; -- clearAdaptiveRenderExitTimeout(ctx); -- if (exitDelay <= 0) { -- setAdaptiveRender(ctx, "normal", "scroll"); -- } else { -- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); -- } --} --function setAdaptiveRender(ctx, mode, reason) { -- var _a3, _b; -- const previousMode = peek$(ctx, "adaptiveRender"); -- if (previousMode !== mode) { -- set$(ctx, "adaptiveRender", mode); -- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); -- } --} --function resetAdaptiveRender(ctx) { ++} ++var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { ++ const absDistance = Math.abs(distance); ++ const within = atThreshold || threshold > 0 && absDistance <= threshold; ++ const updateSnapshot = () => { ++ setSnapshot({ ++ atThreshold, ++ contentSize: context.contentSize, ++ dataLength: context.dataLength, ++ scrollPosition: context.scrollPosition ++ }); ++ }; ++ if (!wasReached) { ++ if (!within) { ++ return false; ++ } ++ onReached(distance); ++ updateSnapshot(); ++ return true; ++ } ++ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); ++ if (reset) { ++ setSnapshot(void 0); ++ return false; ++ } ++ if (within) { ++ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; ++ if (changed) { ++ updateSnapshot(); ++ } ++ } ++ return true; ++}; + -+// src/core/finishScrollTo.ts -+function finishScrollTo(ctx) { - var _a3, _b; -- clearAdaptiveRenderExitTimeout(ctx); -- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; -- if (peek$(ctx, "adaptiveRender") !== mode) { -- setAdaptiveRender(ctx, mode, "initial"); -- } --} --function updateAdaptiveRender(ctx, scrollVelocity, options) { -- var _a3, _b, _c; - const state = ctx.state; -- const adaptiveRender = state.props.adaptiveRender; -- const currentMode = peek$(ctx, "adaptiveRender"); -- if (peek$(ctx, "readyToRender")) { -- if (adaptiveRender) { -- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; -- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; -- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; -- const threshold = currentMode === "light" ? exitVelocity : enterVelocity; -- const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; -- const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; -- if (nextMode !== previousMode) { -- if (nextMode === "light") { -- setAdaptiveRender(ctx, "light", "scroll"); -- scheduleAdaptiveRenderExit(ctx, exitDelay); -- } else if (currentMode === "light") { -- scheduleAdaptiveRenderExit(ctx, exitDelay); -- } -- } -- } else { -- resetAdaptiveRender(ctx); -+ if (state == null ? void 0 : state.scrollingTo) { -+ cancelScrollCompletionChecks(state); -+ const resolvePendingScroll = state.pendingScrollResolve; -+ state.pendingScrollResolve = void 0; -+ const scrollingTo = state.scrollingTo; -+ state.scrollHistory.length = 0; -+ state.scrollingTo = void 0; -+ state.scrollTargetPinnedRange = void 0; -+ if (state.pendingTotalSize !== void 0) { -+ addTotalSize(ctx, null, state.pendingTotalSize); -+ } -+ { -+ state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); -+ } -+ if (scrollingTo.isInitialScroll || state.initialScroll) { -+ const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; -+ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; -+ finishInitialScroll(ctx, { -+ onFinished: () => { -+ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); -+ }, -+ preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, -+ recalculateItems: true, -+ schedulePreservedTargetClear: shouldPreserveResizeTarget, -+ syncObservedOffset: isOffsetSession, -+ waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame -+ }); -+ return; - } -+ recalculateSettledScroll(ctx); -+ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); - } - } - --// src/utils/getEffectiveDrawDistance.ts --var INITIAL_DRAW_DISTANCE = 50; --function getEffectiveDrawDistance(ctx, mode) { -- var _a3; -- const drawDistance = ctx.state.props.drawDistance; -- const initialScroll = ctx.state.initialScroll; -- const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; -- const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; -- return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; --} --function scheduleFullDrawDistancePrewarm(ctx) { -- const { state } = ctx; -- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { -+// src/core/doScrollTo.ts -+var SCROLL_END_IDLE_MS = 80; -+var SCROLL_END_MAX_MS = 1500; -+var SMOOTH_SCROLL_DURATION_MS = 320; -+var SCROLL_END_TARGET_EPSILON = 1; -+function doScrollTo(ctx, params) { -+ var _a3, _b; ++// src/utils/edgeReachedGate.ts ++function resetEdgeLatch(ctx, edge) { ++ const state = ctx.state; ++ if (edge === "start") { ++ state.isStartReached = false; ++ state.startReachedSnapshot = void 0; ++ } else { ++ state.isEndReached = false; ++ state.endReachedSnapshot = void 0; ++ } ++} ++function resetSharedEdgeGateIfOutsideHysteresis(ctx) { ++ const state = ctx.state; ++ if (!state.edgeReachedGate) { ++ return; ++ } ++ const contentSize = getContentSize(ctx); ++ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); ++ const isContentLess = contentSize < state.scrollLength; ++ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; ++ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; ++ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); ++ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); ++ if (isOutsideStart && isOutsideEnd) { ++ state.edgeReachedGate = void 0; ++ } ++} ++function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { ++ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; ++} ++function markReachedEdge(ctx) { ++ ctx.state.edgeReachedGate = "closed"; ++} ++function prepareReachedEdgeForNextUserScroll(ctx) { ++ if (ctx.state.edgeReachedGate) { ++ ctx.state.edgeReachedGate = "prepared"; ++ } ++} ++function beginReachedEdgeUserScroll(ctx, scrollDelta) { ++ const state = ctx.state; ++ if (state.edgeReachedGate !== "prepared") { ++ return void 0; ++ } ++ const allowedEdge = scrollDelta < 0 ? "start" : "end"; ++ state.edgeReachedGate = "closed"; ++ resetEdgeLatch(ctx, allowedEdge); ++ return allowedEdge; ++} ++ ++// src/utils/hasActiveInitialScroll.ts ++function hasActiveInitialScroll(state) { ++ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; ++} ++ ++// src/utils/checkAtBottom.ts ++function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { ++ var _a3; + const state = ctx.state; -+ const { animated, horizontal, offset } = params; -+ state.scheduledWork.cancel("platformScrollCompletion"); -+ const scroller = state.refScroller.current; -+ const node = scroller == null ? void 0 : scroller.getScrollableNode(); -+ if (!scroller || !node) { - return; - } -- state.scheduledWork.frame(() => { -- var _a3; -- return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); -- }, "fullDrawDistancePrewarm"); --} -- --// src/utils/setInitialRenderState.ts --function resetInitialRenderState(ctx, { -- resetLayout, -- resetInitialScroll --}) { -- const { state } = ctx; -- if (resetLayout) { -- state.didContainersLayout = false; -- state.queuedInitialLayout = false; -- } -- if (resetInitialScroll) { -- state.didFinishInitialScroll = false; -+ const isAnimated = !!animated; -+ const isHorizontal = !!horizontal; -+ const contentSize = isHorizontal ? getContentSize(ctx) : void 0; -+ const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; -+ const top = isHorizontal ? 0 : offset; -+ scroller.scrollTo({ animated: isAnimated, x: left, y: top }); -+ if (isAnimated) { -+ const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; -+ listenForScrollEnd(ctx, { -+ readOffset: () => scroller.getCurrentScrollOffset(), -+ target, -+ targetOffset: offset -+ }); -+ } else { -+ state.scroll = offset; -+ const targetToken = state.scrollingTo; -+ state.scheduledWork.timeout( -+ () => { -+ if (targetToken === state.scrollingTo) { -+ finishScrollTo(ctx); ++ if (!state) { ++ return; ++ } ++ const { ++ queuedInitialLayout, ++ scrollLength, ++ scroll, ++ maintainingScrollAtEnd, ++ props: { maintainScrollAtEndThreshold, onEndReachedThreshold } ++ } = state; ++ const contentSize = getContentSize(ctx); ++ resetSharedEdgeGateIfOutsideHysteresis(ctx); ++ if (contentSize > 0 && queuedInitialLayout) { ++ const insetEnd = getContentInsetEnd(ctx); ++ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; ++ const isContentLess = contentSize < scrollLength; ++ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); ++ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); ++ set$( ++ ctx, ++ "isWithinMaintainScrollAtEndThreshold", ++ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength ++ ); ++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; ++ if (!shouldSkipThresholdChecks) { ++ state.isEndReached = checkThreshold( ++ distanceFromEnd, ++ isContentLess, ++ onEndReachedThreshold * scrollLength, ++ state.isEndReached, ++ state.endReachedSnapshot, ++ { ++ contentSize, ++ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, ++ scrollPosition: scroll ++ }, ++ (distance) => { ++ var _a4, _b; ++ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { ++ markReachedEdge(ctx); ++ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); ++ } ++ }, ++ (snapshot) => { ++ state.endReachedSnapshot = snapshot; ++ } ++ ); ++ } ++ } ++} ++ ++// src/utils/checkAtTop.ts ++function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { ++ const state = ctx == null ? void 0 : ctx.state; ++ if (!state) { ++ return; ++ } ++ const { ++ isStartReached, ++ props: { data, onStartReachedThreshold }, ++ scroll, ++ scrollLength, ++ startReachedSnapshot, ++ totalSize ++ } = state; ++ const dataLength = data.length; ++ const threshold = onStartReachedThreshold * scrollLength; ++ resetSharedEdgeGateIfOutsideHysteresis(ctx); ++ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { ++ state.isStartReached = false; ++ state.startReachedSnapshot = void 0; ++ } ++ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); ++ set$(ctx, "isNearStart", scroll <= threshold); ++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; ++ if (!shouldSkipThresholdChecks) { ++ state.isStartReached = checkThreshold( ++ scroll, ++ false, ++ threshold, ++ state.isStartReached, ++ startReachedSnapshot, ++ { ++ contentSize: totalSize, ++ dataLength, ++ scrollPosition: scroll ++ }, ++ (distance) => { ++ var _a3, _b; ++ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { ++ markReachedEdge(ctx); ++ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); + } + }, -+ 100, -+ "platformScrollCompletion" ++ (snapshot) => { ++ state.startReachedSnapshot = snapshot; ++ } + ); - } -- set$(ctx, "readyToRender", false); -- resetAdaptiveRender(ctx); ++ } } --function setInitialRenderState(ctx, { -- didLayout, -- didInitialScroll --}) { -- const { state } = ctx; -- const { -- loadStartTime, -- props: { onLoad } -- } = state; -- if (didLayout) { -- state.didContainersLayout = true; -- } -- if (didInitialScroll) { -- state.didFinishInitialScroll = true; -- } -- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); -- if (isReadyToRender && !peek$(ctx, "readyToRender")) { -- set$(ctx, "readyToRender", true); -- setAdaptiveRender(ctx, "normal", "ready"); -- if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { -- scheduleFullDrawDistancePrewarm(ctx); -- } -- if (!state.didLoad) { -- state.didLoad = true; -- if (onLoad) { -- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); -- } -- } -+function listenForScrollEnd(ctx, params) { -+ const { readOffset, target, targetOffset } = params; -+ if (!target) { -+ finishScrollTo(ctx); -+ return; - } --} -- --// src/core/finishInitialScroll.ts --var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; --function syncInitialScrollOffset(state, offset) { -- state.scroll = offset; -- state.scrollPending = offset; -- state.scrollPrev = offset; --} --function clearPreservedInitialScrollTargetTimeout(state) { -- state.scheduledWork.cancel("preservedInitialScroll"); --} --function clearPreservedInitialScrollTarget(state) { -- clearPreservedInitialScrollTargetTimeout(state); -- state.clearPreservedInitialScrollOnNextFinish = void 0; -- state.initialScroll = void 0; -- setInitialScrollSession(state); --} --function supersedeInitialScroll(ctx) { -- var _a3, _b, _c; -- const state = ctx.state; -- const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; -- if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { -- if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { -- cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -+ const supportsScrollEnd = "onscrollend" in target; -+ let idleTimeout; -+ let settled = false; -+ const { scheduledWork, scrollingTo: targetToken } = ctx.state; -+ const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); -+ const cleanup = () => { -+ target.removeEventListener("scroll", onScroll2); -+ if (supportsScrollEnd) { -+ target.removeEventListener("scrollend", onScrollEnd); - } -- if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { -- cancelScrollCompletionChecks(state); -- state.scrollingTo = void 0; -- state.scrollTargetPinnedRange = void 0; -+ if (idleTimeout !== void 0) { -+ clearTimeout(idleTimeout); - } -- initialScrollCompletion.resetFlags(state); -- setInitialScrollSession(state, { bootstrap: null }); -- finishInitialScroll(ctx); -- } --} --function finishInitialScroll(ctx, options) { -- var _a3, _b, _c; + +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; - const state = ctx.state; -- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { -- syncInitialScrollOffset(state, options.resolvedOffset); -- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { -- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); -- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { -- syncInitialScrollOffset(state, observedOffset); -+ clearTimeout(maxTimeout); -+ }; -+ const cancel = () => { -+ if (!settled) { -+ settled = true; -+ cleanup(); - } +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; - } -- const complete = () => { -- var _a4, _b2, _c2, _d, _e; -- const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; -- const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0; -- initialScrollWatchdog.clear(state); -- if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) { -- state.clearPreservedInitialScrollOnNextFinish = void 0; -- setInitialScrollSession(state); -- clearPreservedInitialScrollTargetTimeout(state); -- if (options == null ? void 0 : options.schedulePreservedTargetClear) { -- state.scheduledWork.timeout( -- () => { -- var _a5; -- if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) { -- return; -- } -- clearPreservedInitialScrollTarget(state); -- }, -- PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, -- "preservedInitialScroll" -- ); -- } -- } else { -- clearPreservedInitialScrollTarget(state); -+ }; -+ const finish = (reason) => { -+ if (settled) return; -+ if (targetToken !== ctx.state.scrollingTo) { -+ scheduledWork.cancel("platformScrollCompletion"); -+ return; - } -- if (options == null ? void 0 : options.recalculateItems) { -- recalculateSettledScroll(ctx); -+ const currentOffset = readOffset(); -+ const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; -+ if (reason === "scrollend" && !isNearTarget) { -+ return; - } -- setInitialRenderState(ctx, { didInitialScroll: true }); -- if (shouldReleaseDeferredPublicOnScroll) { -- releaseDeferredPublicOnScroll(ctx, finalScrollOffset); -+ scheduledWork.cancel("platformScrollCompletion"); -+ finishScrollTo(ctx); -+ }; -+ const onScroll2 = () => { -+ if (idleTimeout !== void 0) { -+ clearTimeout(idleTimeout); - } -- (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); -+ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); - }; -- if (options == null ? void 0 : options.waitForCompletionFrame) { -- requestAnimationFrame(complete); -- return; -+ const onScrollEnd = () => finish("scrollend"); -+ target.addEventListener("scroll", onScroll2); -+ if (supportsScrollEnd) { -+ target.addEventListener("scrollend", onScrollEnd); -+ } else { -+ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } +- } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } +- } +- return offset; ++// src/utils/checkThresholds.ts ++function checkThresholds(ctx, allowedEdge) { ++ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; ++ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); ++ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + } + +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { ++// src/core/recalculateSettledScroll.ts ++function recalculateSettledScroll(ctx) { ++ var _a3, _b; + const state = ctx.state; +- const contentSize = getContentSize(ctx); +- let clampedOffset = offset; +- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { +- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); +- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; +- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; +- const maxOffset = baseMaxOffset + extraEndOffset; +- clampedOffset = Math.min(offset, maxOffset); ++ if ((_a3 = state.props) == null ? void 0 : _a3.data) { ++ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); } -- complete(); -+ scheduledWork.register("platformScrollCompletion", cancel); +- clampedOffset = Math.max(0, clampedOffset); +- return clampedOffset; ++ checkThresholds(ctx); } --// src/core/calculateOffsetForIndex.ts --function calculateOffsetForIndex(ctx, index) { -- const state = ctx.state; -- return index !== void 0 ? state.positions[index] || 0 : 0; + // src/core/finishScrollTo.ts +@@ -1401,7 +1017,182 @@ function listenForScrollEnd(ctx, params) { + } else { + idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); + } +- scheduledWork.register("platformScrollCompletion", cancel); ++ scheduledWork.register("platformScrollCompletion", cancel); ++} ++ +// src/core/initialScrollSession.ts +var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +function hasInitialScrollSessionCompletion(completion) { + return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); - } -- --// src/core/getStartOffsetAdjustment.ts --function getStartOffsetAdjustment(ctx) { -- const { state } = ctx; -- const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; -- return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); ++} +function clearInitialScrollSession(state) { + state.initialScrollSession = void 0; + return void 0; - } -- --// src/utils/getId.ts --function getId(state, index) { -- const { data, keyExtractor } = state.props; -- if (!data) { -- return ""; -- } -- const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; -- const id = ret; -- state.idCache[index] = id; -- return id; ++} +function createInitialScrollSession(options) { + const { bootstrap, completion, kind, previousDataLength } = options; + return kind === "offset" ? { @@ -11835,13 +10605,7 @@ index 95465f2..9ac6d3f 100644 + kind, + previousDataLength + }; - } -- --// src/core/updateContentMetricsState.ts --function getRawContentLength(ctx) { -- var _a3, _b, _c; -- const { state, values } = ctx; -- return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); ++} +function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { + var _a4, _b2; + if (!state.initialScrollSession) { @@ -11860,30 +10624,7 @@ index 95465f2..9ac6d3f 100644 + } + (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; + return state.initialScrollSession.completion; - } --function getAlignItemsAtEndPadding(ctx) { -- const { state } = ctx; -- const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; -- return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; --} --function updateContentMetricsState(ctx) { -- const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; -- const nextPadding = getAlignItemsAtEndPadding(ctx); -- if (previousPadding !== nextPadding) { -- set$(ctx, "alignItemsAtEndPadding", nextPadding); -- } --} -- --// src/core/addTotalSize.ts --function addTotalSize(ctx, key, add, notifyTotalSize = true) { -- const state = ctx.state; -- const prevTotalSize = state.totalSize; -- let totalSize = state.totalSize; -- if (key === null) { -- totalSize = add; -- if (state.timeoutSetPaddingTop) { -- clearTimeout(state.timeoutSetPaddingTop); -- state.timeoutSetPaddingTop = void 0; ++} +var initialScrollCompletion = { + didDispatchNativeScroll(state) { + var _a3, _b; @@ -11902,21 +10643,11 @@ index 95465f2..9ac6d3f 100644 + resetFlags(state) { + if (!state.initialScrollSession) { + return; - } -- } else { -- totalSize += add; ++ } + const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); + completion.didDispatchNativeScroll = void 0; + completion.didRetrySilentInitialScroll = void 0; - } -- if (prevTotalSize !== totalSize) { -- { -- state.pendingTotalSize = void 0; -- state.totalSize = totalSize; -- if (notifyTotalSize) { -- set$(ctx, "totalSize", totalSize); -- } -- updateContentMetricsState(ctx); ++ } +}; +var initialScrollWatchdog = { + clear(state) { @@ -11940,25 +10671,13 @@ index 95465f2..9ac6d3f 100644 + var _a3, _b; + if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { + return; - } -- } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { -- set$(ctx, "totalSize", totalSize); ++ } + const completion = ensureInitialScrollSessionCompletion(state); + completion.watchdog = watchdog ? { + startScroll: watchdog.startScroll, + targetOffset: watchdog.targetOffset + } : void 0; - } --} -- --// src/core/setSize.ts --function setSize(ctx, itemKey, size, notifyTotalSize = true) { -- const state = ctx.state; -- const { sizes } = state; -- const previousSize = sizes.get(itemKey); -- const diff = previousSize !== void 0 ? size - previousSize : size; -- if (diff !== 0) { -- addTotalSize(ctx, itemKey, diff, notifyTotalSize); ++ } +}; +function setInitialScrollSession(state, options = {}) { + var _a3, _b, _c, _d; @@ -11969,25 +10688,10 @@ index 95465f2..9ac6d3f 100644 + const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; + if (!kind) { + return clearInitialScrollSession(state); - } -- sizes.set(itemKey, size); --} -- --// src/utils/helpers.ts --function isFunction(obj) { -- return typeof obj === "function"; --} --function isArray(obj) { -- return Array.isArray(obj); --} --var warned = /* @__PURE__ */ new Set(); --function warnDevOnce(id, text) { -- if (IS_DEV && !warned.has(id)) { -- warned.add(id); -- console.warn(`[legend-list] ${text}`); ++ } + if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { + return clearInitialScrollSession(state); - } ++ } + const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; + state.initialScrollSession = createInitialScrollSession({ + bootstrap, @@ -11996,25 +10700,13 @@ index 95465f2..9ac6d3f 100644 + previousDataLength + }); + return state.initialScrollSession; - } --function roundSize(size) { -- return Math.floor(size * 8) / 8; --} --function isNullOrUndefined(value) { -- return value === null || value === void 0; --} --function getPadding(s, type) { -- var _a3, _b, _c; -- const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; -- return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; ++} +var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; +var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; +var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; +function clearAdaptiveRenderExitTimeout(ctx) { + ctx.state.scheduledWork.cancel("adaptiveRender"); - } --function extractPadding(style, contentContainerStyle, type) { -- return getPadding(style, type) + getPadding(contentContainerStyle, type); ++} +function scheduleAdaptiveRenderExit(ctx, exitDelay) { + const state = ctx.state; + clearAdaptiveRenderExitTimeout(ctx); @@ -12023,57 +10715,23 @@ index 95465f2..9ac6d3f 100644 + } else { + state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); + } - } --function findContainerId(ctx, key) { ++} +function setAdaptiveRender(ctx, mode, reason) { - var _a3, _b; -- const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); -- if (directMatch !== void 0) { -- return directMatch; -- } -- const numContainers = peek$(ctx, "numContainers"); -- for (let i = 0; i < numContainers; i++) { -- const itemKey = peek$(ctx, `containerItemKey${i}`); -- if (itemKey === key) { -- return i; -- } ++ var _a3, _b; + const previousMode = peek$(ctx, "adaptiveRender"); + if (previousMode !== mode) { + set$(ctx, "adaptiveRender", mode); + (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); - } -- return -1; - } -- --// src/utils/getItemSize.ts --function getKnownOrFixedSize(ctx, key, index, data, resolved) { ++ } ++} +function resetAdaptiveRender(ctx) { - var _a3, _b; -- const state = ctx.state; -- const { getFixedItemSize, getItemType } = state.props; -- let size = key ? state.sizesKnown.get(key) : void 0; -- if (size === void 0 && key && getFixedItemSize) { -- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; -- const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); -- if (fixedSize !== void 0) { -- size = fixedSize + ctx.scrollAxisGap; -- state.sizesKnown.set(key, size); -- } ++ var _a3, _b; + clearAdaptiveRenderExitTimeout(ctx); + const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; + if (peek$(ctx, "adaptiveRender") !== mode) { + setAdaptiveRender(ctx, mode, "initial"); - } -- return size; --} --function getKnownOrFixedItemSize(ctx, index) { -- const key = getId(ctx.state, index); -- return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); - } --function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { -- for (let index = startIndex; index <= endIndex; index++) { -- if (getKnownOrFixedItemSize(ctx, index) === void 0) { -- return false; ++ } ++} +function updateAdaptiveRender(ctx, scrollVelocity, options) { + var _a3, _b, _c; + const state = ctx.state; @@ -12088,301 +10746,21 @@ index 95465f2..9ac6d3f 100644 + const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; + const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; + if (nextMode !== previousMode) { -+ if (nextMode === "light") { -+ setAdaptiveRender(ctx, "light", "scroll"); -+ scheduleAdaptiveRenderExit(ctx, exitDelay); -+ } else if (currentMode === "light") { -+ scheduleAdaptiveRenderExit(ctx, exitDelay); -+ } -+ } -+ } else { -+ resetAdaptiveRender(ctx); - } - } -- return true; - } --function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { -- var _a3, _b, _c, _d; -+ -+// src/core/doMaintainScrollAtEnd.ts -+function doMaintainScrollAtEnd(ctx) { - const state = ctx.state; - const { -- sizes, -- averageSizes, -- props: { estimatedItemSize, getItemType }, -- scrollingTo -+ didContainersLayout, -+ pendingNativeMVCPAdjust, -+ refScroller, -+ props: { maintainScrollAtEnd } - } = state; -- const sizeKnown = state.sizesKnown.get(key); -- if (sizeKnown !== void 0) { -- return sizeKnown; -+ const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); -+ const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout); -+ if (pendingNativeMVCPAdjust) { -+ state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd; -+ return false; - } -- let size; -- const renderedSize = sizes.get(key); -- if (preferCachedSize) { -- if (renderedSize !== void 0) { -- return renderedSize; -- } -- } -- size = getKnownOrFixedSize(ctx, key, index, data, resolved); -- if (size !== void 0) { -- setSize(ctx, key, size, notifyTotalSize); -- return size; -- } -- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; -- if (useAverageSize && !scrollingTo) { -- const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; -- if (averageSizeForType !== void 0) { -- size = roundSize(averageSizeForType); -- } -- } -- if (size === void 0 && renderedSize !== void 0) { -- return renderedSize; -- } -- if (size === void 0 && useAverageSize && scrollingTo) { -- const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; -- if (averageSizeForType !== void 0) { -- size = roundSize(averageSizeForType); -- } -- } -- if (size === void 0) { -- size = estimatedItemSize + ctx.scrollAxisGap; -- } -- setSize(ctx, key, size, notifyTotalSize); -- return size; --} --function getItemSizeAtIndex(ctx, index) { -- if (index === void 0 || index < 0) { -- return void 0; -- } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); --} -- --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; -- } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } -- } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } -- } -- return offset; --} -- --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { -- const state = ctx.state; -- const contentSize = getContentSize(ctx); -- let clampedOffset = offset; -- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { -- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); -- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; -- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; -- const maxOffset = baseMaxOffset + extraEndOffset; -- clampedOffset = Math.min(offset, maxOffset); -- } -- clampedOffset = Math.max(0, clampedOffset); -- return clampedOffset; --} -- --// src/core/finishScrollTo.ts --function finishScrollTo(ctx) { -- var _a3, _b; -- const state = ctx.state; -- if (state == null ? void 0 : state.scrollingTo) { -- cancelScrollCompletionChecks(state); -- const resolvePendingScroll = state.pendingScrollResolve; -- state.pendingScrollResolve = void 0; -- const scrollingTo = state.scrollingTo; -- state.scrollHistory.length = 0; -- state.scrollingTo = void 0; -- state.scrollTargetPinnedRange = void 0; -- if (state.pendingTotalSize !== void 0) { -- addTotalSize(ctx, null, state.pendingTotalSize); -- } -- { -- state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); -- } -- if (scrollingTo.isInitialScroll || state.initialScroll) { -- const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; -- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; -- finishInitialScroll(ctx, { -- onFinished: () => { -- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); -- }, -- preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, -- recalculateItems: true, -- schedulePreservedTargetClear: shouldPreserveResizeTarget, -- syncObservedOffset: isOffsetSession, -- waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame -- }); -- return; -- } -- recalculateSettledScroll(ctx); -- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); -- } --} -- --// src/core/doScrollTo.ts --var SCROLL_END_IDLE_MS = 80; --var SCROLL_END_MAX_MS = 1500; --var SMOOTH_SCROLL_DURATION_MS = 320; --var SCROLL_END_TARGET_EPSILON = 1; --function doScrollTo(ctx, params) { -- var _a3, _b; -- const state = ctx.state; -- const { animated, horizontal, offset } = params; -- state.scheduledWork.cancel("platformScrollCompletion"); -- const scroller = state.refScroller.current; -- const node = scroller == null ? void 0 : scroller.getScrollableNode(); -- if (!scroller || !node) { -- return; -- } -- const isAnimated = !!animated; -- const isHorizontal = !!horizontal; -- const contentSize = isHorizontal ? getContentSize(ctx) : void 0; -- const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; -- const top = isHorizontal ? 0 : offset; -- scroller.scrollTo({ animated: isAnimated, x: left, y: top }); -- if (isAnimated) { -- const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; -- listenForScrollEnd(ctx, { -- readOffset: () => scroller.getCurrentScrollOffset(), -- target, -- targetOffset: offset -- }); -- } else { -- state.scroll = offset; -- const targetToken = state.scrollingTo; -- state.scheduledWork.timeout( -- () => { -- if (targetToken === state.scrollingTo) { -- finishScrollTo(ctx); -- } -- }, -- 100, -- "platformScrollCompletion" -- ); -- } --} --function listenForScrollEnd(ctx, params) { -- const { readOffset, target, targetOffset } = params; -- if (!target) { -- finishScrollTo(ctx); -- return; -- } -- const supportsScrollEnd = "onscrollend" in target; -- let idleTimeout; -- let settled = false; -- const { scheduledWork, scrollingTo: targetToken } = ctx.state; -- const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); -- const cleanup = () => { -- target.removeEventListener("scroll", onScroll2); -- if (supportsScrollEnd) { -- target.removeEventListener("scrollend", onScrollEnd); -- } -- if (idleTimeout !== void 0) { -- clearTimeout(idleTimeout); -- } -- clearTimeout(maxTimeout); -- }; -- const cancel = () => { -- if (!settled) { -- settled = true; -- cleanup(); -- } -- }; -- const finish = (reason) => { -- if (settled) return; -- if (targetToken !== ctx.state.scrollingTo) { -- scheduledWork.cancel("platformScrollCompletion"); -- return; -- } -- const currentOffset = readOffset(); -- const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; -- if (reason === "scrollend" && !isNearTarget) { -- return; -- } -- scheduledWork.cancel("platformScrollCompletion"); -- finishScrollTo(ctx); -- }; -- const onScroll2 = () => { -- if (idleTimeout !== void 0) { -- clearTimeout(idleTimeout); -- } -- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); -- }; -- const onScrollEnd = () => finish("scrollend"); -- target.addEventListener("scroll", onScroll2); -- if (supportsScrollEnd) { -- target.addEventListener("scrollend", onScrollEnd); -- } else { -- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); -- } -- scheduledWork.register("platformScrollCompletion", cancel); --} -- --// src/core/doMaintainScrollAtEnd.ts --function doMaintainScrollAtEnd(ctx) { -- const state = ctx.state; -- const { -- didContainersLayout, -- pendingNativeMVCPAdjust, -- refScroller, -- props: { maintainScrollAtEnd } -- } = state; -- const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); -- const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout); -- if (pendingNativeMVCPAdjust) { -- state.pendingMaintainScrollAtEnd = shouldMaintainScrollAtEnd; -- return false; -- } -- if (shouldMaintainScrollAtEnd) { -- state.pendingMaintainScrollAtEnd = false; -- const contentSize = getContentSize(ctx); -- if (contentSize < state.scrollLength) { -- state.scroll = 0; -+ if (shouldMaintainScrollAtEnd) { -+ state.pendingMaintainScrollAtEnd = false; -+ const contentSize = getContentSize(ctx); -+ if (contentSize < state.scrollLength) { -+ state.scroll = 0; - } - if (!state.maintainingScrollAtEnd) { - const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; ++ if (nextMode === "light") { ++ setAdaptiveRender(ctx, "light", "scroll"); ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } else if (currentMode === "light") { ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } ++ } ++ } else { ++ resetAdaptiveRender(ctx); ++ } ++ } + } + + // src/core/doMaintainScrollAtEnd.ts +@@ -1430,6 +1221,7 @@ function doMaintainScrollAtEnd(ctx) { const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; state.maintainingScrollAtEnd = pendingState; @@ -12390,27 +10768,10 @@ index 95465f2..9ac6d3f 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1788,23 +1580,44 @@ function prepareMVCP(ctx, dataChanged) { +@@ -1788,6 +1580,27 @@ function prepareMVCP(ctx, dataChanged) { } } --// src/utils/getScrollVelocity.ts --var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; --var SCROLL_VELOCITY_HALF_LIFE_MS = 200; --var getScrollVelocity = (state) => { -- const { scrollHistory } = state; -- const newestIndex = scrollHistory.length - 1; -- if (newestIndex < 1) { -- return 0; -- } -- const newest = scrollHistory[newestIndex]; -- if (Date.now() - newest.time > MAX_SCROLL_VELOCITY_WINDOW_MS) { -- return 0; -- } -- let direction = 0; -- let weightedVelocity = 0; -- let totalWeight = 0; -- for (let i = newestIndex; i > 0; i--) { +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { @@ -12432,254 +10793,24 @@ index 95465f2..9ac6d3f 100644 + }, "fullDrawDistancePrewarm"); +} + -+// src/utils/getScrollVelocity.ts -+var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; -+var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -+var getScrollVelocity = (state) => { -+ const { scrollHistory } = state; -+ const newestIndex = scrollHistory.length - 1; -+ if (newestIndex < 1) { -+ return 0; -+ } -+ const newest = scrollHistory[newestIndex]; -+ if (Date.now() - newest.time > MAX_SCROLL_VELOCITY_WINDOW_MS) { -+ return 0; -+ } -+ let direction = 0; -+ let weightedVelocity = 0; -+ let totalWeight = 0; -+ for (let i = newestIndex; i > 0; i--) { - const current = scrollHistory[i]; - const previous = scrollHistory[i - 1]; - const scrollDiff = current.scroll - previous.scroll; -@@ -1823,281 +1636,604 @@ var getScrollVelocity = (state) => { - if (scrollDiff === 0 || timeDiff <= 0) { - continue; + // src/utils/getScrollVelocity.ts + var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; + var SCROLL_VELOCITY_HALF_LIFE_MS = 200; +@@ -2034,70 +1847,395 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { + if (nextTop === void 0 || nextTop > viewportEnd) { + break; } -- const age = newest.time - current.time; -- const weight = Math.exp(-age / SCROLL_VELOCITY_HALF_LIFE_MS); -- weightedVelocity += scrollDiff / timeDiff * weight; -- totalWeight += weight; +- end++; - } -- return totalWeight > 0 ? weightedVelocity / totalWeight : 0; --}; -- --// src/utils/hasActiveMVCPAnchorLock.ts --function hasActiveMVCPAnchorLock(state) { -- const lock = state.mvcpAnchorLock; -- if (!lock) { -- return false; -+ const age = newest.time - current.time; -+ const weight = Math.exp(-age / SCROLL_VELOCITY_HALF_LIFE_MS); -+ weightedVelocity += scrollDiff / timeDiff * weight; -+ totalWeight += weight; -+ } -+ return totalWeight > 0 ? weightedVelocity / totalWeight : 0; -+}; -+ -+// src/utils/hasActiveMVCPAnchorLock.ts -+function hasActiveMVCPAnchorLock(state) { -+ const lock = state.mvcpAnchorLock; -+ if (!lock) { -+ return false; -+ } -+ if (Date.now() > lock.expiresAt) { -+ state.mvcpAnchorLock = void 0; -+ return false; -+ } -+ return true; -+} -+ -+// src/utils/isInMVCPActiveMode.ts -+function isInMVCPActiveMode(state) { -+ return state.dataChangeNeedsScrollUpdate || hasActiveMVCPAnchorLock(state); -+} -+ -+// src/core/updateScroll.ts -+function updateScroll(ctx, newScroll, forceUpdate, options) { -+ var _a3; -+ const state = ctx.state; -+ const { ignoreScrollFromMVCP, lastScrollAdjustForHistory, scrollAdjustHandler, scrollHistory, scrollingTo } = state; -+ const prevScroll = state.scroll; -+ if ((options == null ? void 0 : options.markHasScrolled) !== false) { -+ state.hasScrolled = true; -+ } -+ const currentTime = Date.now(); -+ state.lastBatchingAction = currentTime; -+ const adjust = scrollAdjustHandler.getAdjust(); -+ const adjustChanged = lastScrollAdjustForHistory !== void 0 && Math.abs(adjust - lastScrollAdjustForHistory) > 0.1; -+ if (adjustChanged) { -+ scrollHistory.length = 0; -+ } -+ state.lastScrollAdjustForHistory = adjust; -+ if (scrollingTo === void 0 && !(scrollHistory.length === 0 && newScroll === state.scroll)) { -+ if (!adjustChanged) { -+ scrollHistory.push({ scroll: newScroll, time: currentTime }); -+ } -+ } -+ if (scrollHistory.length > 5) { -+ scrollHistory.shift(); -+ } -+ if (ignoreScrollFromMVCP && !scrollingTo) { -+ const { lt, gt } = ignoreScrollFromMVCP; -+ if (lt && newScroll < lt || gt && newScroll > gt) { -+ state.ignoreScrollFromMVCPIgnored = true; -+ return; -+ } -+ } -+ state.scrollPrev = prevScroll; -+ state.scrollPrevTime = state.scrollTime; -+ state.scroll = newScroll; -+ state.scrollTime = currentTime; -+ const scrollDelta = Math.abs(newScroll - prevScroll); -+ const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; -+ const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; -+ const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); -+ const scrollLength = state.scrollLength; -+ const isLargeUserScrollJump = scrollLength > 0 && scrollingTo === void 0 && scrollDelta > scrollLength && !state.pendingNativeMVCPAdjust; -+ const scrollVelocity = getScrollVelocity(state); -+ updateAdaptiveRender(ctx, scrollVelocity, { forceLight: isLargeUserScrollJump }); -+ const lastCalculated = state.scrollLastCalculate; -+ const useAggressiveItemRecalculation = isInMVCPActiveMode(state); -+ const shouldUpdate = useAggressiveItemRecalculation || didResolvePendingNativeMVCPAdjust || allowedEdge !== void 0 || forceUpdate || lastCalculated === void 0 || Math.abs(state.scroll - lastCalculated) > 2; -+ if (shouldUpdate) { -+ state.scrollLastCalculate = state.scroll; -+ state.ignoreScrollFromMVCPIgnored = false; -+ state.lastScrollDelta = scrollDelta; -+ const runCalculateItems = () => { -+ var _a4; -+ const calculateItemsParams = { -+ doMVCP: scrollingTo !== void 0, -+ scrollVelocity -+ }; -+ if (isLargeUserScrollJump) { -+ calculateItemsParams.drawDistanceMode = "visible-first"; -+ } -+ (_a4 = state.triggerCalculateItemsInView) == null ? void 0 : _a4.call(state, calculateItemsParams); -+ checkThresholds(ctx, allowedEdge); -+ }; -+ if (isLargeUserScrollJump) { -+ state.mvcpAnchorLock = void 0; -+ state.pendingNativeMVCPAdjust = void 0; -+ state.userScrollAnchorReset = { keys: /* @__PURE__ */ new Set() }; -+ state.scheduledWork.cancel("mvcpRecalculate"); -+ flushSync(runCalculateItems); -+ scheduleFullDrawDistancePrewarm(ctx); -+ } else { -+ runCalculateItems(); -+ } -+ const shouldMaintainScrollAtEndAfterPendingSettle = !!state.pendingMaintainScrollAtEnd || !!((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onDataChange); -+ if (didResolvePendingNativeMVCPAdjust && shouldMaintainScrollAtEndAfterPendingSettle) { -+ state.pendingMaintainScrollAtEnd = false; -+ doMaintainScrollAtEnd(ctx); -+ } -+ state.dataChangeNeedsScrollUpdate = false; -+ state.lastScrollDelta = 0; -+ } -+} -+ -+// src/core/scrollTo.ts -+function getAverageSizeSnapshot(state) { -+ if (Object.keys(state.averageSizes).length === 0) { -+ return void 0; -+ } -+ const snapshot = {}; -+ for (const itemType in state.averageSizes) { -+ const averages = state.averageSizes[itemType]; -+ snapshot[itemType] = averages.avg; -+ } -+ return snapshot; -+} -+function syncInitialScrollNativeWatchdog(state, options) { -+ var _a3; -+ const { isInitialScroll, requestedOffset, targetOffset } = options; -+ const existingWatchdog = initialScrollWatchdog.get(state); -+ const shouldWatchInitialNativeScroll = !state.didFinishInitialScroll && (isInitialScroll || !!existingWatchdog) && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); -+ const shouldClearInitialNativeScrollWatchdog = !state.didFinishInitialScroll && !!existingWatchdog && initialScrollWatchdog.isAtZeroTargetOffset(requestedOffset); -+ if (shouldWatchInitialNativeScroll) { -+ state.hasScrolled = false; -+ initialScrollWatchdog.set(state, { -+ startScroll: (_a3 = existingWatchdog == null ? void 0 : existingWatchdog.startScroll) != null ? _a3 : state.scroll, -+ targetOffset -+ }); -+ return; -+ } -+ if (shouldClearInitialNativeScrollWatchdog) { -+ initialScrollWatchdog.clear(state); -+ } -+} -+function findPositionIndexAtOrBeforeOffset(ctx, offset) { -+ const state = ctx.state; -+ const dataLength = state.props.data.length; -+ let low = 0; -+ let high = dataLength - 1; -+ let match; -+ while (low <= high) { -+ const mid = Math.floor((low + high) / 2); -+ const top = state.positions[mid]; -+ if (top === void 0) { -+ high = mid - 1; -+ } else { -+ if (top <= offset) { -+ match = mid; -+ low = mid + 1; -+ } else { -+ high = mid - 1; -+ } -+ } -+ } -+ return match; -+} -+function getItemBottom(ctx, index) { -+ var _a3; -+ const top = ctx.state.positions[index]; -+ if (top === void 0) { -+ return void 0; -+ } -+ const itemSize = (_a3 = getItemSizeAtIndex(ctx, index)) != null ? _a3 : 0; -+ return top + (Number.isFinite(itemSize) ? itemSize : 0); -+} -+function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { -+ const state = ctx.state; -+ const dataLength = state.props.data.length; -+ if (dataLength === 0) { -+ return void 0; -+ } -+ const viewportStart = Math.max(0, targetOffset); -+ const viewportEnd = Math.max(viewportStart, targetOffset + state.scrollLength); -+ let start = targetIndex !== void 0 ? Math.max(0, Math.min(dataLength - 1, targetIndex)) : findPositionIndexAtOrBeforeOffset(ctx, viewportStart); -+ if (start === void 0) { -+ return void 0; -+ } -+ if (targetIndex !== void 0 && state.positions[start] === void 0) { -+ return { end: start, start }; -+ } -+ if (targetIndex === void 0) { -+ const startBottom = getItemBottom(ctx, start); -+ if (startBottom === void 0 || startBottom <= viewportStart) { -+ return void 0; -+ } -+ } -+ while (start > 0) { -+ const top = state.positions[start]; -+ if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { -+ break; -+ } -+ start--; -+ } -+ while (start > 0) { -+ const previousBottom = getItemBottom(ctx, start - 1); -+ if (previousBottom === void 0 || previousBottom <= viewportStart) { -+ break; -+ } -+ start--; -+ } -+ let end = start; -+ while (end + 1 < dataLength) { -+ const nextTop = state.positions[end + 1]; -+ if (nextTop === void 0 || nextTop > viewportEnd) { -+ break; -+ } +- return { end, start }; +-} +-function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); +- if (range) { +- ctx.state.scrollTargetPinnedRange = range; +- ctx.state.scrollForNextCalculateItemsInView = void 0; +- } else { +- ctx.state.scrollTargetPinnedRange = void 0; + end++; + } + return { end, start }; @@ -12747,30 +10878,23 @@ index 95465f2..9ac6d3f 100644 + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); + } - } -- if (Date.now() > lock.expiresAt) { -- state.mvcpAnchorLock = void 0; -- return false; ++ } + if (forceScroll || !isInitialScroll || Platform.OS === "android") { + doScrollTo(ctx, { animated, horizontal, offset }); + } else { + state.scroll = offset; - } -- return true; - } - --// src/utils/isInMVCPActiveMode.ts --function isInMVCPActiveMode(state) { -- return state.dataChangeNeedsScrollUpdate || hasActiveMVCPAnchorLock(state); ++ } ++} ++ +// src/core/scrollTargetSettle.ts -+var SETTLE_POSITION_EPSILON = 0.5; ++var SETTLE_POSITION_EPSILON = 1; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; +var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; -+function releaseScrollTargetForUserInteraction(ctx) { -+ if (ctx.state.scrollTargetSettle) { -+ clearScrollTargetSettle(ctx.state); ++function releaseScrollTargetForUserInteraction(state) { ++ if (state.scrollTargetSettle) { ++ clearScrollTargetSettle(state); + } +} +function clearScrollTargetSettle(state) { @@ -12787,6 +10911,7 @@ index 95465f2..9ac6d3f 100644 + clearScrollTargetSettle(state); + return; + } ++ clearScrollTargetSettle(state); + const now = Date.now(); + state.scrollTargetSettle = { + corrections: 0, @@ -12799,48 +10924,28 @@ index 95465f2..9ac6d3f 100644 + viewPosition + }; + state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline"); - } -- --// src/core/updateScroll.ts --function updateScroll(ctx, newScroll, forceUpdate, options) { ++} +function getSettleTargetOffset(ctx, settle, index, position) { + const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; + return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} +function applyScrollTargetCorrection(ctx, id) { - var _a3; - const state = ctx.state; -- const { ignoreScrollFromMVCP, lastScrollAdjustForHistory, scrollAdjustHandler, scrollHistory, scrollingTo } = state; -- const prevScroll = state.scroll; -- if ((options == null ? void 0 : options.markHasScrolled) !== false) { -- state.hasScrolled = true; ++ var _a3; ++ const state = ctx.state; + const settle = state.scrollTargetSettle; + if (!settle || settle.id !== id) { + return; - } -- const currentTime = Date.now(); -- state.lastBatchingAction = currentTime; -- const adjust = scrollAdjustHandler.getAdjust(); -- const adjustChanged = lastScrollAdjustForHistory !== void 0 && Math.abs(adjust - lastScrollAdjustForHistory) > 0.1; -- if (adjustChanged) { -- scrollHistory.length = 0; ++ } + const index = state.indexByKey.get(id); + const position = index === void 0 ? void 0 : state.positions[index]; + if (index === void 0 || position === void 0 || Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return; - } -- state.lastScrollAdjustForHistory = adjust; -- if (scrollingTo === void 0 && !(scrollHistory.length === 0 && newScroll === state.scroll)) { -- if (!adjustChanged) { -- scrollHistory.push({ scroll: newScroll, time: currentTime }); -- } ++ } + if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { + clearScrollTargetSettle(state); + return; - } -- if (scrollHistory.length > 5) { -- scrollHistory.shift(); ++ } + settle.corrections++; + settle.measuredIndex = void 0; + scrollTo(ctx, { @@ -12863,65 +10968,21 @@ index 95465f2..9ac6d3f 100644 + const settle = state.scrollTargetSettle; + if (!settle) { + return false; - } -- if (ignoreScrollFromMVCP && !scrollingTo) { -- const { lt, gt } = ignoreScrollFromMVCP; -- if (lt && newScroll < lt || gt && newScroll > gt) { -- state.ignoreScrollFromMVCPIgnored = true; -- return; ++ } + const measured = options == null ? void 0 : options.minIndexSizeChanged; + if (measured !== void 0) { + settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); ++ settle.expiresAt = Date.now() + SETTLE_TTL_MS; + } + if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return false; - } ++ } + settle.quietPasses = 0; + settle.expiresAt = Date.now() + SETTLE_TTL_MS; + return false; - } -- state.scrollPrev = prevScroll; -- state.scrollPrevTime = state.scrollTime; -- state.scroll = newScroll; -- state.scrollTime = currentTime; -- const scrollDelta = Math.abs(newScroll - prevScroll); -- const isUserScrollEvent = !!(options == null ? void 0 : options.fromNativeScrollEvent) && scrollDelta > 0.1 && !adjustChanged && scrollingTo === void 0 && !state.pendingNativeMVCPAdjust; -- const allowedEdge = isUserScrollEvent ? beginReachedEdgeUserScroll(ctx, newScroll - prevScroll) : void 0; -- const didResolvePendingNativeMVCPAdjust = resolvePendingNativeMVCPAdjust(ctx, newScroll); -- const scrollLength = state.scrollLength; -- const isLargeUserScrollJump = scrollLength > 0 && scrollingTo === void 0 && scrollDelta > scrollLength && !state.pendingNativeMVCPAdjust; -- const scrollVelocity = getScrollVelocity(state); -- updateAdaptiveRender(ctx, scrollVelocity, { forceLight: isLargeUserScrollJump }); -- const lastCalculated = state.scrollLastCalculate; -- const useAggressiveItemRecalculation = isInMVCPActiveMode(state); -- const shouldUpdate = useAggressiveItemRecalculation || didResolvePendingNativeMVCPAdjust || allowedEdge !== void 0 || forceUpdate || lastCalculated === void 0 || Math.abs(state.scroll - lastCalculated) > 2; -- if (shouldUpdate) { -- state.scrollLastCalculate = state.scroll; -- state.ignoreScrollFromMVCPIgnored = false; -- state.lastScrollDelta = scrollDelta; -- const runCalculateItems = () => { -- var _a4; -- const calculateItemsParams = { -- doMVCP: scrollingTo !== void 0, -- scrollVelocity -- }; -- if (isLargeUserScrollJump) { -- calculateItemsParams.drawDistanceMode = "visible-first"; -- } -- (_a4 = state.triggerCalculateItemsInView) == null ? void 0 : _a4.call(state, calculateItemsParams); -- checkThresholds(ctx, allowedEdge); -- }; -- if (isLargeUserScrollJump) { -- state.mvcpAnchorLock = void 0; -- state.pendingNativeMVCPAdjust = void 0; -- state.userScrollAnchorReset = { keys: /* @__PURE__ */ new Set() }; -- state.scheduledWork.cancel("mvcpRecalculate"); -- flushSync(runCalculateItems); -- scheduleFullDrawDistancePrewarm(ctx); -- } else { -- runCalculateItems(); ++ } + const index = state.indexByKey.get(settle.id); + const position = index === void 0 ? void 0 : state.positions[index]; + if (index === void 0 || position === void 0) { @@ -12943,11 +11004,7 @@ index 95465f2..9ac6d3f 100644 + settle.quietPasses++; + if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { + clearScrollTargetSettle(state); - } -- const shouldMaintainScrollAtEndAfterPendingSettle = !!state.pendingMaintainScrollAtEnd || !!((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onDataChange); -- if (didResolvePendingNativeMVCPAdjust && shouldMaintainScrollAtEndAfterPendingSettle) { -- state.pendingMaintainScrollAtEnd = false; -- doMaintainScrollAtEnd(ctx); ++ } + return false; + } + settle.quietPasses = 0; @@ -12986,9 +11043,7 @@ index 95465f2..9ac6d3f 100644 + nativeEvent: { + ...event.nativeEvent, + contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } - } -- state.dataChangeNeedsScrollUpdate = false; -- state.lastScrollDelta = 0; ++ } + }; +} +function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { @@ -13005,13 +11060,9 @@ index 95465f2..9ac6d3f 100644 + (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 + ) + ); - } - } - --// src/core/scrollTo.ts --function getAverageSizeSnapshot(state) { -- if (Object.keys(state.averageSizes).length === 0) { -- return void 0; ++ } ++} ++ +// src/utils/setInitialRenderState.ts +function resetInitialRenderState(ctx, { + resetLayout, @@ -13021,31 +11072,13 @@ index 95465f2..9ac6d3f 100644 + if (resetLayout) { + state.didContainersLayout = false; + state.queuedInitialLayout = false; - } -- const snapshot = {}; -- for (const itemType in state.averageSizes) { -- const averages = state.averageSizes[itemType]; -- snapshot[itemType] = averages.avg; ++ } + if (resetInitialScroll) { + state.didFinishInitialScroll = false; - } -- return snapshot; ++ } + set$(ctx, "readyToRender", false); + resetAdaptiveRender(ctx); - } --function syncInitialScrollNativeWatchdog(state, options) { -- var _a3; -- const { isInitialScroll, requestedOffset, targetOffset } = options; -- const existingWatchdog = initialScrollWatchdog.get(state); -- const shouldWatchInitialNativeScroll = !state.didFinishInitialScroll && (isInitialScroll || !!existingWatchdog) && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); -- const shouldClearInitialNativeScrollWatchdog = !state.didFinishInitialScroll && !!existingWatchdog && initialScrollWatchdog.isAtZeroTargetOffset(requestedOffset); -- if (shouldWatchInitialNativeScroll) { -- state.hasScrolled = false; -- initialScrollWatchdog.set(state, { -- startScroll: (_a3 = existingWatchdog == null ? void 0 : existingWatchdog.startScroll) != null ? _a3 : state.scroll, -- targetOffset -- }); -- return; ++} +function setInitialRenderState(ctx, { + didLayout, + didInitialScroll @@ -13057,30 +11090,10 @@ index 95465f2..9ac6d3f 100644 + } = state; + if (didLayout) { + state.didContainersLayout = true; - } -- if (shouldClearInitialNativeScrollWatchdog) { -- initialScrollWatchdog.clear(state); ++ } + if (didInitialScroll) { + state.didFinishInitialScroll = true; - } --} --function findPositionIndexAtOrBeforeOffset(ctx, offset) { -- const state = ctx.state; -- const dataLength = state.props.data.length; -- let low = 0; -- let high = dataLength - 1; -- let match; -- while (low <= high) { -- const mid = Math.floor((low + high) / 2); -- const top = state.positions[mid]; -- if (top === void 0) { -- high = mid - 1; -- } else { -- if (top <= offset) { -- match = mid; -- low = mid + 1; -- } else { -- high = mid - 1; ++ } + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); + if (isReadyToRender && !peek$(ctx, "readyToRender")) { + set$(ctx, "readyToRender", true); @@ -13092,19 +11105,10 @@ index 95465f2..9ac6d3f 100644 + state.didLoad = true; + if (onLoad) { + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); - } - } - } -- return match; - } --function getItemBottom(ctx, index) { -- var _a3; -- const top = ctx.state.positions[index]; -- if (top === void 0) { -- return void 0; -- } -- const itemSize = (_a3 = getItemSizeAtIndex(ctx, index)) != null ? _a3 : 0; -- return top + (Number.isFinite(itemSize) ? itemSize : 0); ++ } ++ } ++ } ++} + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -13112,8 +11116,7 @@ index 95465f2..9ac6d3f 100644 + state.scroll = offset; + state.scrollPending = offset; + state.scrollPrev = offset; - } --function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { ++} +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -13125,65 +11128,17 @@ index 95465f2..9ac6d3f 100644 +} +function supersedeInitialScroll(ctx) { + var _a3, _b, _c; - const state = ctx.state; -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return void 0; -- } -- const viewportStart = Math.max(0, targetOffset); -- const viewportEnd = Math.max(viewportStart, targetOffset + state.scrollLength); -- let start = targetIndex !== void 0 ? Math.max(0, Math.min(dataLength - 1, targetIndex)) : findPositionIndexAtOrBeforeOffset(ctx, viewportStart); -- if (start === void 0) { -- return void 0; -- } -- if (targetIndex !== void 0 && state.positions[start] === void 0) { -- return { end: start, start }; -- } -- if (targetIndex === void 0) { -- const startBottom = getItemBottom(ctx, start); -- if (startBottom === void 0 || startBottom <= viewportStart) { -- return void 0; -- } -- } -- while (start > 0) { -- const top = state.positions[start]; -- if (top === void 0 || top <= viewportStart || state.positions[start - 1] === void 0) { -- break; -- } -- start--; -- } -- while (start > 0) { -- const previousBottom = getItemBottom(ctx, start - 1); -- if (previousBottom === void 0 || previousBottom <= viewportStart) { -- break; ++ const state = ctx.state; + const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; + if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { + if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { + cancelAnimationFrame(bootstrapInitialScroll.frameHandle); - } -- start--; -- } -- let end = start; -- while (end + 1 < dataLength) { -- const nextTop = state.positions[end + 1]; -- if (nextTop === void 0 || nextTop > viewportEnd) { -- break; ++ } + if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { + cancelScrollCompletionChecks(state); + state.scrollingTo = void 0; + state.scrollTargetPinnedRange = void 0; - } -- end++; -- } -- return { end, start }; --} --function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -- if (range) { -- ctx.state.scrollTargetPinnedRange = range; -- ctx.state.scrollForNextCalculateItemsInView = void 0; -- } else { -- ctx.state.scrollTargetPinnedRange = void 0; ++ } + initialScrollCompletion.resetFlags(state); + setInitialScrollSession(state, { bootstrap: null }); + finishInitialScroll(ctx); @@ -13292,15 +11247,15 @@ index 95465f2..9ac6d3f 100644 } // src/core/scrollToIndex.ts -@@ -4329,6 +4465,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4329,6 +4467,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); -+ const minIndexSizeChangedThisPass = dataChanged ? 0 : minIndexSizeChanged; ++ const minIndexSizeChangedThisPass = minIndexSizeChanged; if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4346,8 +4483,17 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4346,8 +4485,17 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -13320,11 +11275,33 @@ index 95465f2..9ac6d3f 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5894,6 +6040,119 @@ function useRafCoalescer(callback) { - return coalescer; +@@ -5906,6 +6054,21 @@ function getDocumentScrollerNode() { + } + return document.scrollingElement || document.documentElement || document.body; + } ++function getScrollAxis(horizontal) { ++ return horizontal ? { ++ contentSizeKey: "scrollWidth", ++ paddingEndProp: "paddingRight", ++ viewportSizeKey: "clientWidth", ++ x: 1, ++ y: 0 ++ } : { ++ contentSizeKey: "scrollHeight", ++ paddingEndProp: "paddingBottom", ++ viewportSizeKey: "clientHeight", ++ x: 0, ++ y: 1 ++ }; ++} + function getWindowScrollPosition() { + var _a3, _b, _c, _d; + if (typeof window === "undefined") { +@@ -5982,9 +6145,175 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll + }; } -+// src/components/temporaryEndPadding.ts ++// src/components/webTemporaryEndPadding.ts +var entriesByNode = /* @__PURE__ */ new WeakMap(); +var nextRequestId = 1; +function readResolvedPadding(node, prop) { @@ -13345,6 +11322,20 @@ index 95465f2..9ac6d3f 100644 + node.style[prop] = total === 0 ? entry.baseline : `${entry.baselineSize + total}px`; + entry.lastApplied = node.style[prop]; +} ++function drainPendingReleases(entry) { ++ if (entry.resetHandle !== void 0) { ++ cancelAnimationFrame(entry.resetHandle); ++ entry.resetHandle = void 0; ++ } ++ if (entry.pendingReleases.size === 0) { ++ return; ++ } ++ const releases = [...entry.pendingReleases]; ++ entry.pendingReleases.clear(); ++ for (const pending of releases) { ++ pending(); ++ } ++} +function releaseEntry(node, prop, requestId) { + const entries = entriesByNode.get(node); + const entry = entries == null ? void 0 : entries[prop]; @@ -13355,10 +11346,8 @@ index 95465f2..9ac6d3f 100644 + applyPadding(node, prop, entry); + } + if (entry.requests.size === 0) { -+ if (entry.resetHandle !== void 0) { -+ cancelAnimationFrame(entry.resetHandle); -+ } + entries == null ? true : delete entries[prop]; ++ drainPendingReleases(entry); + } +} +function addTemporaryEndPadding(node, prop, extraSize) { @@ -13411,7 +11400,10 @@ index 95465f2..9ac6d3f 100644 +} +function getTemporaryEndPadding(node, prop) { + var _a3; -+ const entry = node ? (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop] : void 0; ++ if (!node) { ++ return 0; ++ } ++ const entry = (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop]; + if (!entry || !isOwnedByUs(node, prop, entry)) { + return 0; + } @@ -13427,43 +11419,77 @@ index 95465f2..9ac6d3f 100644 + if (!entry) { + continue; + } -+ if (entry.resetHandle !== void 0) { -+ cancelAnimationFrame(entry.resetHandle); -+ } + if (isOwnedByUs(node, prop, entry)) { + node.style[prop] = entry.baseline; + } + delete entries[prop]; ++ entry.requests.clear(); ++ drainPendingReleases(entry); + } +} + - // src/components/webConstants.ts - var LEGEND_LIST_CONTENT_CONTAINER_CLASS = "legend-list-content-container"; - var LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS = "legend-list-scrollbar-x-hidden"; -@@ -5985,6 +6244,10 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; +var SMOOTH_SCROLL_MAX_MS = 2e3; ++var MAX_BORROW_VIEWPORTS = 3; ++var BORROW_WATCH_FRAME_INTERVAL = 3; +var BORROW_MEASURE_ATTEMPTS = 2; -+var USER_INTERACTION_EVENTS = ["wheel", "pointerdown", "touchstart", "keydown"]; ++var USER_DRAG_SLOP = 8; ++var WHEEL_MOMENTUM_GRACE_MS = 150; ++var SCROLL_KEYS = /* @__PURE__ */ new Set([ ++ " ", ++ "ArrowDown", ++ "ArrowLeft", ++ "ArrowRight", ++ "ArrowUp", ++ "End", ++ "Home", ++ "PageDown", ++ "PageUp" ++]); ++function isTextEntryTarget(target) { ++ var _a3; ++ const element = target; ++ const tag = (_a3 = element == null ? void 0 : element.tagName) == null ? void 0 : _a3.toLowerCase(); ++ return tag === "input" || tag === "textarea" || tag === "select" || !!(element == null ? void 0 : element.isContentEditable); ++} ++function pointerPosition(event) { ++ var _a3; ++ const touch = (_a3 = event.touches) == null ? void 0 : _a3[0]; ++ if (touch) { ++ return { x: touch.clientX, y: touch.clientY }; ++ } ++ const pointer = event; ++ return typeof pointer.clientX === "number" ? { x: pointer.clientX, y: pointer.clientY } : void 0; ++} ++function isScrollKey(event) { ++ if (event.altKey || event.ctrlKey || event.metaKey) { ++ return false; ++ } ++ return SCROLL_KEYS.has(event.key) && !isTextEntryTarget(event.target); ++} var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6053,6 +6316,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6053,6 +6382,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); -+ const paddingEndProp = horizontal ? isHorizontalRTL(ctx.state) ? "paddingLeft" : "paddingRight" : "paddingBottom"; ++ const paddingEndProp = getScrollAxis(horizontal).paddingEndProp; + const getCommittedMaxScrollOffset = useCallback( + (maxOffset) => Math.max(0, maxOffset - getTemporaryEndPadding(contentRef.current, paddingEndProp)), + [paddingEndProp] + ); ++ const getViewportExtent = useCallback(() => { ++ const layout = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); ++ return horizontal ? layout.width : layout.height; ++ }, [horizontal, isWindowScroll]); const getMaxScrollOffset = useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6073,6 +6341,95 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,8 +6411,153 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -13478,7 +11504,13 @@ index 95465f2..9ac6d3f 100644 + const contentNode = contentRef.current; + (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); + const committedMaxOffset = getCommittedMaxScrollOffset(getMaxScrollOffset()); -+ if (!contentNode || offset <= committedMaxOffset + SCROLL_EXTENT_EPSILON) { ++ const viewportExtent = getViewportExtent(); ++ const reachLimit = viewportExtent > 0 ? committedMaxOffset + MAX_BORROW_VIEWPORTS * viewportExtent : Number.POSITIVE_INFINITY; ++ if (!contentNode || !Number.isFinite(offset) || offset > reachLimit) { ++ run(clampOffset(offset, maxOffset)); ++ return; ++ } ++ if (offset <= committedMaxOffset + SCROLL_EXTENT_EPSILON) { + run(clampOffset(offset, maxOffset)); + return; + } @@ -13506,12 +11538,13 @@ index 95465f2..9ac6d3f 100644 + } + const scrollTarget = getScrollTarget(); + const supportsScrollEnd = !!scrollTarget && "onscrollend" in scrollTarget; ++ const timers = {}; + const finish = () => { + if (animatedPaddingReleaseRef.current !== finish) { + return; + } + animatedPaddingReleaseRef.current = void 0; -+ clearTimeout(settleTimeout); ++ clearTimeout(timers.settle); + scrollTarget == null ? void 0 : scrollTarget.removeEventListener("scrollend", finishIfArrived); + cancelAnimationFrame(borrowWatchRef.current); + release(); @@ -13521,25 +11554,36 @@ index 95465f2..9ac6d3f 100644 + finish(); + } + }; ++ let framesUntilCheck = 0; + const releaseWhenContentCommits = () => { + if (animatedPaddingReleaseRef.current !== finish) { + return; + } -+ if (getCommittedMaxScrollOffset(getMaxScrollOffset()) + SCROLL_EXTENT_EPSILON >= offset) { -+ finish(); -+ return; ++ if (framesUntilCheck-- <= 0) { ++ framesUntilCheck = BORROW_WATCH_FRAME_INTERVAL; ++ if (getCommittedMaxScrollOffset(getMaxScrollOffset()) + SCROLL_EXTENT_EPSILON >= offset) { ++ finish(); ++ return; ++ } + } + borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); + }; + animatedPaddingReleaseRef.current = finish; + cancelAnimationFrame(borrowWatchRef.current); + borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); -+ const settleTimeout = setTimeout(finish, SMOOTH_SCROLL_MAX_MS); ++ timers.settle = setTimeout(finish, SMOOTH_SCROLL_MAX_MS); + if (supportsScrollEnd) { + scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finishIfArrived); + } + }, -+ [getCommittedMaxScrollOffset, getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, paddingEndProp] ++ [ ++ getCommittedMaxScrollOffset, ++ getCurrentScrollOffset, ++ getMaxScrollOffset, ++ getScrollTarget, ++ getViewportExtent, ++ paddingEndProp ++ ] + ); + useEffect( + () => () => { @@ -13553,13 +11597,53 @@ index 95465f2..9ac6d3f 100644 + }, + [] + ); ++ const interactionArmedAtRef = useRef(0); ++ const dragOriginRef = useRef(void 0); + const reportUserInteraction = useCallback(() => { -+ releaseScrollTargetForUserInteraction(ctx); ++ dragOriginRef.current = void 0; ++ releaseScrollTargetForUserInteraction(ctx.state); + }, [ctx]); ++ const onWheel = useCallback( ++ (event) => { ++ if (event.timeStamp - interactionArmedAtRef.current < WHEEL_MOMENTUM_GRACE_MS) { ++ return; ++ } ++ reportUserInteraction(); ++ }, ++ [reportUserInteraction] ++ ); ++ const onPointerDown = useCallback((event) => { ++ const point = pointerPosition(event); ++ dragOriginRef.current = point; ++ }, []); ++ const onPointerMove = useCallback( ++ (event) => { ++ const origin = dragOriginRef.current; ++ const point = origin && pointerPosition(event); ++ if (!origin || !point) { ++ return; ++ } ++ if (Math.abs(point.x - origin.x) > USER_DRAG_SLOP || Math.abs(point.y - origin.y) > USER_DRAG_SLOP) { ++ reportUserInteraction(); ++ } ++ }, ++ [reportUserInteraction] ++ ); ++ const onKeyDown = useCallback( ++ (event) => { ++ if (isScrollKey(event)) { ++ reportUserInteraction(); ++ } ++ }, ++ [reportUserInteraction] ++ ); const scrollToLocalOffset = useCallback( (offset, animated) => { ++ interactionArmedAtRef.current = typeof performance !== "undefined" ? performance.now() : 0; const scrollElement = scrollRef.current; -@@ -6095,14 +6452,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + const target = getScrollTarget(); + if (!target || typeof target.scrollTo !== "function") { +@@ -6095,14 +6578,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); options.left = left; options.top = top; @@ -13586,7 +11670,7 @@ index 95465f2..9ac6d3f 100644 ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6492,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6128,8 +6618,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -13596,7 +11680,7 @@ index 95465f2..9ac6d3f 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6142,7 +6505,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6142,7 +6631,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ if (!onScroll2 || !scrollRef.current) { return; } @@ -13613,25 +11697,74 @@ index 95465f2..9ac6d3f 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6200,11 +6571,17 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6200,11 +6697,26 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ const target = getScrollTarget(); if (!target) return; target.addEventListener("scroll", handleScroll, { passive: true }); -+ for (const type of USER_INTERACTION_EVENTS) { -+ target.addEventListener(type, reportUserInteraction, { passive: true }); -+ } ++ const listenerOptions = { capture: true, passive: true }; ++ const removeOptions = { capture: true }; ++ const interactionTarget = scrollRef.current; ++ target.addEventListener("wheel", onWheel, listenerOptions); ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener("pointerdown", onPointerDown, listenerOptions); ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener("pointermove", onPointerMove, listenerOptions); ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener("touchstart", onPointerDown, listenerOptions); ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener("touchmove", onPointerMove, listenerOptions); ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener("keydown", onKeyDown, listenerOptions); if ("onscrollend" in target) { target.addEventListener("scrollend", emitScrollEnd); } return () => { target.removeEventListener("scroll", handleScroll); -+ for (const type of USER_INTERACTION_EVENTS) { -+ target.removeEventListener(type, reportUserInteraction); -+ } ++ target.removeEventListener("wheel", onWheel, removeOptions); ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("pointerdown", onPointerDown, removeOptions); ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("pointermove", onPointerMove, removeOptions); ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("touchstart", onPointerDown, removeOptions); ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("touchmove", onPointerMove, removeOptions); ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("keydown", onKeyDown, removeOptions); if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6390,8 +6767,6 @@ function ScrollAdjust() { +@@ -6215,7 +6727,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + } + scrollEventCoalescer.cancel(); + }; +- }, [emitScrollEnd, getScrollTarget, handleScroll, scrollEventCoalescer]); ++ }, [ ++ emitScrollEnd, ++ getScrollTarget, ++ handleScroll, ++ onKeyDown, ++ onPointerDown, ++ onPointerMove, ++ onWheel, ++ scrollEventCoalescer ++ ]); + useEffect(() => { + const doScroll = () => { + if (contentOffset) { +@@ -6358,21 +6879,6 @@ function useValueListener$(key, callback) { + } + + // src/components/ScrollAdjust.tsx +-function getScrollAdjustAxis(horizontal) { +- return horizontal ? { +- contentSizeKey: "scrollWidth", +- paddingEndProp: "paddingRight", +- viewportSizeKey: "clientWidth", +- x: 1, +- y: 0 +- } : { +- contentSizeKey: "scrollHeight", +- paddingEndProp: "paddingBottom", +- viewportSizeKey: "clientHeight", +- x: 0, +- y: 1 +- }; +-} + function getScrollAdjustTarget(ctx, contentNode) { + var _a3, _b, _c; + const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; +@@ -6390,8 +6896,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3.useRef(0); const lastScrollAdjustUserOffsetRef = React3.useRef(0); @@ -13640,7 +11773,16 @@ index 95465f2..9ac6d3f 100644 const contentNodeRef = React3.useRef(null); const callback = React3.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6417,29 +6792,10 @@ function ScrollAdjust() { +@@ -6402,7 +6906,7 @@ function ScrollAdjust() { + const target = getScrollAdjustTarget(ctx, contentNodeRef.current); + if (target) { + const horizontal = !!ctx.state.props.horizontal; +- const axis = getScrollAdjustAxis(horizontal); ++ const axis = getScrollAxis(horizontal); + const { contentNode, scrollElement: el } = target; + const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; + const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; +@@ -6417,29 +6921,10 @@ function ScrollAdjust() { const nextScroll = currentScroll + scrollDelta; const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; if (needsTemporaryPadding) { @@ -13672,11 +11814,11 @@ index 95465f2..9ac6d3f 100644 } else { scrollBy(); } -@@ -8267,6 +8623,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8267,6 +8752,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); -+ clearScrollTargetSettle(state); ++ releaseScrollTargetForUserInteraction(state); (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts index 00ca773a5542..5c332481f515 100644 --- a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts +++ b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts @@ -1,5 +1,6 @@ +import type {ChainablePromiseElement} from 'webdriverio' import {expect} from '@wdio/globals' -import {anyExist, el, els, tab, waitForTestID} from '../helpers/elements' +import {anyExist, byText, el, els, tab, waitForTestID} from '../helpers/elements' import {escapeToTabs} from '../helpers/navigate' import * as T from '../../shared/test-ids' @@ -11,6 +12,12 @@ const QUERY = 'one' // A word whose hit sits among the messages already on screen: jumping a few rows is the case where // the list has nothing to load and the scroll lands against a content size that has not caught up. const SAME_SCREEN_QUERY = 'working' +// Flings back through the thread until the top of the loaded page is on screen. +const MAX_FLINGS = 20 +const FLING_DISTANCE = 420 +// A fling moves the top-of-thread marker toward the viewport; only a prepend moves it away, and by +// far more than a fling's worth. +const PREPEND_MIN_SHIFT = 600 // iOS 26 puts the conversation's header actions in a native overflow menu (one glass pill), so // there is no React view to carry a testID - the bar button and its menu item are addressed by the @@ -27,24 +34,52 @@ const openThreadSearch = async () => { await el(T.CHAT_HEADER_SEARCH_BUTTON).click() } -const boundsOf = async (id: string) => { - const element = el(id) +type Bounds = {height: number; y: number} + +const boundsOfElement = async (element: ChainablePromiseElement): Promise => { const [location, size] = await Promise.all([element.getLocation(), element.getSize()]) return {height: size.height, y: location.y} } +const boundsOf = async (id: string): Promise => boundsOfElement(el(id)) + +// Off-screen rows are pruned from the accessibility tree, so a missing element is a real answer +// ("not on screen"), not an error - the caller decides what that means. +const maybeBoundsOf = async (element: ChainablePromiseElement): Promise => { + if (!(await element.isExisting().catch(() => false))) return undefined + return boundsOfElement(element).catch(() => undefined) +} + +const overlapsViewport = (thing: Bounds, list: Bounds): boolean => + Math.min(thing.y + thing.height, list.y + list.height) - Math.max(thing.y, list.y) > 0 + // The row keeps its marker while it is the selected hit, but a virtualised list renders rows // outside the viewport too - so the marker existing says nothing about whether it can be seen. // Compare where the row is against where the list is. const hitOverlapsViewport = async (): Promise => { const [hit, list] = await Promise.all([boundsOf(T.CHAT_SEARCH_HIT), boundsOf(T.CHAT_MESSAGE_LIST)]) - const overlap = Math.min(hit.y + hit.height, list.y + list.height) - Math.max(hit.y, list.y) - if (overlap <= 0) { + const overlaps = overlapsViewport(hit, list) + if (!overlaps) { console.log(`hit off screen: row ${hit.y}..${hit.y + hit.height}, list ${list.y}..${list.y + list.height}`) } - return overlap > 0 + return overlaps } +// The selected hit once the thread has been dragged away from it: undefined when the row has left +// the viewport entirely, which is the state the drag is meant to produce. +const hitIfOnScreen = async (): Promise => { + const hit = await maybeBoundsOf(el(T.CHAT_SEARCH_HIT)) + if (!hit) return undefined + const list = await boundsOf(T.CHAT_MESSAGE_LIST) + return overlapsViewport(hit, list) ? hit : undefined +} + +// The thread's top-of-loaded-window marker. It stays in the render window while off screen, so its +// position is readable throughout - and a page-in is visible in that position directly: a fling +// moves the marker back toward the viewport, while a prepend pushes it thousands of pixels away. +const loadingOlderPosition = async (): Promise => + (await maybeBoundsOf(byText('Digging ancient')))?.y + const runSearch = async (query: string, steps: number) => { await openThreadSearch() await waitForTestID(T.CHAT_THREAD_SEARCH_NEXT, 5000) @@ -74,6 +109,20 @@ const closeThreadSearch = async () => { await browser.pause(500) } +// A fast flick that coasts - used only to travel back through the thread, never to establish the +// position the assertion depends on. +const flingThread = async (distance: number) => { + const list = await boundsOf(T.CHAT_MESSAGE_LIST) + const midY = Math.round(list.y + list.height / 2) + await browser + .action('pointer') + .move({x: 200, y: midY - distance / 2}) + .down() + .move({duration: 120, x: 200, y: midY + distance / 2}) + .up() + .perform() +} + // Drag the thread without lifting into a fling, so it ends where it was left rather than coasting. const dragThread = async (distance: number) => { const list = await boundsOf(T.CHAT_MESSAGE_LIST) @@ -124,26 +173,39 @@ describe('chat thread search', () => { // A moment after landing is when the list is still measuring, and where anything holding the // scroll target used to pull the thread back out from under the user. await browser.pause(1000) - const beforeDrag = await boundsOf(T.CHAT_SEARCH_HIT) - // Several drags toward older messages, which is what asks the thread to page more in. The - // prepend that follows shifts every row's index, and that is what used to re-centre the list - // out from under the reader. - for (let drag = 0; drag < 3; drag++) { - await dragThread(260) - await browser.pause(250) + expect(await hitIfOnScreen()).toBeDefined() + + // Travel back toward older messages until a page of them actually arrives. The page-in is the + // point: the prepend shifts every row's index, and an index-keyed re-centre reads that as a new + // target and yanks the thread back to the hit. + let previousMarker = await loadingOlderPosition() + let pagedIn = false + for (let fling = 0; fling < MAX_FLINGS && !pagedIn; fling++) { + await flingThread(FLING_DISTANCE) + await browser.pause(200) + const marker = await loadingOlderPosition() + if (marker !== undefined && previousMarker !== undefined && marker < previousMarker - PREPEND_MIN_SHIFT) { + console.log(`page-in: top-of-thread marker moved ${previousMarker} -> ${marker}`) + pagedIn = true + } + previousMarker = marker } - await browser.pause(300) - const afterDrag = await boundsOf(T.CHAT_SEARCH_HIT) + // Not provoking a page-in proves nothing about snapping back, so fail instead of passing. + if (!pagedIn) throw new Error('dragging never loaded another page of older messages') + + // End on a controlled drag so the thread rests where the user left it rather than coasting. + await dragThread(200) + await browser.pause(400) + if (await hitIfOnScreen()) throw new Error('the hit never left the viewport') - // The drag has to have actually moved the thread, or the rest of this proves nothing. - expect(Math.abs(afterDrag.y - beforeDrag.y)).toBeGreaterThan(40) + // Long enough for the page to arrive, prepend, and for the list to finish measuring it. + await browser.pause(2500) - await browser.pause(1500) - const settled = await boundsOf(T.CHAT_SEARCH_HIT) - if (Math.abs(settled.y - afterDrag.y) > 30) { - console.log(`thread snapped back: row was at ${afterDrag.y} after the drag, ${settled.y} a moment later`) + const snappedBack = await hitIfOnScreen() + if (snappedBack) { + console.log(`thread snapped back to the hit: row at ${snappedBack.y} after being dragged away`) } - expect(Math.abs(settled.y - afterDrag.y)).toBeLessThanOrEqual(30) + expect(snappedBack).toBeUndefined() await closeThreadSearch() }) From d4ba158dd1655e8ecc15bd874697606cf2d91a20 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 11 Aug 2026 19:54:40 -0400 Subject: [PATCH 09/38] test(e2e): make the thread-search flow addressable and honest Review of the first version turned up three ways it could pass or fail for the wrong reason, all of them things that would only show up on a device that isn't this one: - `~More` and `~Search` matched anywhere in the tree, including the More tab and the inbox's own search field. Both are now scoped to the navigation bar and the presented menu. - Gestures used a hardcoded x of 200, which on a tablet lands in the inbox pane beside the thread, so the thread would never page in and the failure would claim it did not load. Gestures are now measured from the thread's own frame, less the search bar that overlays it. - Keys went to whatever had focus, but the search field focuses itself a beat after mounting. It carries a testID now and the flow types into it, which also routes through the paste path that exists because per-key injection crashes the app on the older sims. The page-in detector watched for the string "Digging ancient messages...", which unmounts in the very window it was watching for - the marker is replaced while the load is in flight. SpecialTopMessage carries a testID now and is mounted in every state, so the flow measures a thing that stays put. The testID'd wrappers on the hit row, the search bar's Cancel and the search input carry collapsable={false}, or Android view flattening leaves the testID on an empty leaf - which on that platform would make the drag case pass vacuously, since a missing row reads as "not on screen", which is what it asserts. Assertions now report what they saw, require more than a hairline of the row to be visible, read the hit count out of the search bar so wrapping around is guaranteed rather than assumed, and stop swallowing driver errors as "off screen". On teeth, honestly: the wrapping case is real. The drag case reliably provokes a page-in and logs it, but its snap-back detection is timing-dependent - the app mutation that used to fail it three times out of three now fails it about one run in three, and mocha's retries can hide even that. Do not read a green there as proof. The same-screen case still has no native mutation that fails it. Also updates the legend-list patch to the reviewed fork branch. --- .../messages/special-top-message.tsx | 9 +- .../conversation/messages/wrapper/wrapper.tsx | 1 + shared/chat/conversation/search.tsx | 12 +- shared/patches/@legendapp+list+3.3.5.patch | 1856 +++++++++++------ .../ios-appium/flows/chat-search-hit.test.ts | 251 ++- shared/tests/e2e/shared/test-ids.ts | 12 +- 6 files changed, 1372 insertions(+), 769 deletions(-) diff --git a/shared/chat/conversation/messages/special-top-message.tsx b/shared/chat/conversation/messages/special-top-message.tsx index 6b6c4bc021eb..808bd3947e53 100644 --- a/shared/chat/conversation/messages/special-top-message.tsx +++ b/shared/chat/conversation/messages/special-top-message.tsx @@ -15,6 +15,7 @@ import { import {useConversationParticipantsSelector} from '../data-hooks' import * as FS from '@/constants/fs' import {useCurrentUserState} from '@/stores/current-user' +import * as TestIDs from '@/tests/e2e/shared/test-ids' const ErrorMessage = () => { const styles = useStyles() @@ -153,7 +154,13 @@ function SpecialTopMessage() { } return ( - + {hasLoadedEver && loadMoreType === 'noMoreToLoad' && showRetentionNotice && } {hasOlderResetConversation && } diff --git a/shared/chat/conversation/messages/wrapper/wrapper.tsx b/shared/chat/conversation/messages/wrapper/wrapper.tsx index f37b910283ce..a339e2e9108f 100644 --- a/shared/chat/conversation/messages/wrapper/wrapper.tsx +++ b/shared/chat/conversation/messages/wrapper/wrapper.tsx @@ -1023,6 +1023,7 @@ export function WrapperMessage(p: WrapperMessageProps) { direction="vertical" relative={true} fullWidth={true} + collapsable={false} testID={showCenteredHighlight ? TestIDs.CHAT_SEARCH_HIT : undefined} > @@ -518,7 +519,16 @@ const ThreadSearchMobileInner = function ThreadSearchMobileInner(p: CommonProps) - + {/* collapsable={false}: keep this testID'd wrapper (and the EditText under it) as a real + view on Android, where view flattening would otherwise render it as an empty leaf. */} + { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1977,6 +1775,27 @@ var flushSync = (fn) => { - fn(); - }; +@@ -1972,11 +1770,32 @@ function prepareMVCP(ctx, dataChanged) { + } + } +-// src/platform/flushSync.native.ts +-var flushSync = (fn) => { +- fn(); +-}; +- ++// src/platform/flushSync.native.ts ++var flushSync = (fn) => { ++ fn(); ++}; ++ +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { @@ -1205,7 +1215,7 @@ index b3c5a30..1fb0c56 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2276,6 +2104,316 @@ function scrollTo(ctx, params) { +@@ -2276,6 +2104,323 @@ function scrollTo(ctx, params) { } } @@ -1216,9 +1226,7 @@ index b3c5a30..1fb0c56 100644 +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; +function releaseScrollTargetForUserInteraction(state) { -+ if (state.scrollTargetSettle) { -+ clearScrollTargetSettle(state); -+ } ++ clearScrollTargetSettle(state); +} +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; @@ -1283,6 +1291,12 @@ index b3c5a30..1fb0c56 100644 + viewOffset: settle.viewOffset, + viewPosition: settle.viewPosition + }); ++ const scrollingTo = state.scrollingTo; ++ if (scrollingTo && scrollingTo.index === index) { ++ const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); ++ scrollingTo.targetOffset = correctedOffset; ++ scrollingTo.offset = position; ++ } + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +} +function settleScrollTarget(ctx, options) { @@ -1295,7 +1309,10 @@ index b3c5a30..1fb0c56 100644 + const measured = options == null ? void 0 : options.minIndexSizeChanged; + if (measured !== void 0) { + settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); -+ settle.expiresAt = Date.now() + SETTLE_TTL_MS; ++ const targetIndex = state.indexByKey.get(settle.id); ++ if (targetIndex !== void 0 && measured <= targetIndex) { ++ settle.expiresAt = Date.now() + SETTLE_TTL_MS; ++ } + } + if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { @@ -1522,7 +1539,7 @@ index b3c5a30..1fb0c56 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4320,6 +4458,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4320,6 +4465,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -1530,7 +1547,7 @@ index b3c5a30..1fb0c56 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4337,8 +4476,17 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4337,8 +4483,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -1539,18 +1556,19 @@ index b3c5a30..1fb0c56 100644 + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); + const mvcp = state.props.maintainVisibleContentPosition; -+ if (dataChanged && !mvcp.data && !mvcp.size) { -+ clearScrollTargetSettle(state); -+ } ++ const isUnanchoredDataChange = dataChanged && !mvcp.data; + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { -+ settleScrollTarget(ctx, { isCompensating, minIndexSizeChanged: minIndexSizeChangedThisPass }); ++ settleScrollTarget(ctx, { ++ isCompensating, ++ minIndexSizeChanged: isUnanchoredDataChange ? 0 : minIndexSizeChangedThisPass ++ }); + } + if (didMVCPAdjust) { updateScroll2(state.scroll); updateScrollRange(); } -@@ -7652,6 +7800,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7652,6 +7808,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -1559,7 +1577,7 @@ index b3c5a30..1fb0c56 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs -index 40e87cd..64bfde3 100644 +index 40e87cd..b9895df 100644 --- a/node_modules/@legendapp/list/react-native.mjs +++ b/node_modules/@legendapp/list/react-native.mjs @@ -436,611 +436,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -2703,10 +2721,20 @@ index 40e87cd..64bfde3 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1956,6 +1754,27 @@ var flushSync = (fn) => { - fn(); - }; +@@ -1951,11 +1749,32 @@ function prepareMVCP(ctx, dataChanged) { + } + } +-// src/platform/flushSync.native.ts +-var flushSync = (fn) => { +- fn(); +-}; +- ++// src/platform/flushSync.native.ts ++var flushSync = (fn) => { ++ fn(); ++}; ++ +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { @@ -2765,7 +2793,7 @@ index 40e87cd..64bfde3 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2255,6 +2083,316 @@ function scrollTo(ctx, params) { +@@ -2255,6 +2083,323 @@ function scrollTo(ctx, params) { } } @@ -2776,9 +2804,7 @@ index 40e87cd..64bfde3 100644 +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; +function releaseScrollTargetForUserInteraction(state) { -+ if (state.scrollTargetSettle) { -+ clearScrollTargetSettle(state); -+ } ++ clearScrollTargetSettle(state); +} +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; @@ -2843,6 +2869,12 @@ index 40e87cd..64bfde3 100644 + viewOffset: settle.viewOffset, + viewPosition: settle.viewPosition + }); ++ const scrollingTo = state.scrollingTo; ++ if (scrollingTo && scrollingTo.index === index) { ++ const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); ++ scrollingTo.targetOffset = correctedOffset; ++ scrollingTo.offset = position; ++ } + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +} +function settleScrollTarget(ctx, options) { @@ -2855,7 +2887,10 @@ index 40e87cd..64bfde3 100644 + const measured = options == null ? void 0 : options.minIndexSizeChanged; + if (measured !== void 0) { + settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); -+ settle.expiresAt = Date.now() + SETTLE_TTL_MS; ++ const targetIndex = state.indexByKey.get(settle.id); ++ if (targetIndex !== void 0 && measured <= targetIndex) { ++ settle.expiresAt = Date.now() + SETTLE_TTL_MS; ++ } + } + if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { @@ -3082,7 +3117,7 @@ index 40e87cd..64bfde3 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4299,6 +4437,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4299,6 +4444,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -3090,7 +3125,7 @@ index 40e87cd..64bfde3 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4316,8 +4455,17 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4316,8 +4462,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -3099,18 +3134,19 @@ index 40e87cd..64bfde3 100644 + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); + const mvcp = state.props.maintainVisibleContentPosition; -+ if (dataChanged && !mvcp.data && !mvcp.size) { -+ clearScrollTargetSettle(state); -+ } ++ const isUnanchoredDataChange = dataChanged && !mvcp.data; + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { -+ settleScrollTarget(ctx, { isCompensating, minIndexSizeChanged: minIndexSizeChangedThisPass }); ++ settleScrollTarget(ctx, { ++ isCompensating, ++ minIndexSizeChanged: isUnanchoredDataChange ? 0 : minIndexSizeChangedThisPass ++ }); + } + if (didMVCPAdjust) { updateScroll2(state.scroll); updateScrollRange(); } -@@ -7631,6 +7779,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7631,6 +7787,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -3119,7 +3155,7 @@ index 40e87cd..64bfde3 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js -index 914d2da..f03ba1e 100644 +index 914d2da..6240e13 100644 --- a/node_modules/@legendapp/list/react-native.web.js +++ b/node_modules/@legendapp/list/react-native.web.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -3728,25 +3764,15 @@ index 914d2da..f03ba1e 100644 // src/core/getStartOffsetAdjustment.ts function getStartOffsetAdjustment(ctx) { const { state } = ctx; -@@ -1234,58 +635,273 @@ function getItemSizeAtIndex(ctx, index) { - if (index === void 0 || index < 0) { - return void 0; +@@ -1247,45 +648,260 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { + if (viewOffset) { + offset -= viewOffset; } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+ const targetId = getId(ctx.state, index); -+ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+} -+ -+// src/core/calculateOffsetWithOffsetPosition.ts -+function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -+ var _a3; -+ const state = ctx.state; -+ const { index, viewOffset, viewPosition } = params; -+ let offset = offsetParam; -+ if (viewOffset) { -+ offset -= viewOffset; -+ } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } + if (index !== void 0) { + const startOffsetAdjustment = getStartOffsetAdjustment(ctx); + if (startOffsetAdjustment) { @@ -3956,7 +3982,22 @@ index 914d2da..f03ba1e 100644 + if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { + state.isStartReached = false; + state.startReachedSnapshot = void 0; -+ } + } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } + set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); + set$(ctx, "isNearStart", scroll <= threshold); + const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; @@ -3983,50 +4024,19 @@ index 914d2da..f03ba1e 100644 + state.startReachedSnapshot = snapshot; + } + ); -+ } + } +- return offset; } --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; -- } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } -- } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } -- } -- return offset; +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); - } - --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { ++} ++ +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { + var _a3, _b; @@ -4240,6 +4250,68 @@ index 914d2da..f03ba1e 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; +@@ -1525,7 +1317,7 @@ var MVCP_POSITION_EPSILON = 0.1; + var MVCP_ANCHOR_LOCK_TTL_MS = 300; + var MVCP_ANCHOR_LOCK_QUIET_PASSES_TO_RELEASE = 2; + var NATIVE_END_CLAMP_EPSILON = 1; +-function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { ++function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now2) { + if (!enableMVCPAnchorLock) { + state.mvcpAnchorLock = void 0; + return void 0; +@@ -1534,7 +1326,7 @@ function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { + if (!lock) { + return void 0; + } +- const isExpired = now > lock.expiresAt; ++ const isExpired = now2 > lock.expiresAt; + const isMissing = state.indexByKey.get(lock.id) === void 0; + if (isExpired || isMissing || !mvcpData) { + state.mvcpAnchorLock = void 0; +@@ -1544,7 +1336,7 @@ function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { + } + function updateAnchorLock(state, params) { + { +- const { anchorId, anchorPosition, dataChanged, now, positionDiff } = params; ++ const { anchorId, anchorPosition, dataChanged, now: now2, positionDiff } = params; + const enableMVCPAnchorLock = !!dataChanged || !!state.mvcpAnchorLock; + const mvcpData = state.props.maintainVisibleContentPosition.data; + if (!enableMVCPAnchorLock || !mvcpData || state.scrollingTo || !anchorId || anchorPosition === void 0) { +@@ -1557,7 +1349,7 @@ function updateAnchorLock(state, params) { + return; + } + state.mvcpAnchorLock = { +- expiresAt: now + MVCP_ANCHOR_LOCK_TTL_MS, ++ expiresAt: now2 + MVCP_ANCHOR_LOCK_TTL_MS, + id: anchorId, + position: anchorPosition, + quietPasses +@@ -1662,14 +1454,14 @@ function prepareMVCP(ctx, dataChanged) { + const { + maintainVisibleContentPosition: { data: mvcpData, size: mvcpScroll, shouldRestorePosition } + } = props; +- const now = Date.now(); ++ const now2 = Date.now(); + const enableMVCPAnchorLock = (!!dataChanged || !!state.mvcpAnchorLock); + const scrollingTo = state.scrollingTo; + if (dataChanged && state.pendingScrollToEnd && scrollingTo === void 0) { + state.mvcpAnchorLock = void 0; + return void 0; + } +- const anchorLock = resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) ; ++ const anchorLock = resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now2) ; + let prevPosition; + let targetId; + const idsInViewWithPositions = []; +@@ -1786,7 +1578,7 @@ function prepareMVCP(ctx, dataChanged) { + anchorId: anchorIdForLock, + anchorPosition: anchorPositionForLock, + dataChanged, +- now, ++ now: now2, + positionDiff + }); + if (shouldQueueNativeMVCPAdjust()) { @@ -1809,6 +1601,27 @@ function prepareMVCP(ctx, dataChanged) { } } @@ -4268,34 +4340,12 @@ index 914d2da..f03ba1e 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2055,70 +1868,395 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { - if (nextTop === void 0 || nextTop > viewportEnd) { - break; - } -- end++; -- } -- return { end, start }; --} --function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -- if (range) { -- ctx.state.scrollTargetPinnedRange = range; -- ctx.state.scrollForNextCalculateItemsInView = void 0; -- } else { -- ctx.state.scrollTargetPinnedRange = void 0; -+ end++; -+ } -+ return { end, start }; -+} -+function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -+ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -+ if (range) { -+ ctx.state.scrollTargetPinnedRange = range; -+ ctx.state.scrollForNextCalculateItemsInView = void 0; -+ } else { -+ ctx.state.scrollTargetPinnedRange = void 0; -+ } -+} +@@ -2068,57 +1881,389 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + ctx.state.scrollTargetPinnedRange = void 0; + } + } +-function scrollTo(ctx, params) { +- var _a3, _b; +function scrollTo(ctx, params) { + var _a3, _b, _c; + const state = ctx.state; @@ -4365,9 +4415,7 @@ index 914d2da..f03ba1e 100644 +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; +function releaseScrollTargetForUserInteraction(state) { -+ if (state.scrollTargetSettle) { -+ clearScrollTargetSettle(state); -+ } ++ clearScrollTargetSettle(state); +} +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; @@ -4384,11 +4432,11 @@ index 914d2da..f03ba1e 100644 + return; + } + clearScrollTargetSettle(state); -+ const now = Date.now(); ++ const now2 = Date.now(); + state.scrollTargetSettle = { + corrections: 0, -+ deadline: now + SETTLE_MAX_MS, -+ expiresAt: now + SETTLE_TTL_MS, ++ deadline: now2 + SETTLE_MAX_MS, ++ expiresAt: now2 + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, + quietPasses: 0, @@ -4432,6 +4480,12 @@ index 914d2da..f03ba1e 100644 + viewOffset: settle.viewOffset, + viewPosition: settle.viewPosition + }); ++ const scrollingTo = state.scrollingTo; ++ if (scrollingTo && scrollingTo.index === index) { ++ const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); ++ scrollingTo.targetOffset = correctedOffset; ++ scrollingTo.offset = position; ++ } + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +} +function settleScrollTarget(ctx, options) { @@ -4444,7 +4498,10 @@ index 914d2da..f03ba1e 100644 + const measured = options == null ? void 0 : options.minIndexSizeChanged; + if (measured !== void 0) { + settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); -+ settle.expiresAt = Date.now() + SETTLE_TTL_MS; ++ const targetIndex = state.indexByKey.get(settle.id); ++ if (targetIndex !== void 0 && measured <= targetIndex) { ++ settle.expiresAt = Date.now() + SETTLE_TTL_MS; ++ } + } + if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { @@ -4461,8 +4518,8 @@ index 914d2da..f03ba1e 100644 + clearScrollTargetSettle(state); + return false; + } -+ const now = Date.now(); -+ if (now > settle.expiresAt || now > settle.deadline) { ++ const now2 = Date.now(); ++ if (now2 > settle.expiresAt || now2 > settle.deadline) { + clearScrollTargetSettle(state); + return false; + } @@ -4480,7 +4537,7 @@ index 914d2da..f03ba1e 100644 + return false; + } + settle.quietPasses = 0; -+ settle.expiresAt = now + SETTLE_TTL_MS; ++ settle.expiresAt = now2 + SETTLE_TTL_MS; + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} @@ -4614,10 +4671,8 @@ index 914d2da..f03ba1e 100644 + initialScrollCompletion.resetFlags(state); + setInitialScrollSession(state, { bootstrap: null }); + finishInitialScroll(ctx); - } - } --function scrollTo(ctx, params) { -- var _a3, _b; ++ } ++} +function finishInitialScroll(ctx, options) { + var _a3, _b, _c; const state = ctx.state; @@ -4719,7 +4774,7 @@ index 914d2da..f03ba1e 100644 } // src/core/scrollToIndex.ts -@@ -4350,6 +4488,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4350,6 +4495,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -4727,7 +4782,7 @@ index 914d2da..f03ba1e 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4367,8 +4506,17 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4367,8 +4513,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -4736,18 +4791,19 @@ index 914d2da..f03ba1e 100644 + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); + const mvcp = state.props.maintainVisibleContentPosition; -+ if (dataChanged && !mvcp.data && !mvcp.size) { -+ clearScrollTargetSettle(state); -+ } ++ const isUnanchoredDataChange = dataChanged && !mvcp.data; + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { -+ settleScrollTarget(ctx, { isCompensating, minIndexSizeChanged: minIndexSizeChangedThisPass }); ++ settleScrollTarget(ctx, { ++ isCompensating, ++ minIndexSizeChanged: isUnanchoredDataChange ? 0 : minIndexSizeChangedThisPass ++ }); + } + if (didMVCPAdjust) { updateScroll2(state.scroll); updateScrollRange(); } -@@ -5927,6 +6075,21 @@ function getDocumentScrollerNode() { +@@ -5927,6 +6083,21 @@ function getDocumentScrollerNode() { } return document.scrollingElement || document.documentElement || document.body; } @@ -4769,13 +4825,14 @@ index 914d2da..f03ba1e 100644 function getWindowScrollPosition() { var _a3, _b, _c, _d; if (typeof window === "undefined") { -@@ -6003,9 +6166,175 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -6003,9 +6174,213 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll }; } +// src/components/webTemporaryEndPadding.ts +var entriesByNode = /* @__PURE__ */ new WeakMap(); +var nextRequestId = 1; ++var RELEASE_ALL_MAX_PASSES = 5; +function readResolvedPadding(node, prop) { + return Number.parseFloat(window.getComputedStyle(node)[prop]) || 0; +} @@ -4886,22 +4943,30 @@ index 914d2da..f03ba1e 100644 + if (!entries) { + return; + } -+ for (const prop of Object.keys(entries)) { -+ const entry = entries[prop]; -+ if (!entry) { -+ continue; ++ for (let pass = 0; pass < RELEASE_ALL_MAX_PASSES; pass++) { ++ const props = Object.keys(entries); ++ if (props.length === 0) { ++ break; + } -+ if (isOwnedByUs(node, prop, entry)) { -+ node.style[prop] = entry.baseline; ++ for (const prop of props) { ++ const entry = entries[prop]; ++ if (!entry) { ++ continue; ++ } ++ if (isOwnedByUs(node, prop, entry)) { ++ node.style[prop] = entry.baseline; ++ } ++ delete entries[prop]; ++ entry.requests.clear(); ++ drainPendingReleases(entry); + } -+ delete entries[prop]; -+ entry.requests.clear(); -+ drainPendingReleases(entry); + } ++ entriesByNode.delete(node); +} + // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; ++var SCROLLER_MARKER_ATTRIBUTE = "data-legend-list-scroller"; var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; +var SMOOTH_SCROLL_MAX_MS = 2e3; @@ -4921,20 +4986,49 @@ index 914d2da..f03ba1e 100644 + "PageDown", + "PageUp" +]); ++var NON_SCROLLING_KEY_TARGETS = "input,textarea,select,button,[contenteditable],[role=textbox],[role=button]"; +function isTextEntryTarget(target) { + var _a3; + const element = target; -+ const tag = (_a3 = element == null ? void 0 : element.tagName) == null ? void 0 : _a3.toLowerCase(); -+ return tag === "input" || tag === "textarea" || tag === "select" || !!(element == null ? void 0 : element.isContentEditable); ++ if (!element) { ++ return false; ++ } ++ if (element.isContentEditable) { ++ return true; ++ } ++ return !!((_a3 = element.closest) == null ? void 0 : _a3.call(element, NON_SCROLLING_KEY_TARGETS)); +} -+function pointerPosition(event) { -+ var _a3; -+ const touch = (_a3 = event.touches) == null ? void 0 : _a3[0]; -+ if (touch) { -+ return { x: touch.clientX, y: touch.clientY }; ++function pointerPosition(event, id) { ++ const touches = event.touches; ++ if (touches == null ? void 0 : touches.length) { ++ const touch = id === void 0 ? touches[0] : [...touches].find((t) => t.identifier === id); ++ return touch ? { id: touch.identifier, x: touch.clientX, y: touch.clientY } : void 0; + } + const pointer = event; -+ return typeof pointer.clientX === "number" ? { x: pointer.clientX, y: pointer.clientY } : void 0; ++ if (typeof pointer.clientX !== "number") { ++ return void 0; ++ } ++ if (id !== void 0 && pointer.pointerId !== void 0 && pointer.pointerId !== id) { ++ return void 0; ++ } ++ return { id: pointer.pointerId, x: pointer.clientX, y: pointer.clientY }; ++} ++var POINTER_EVENT_HANDLERS = (onDown, onMove, onUp) => [ ++ ["pointerdown", onDown], ++ ["pointermove", onMove], ++ ["pointerup", onUp], ++ ["pointercancel", onUp], ++ ["touchstart", onDown], ++ ["touchmove", onMove], ++ ["touchend", onUp], ++ ["touchcancel", onUp] ++]; ++function now() { ++ return typeof performance !== "undefined" ? performance.now() : 0; ++} ++function isHover(event) { ++ const pointer = event; ++ return pointer.pointerType === "mouse" && pointer.buttons === 0; +} +function isScrollKey(event) { + if (event.altKey || event.ctrlKey || event.metaKey) { @@ -4945,7 +5039,7 @@ index 914d2da..f03ba1e 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6074,6 +6403,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6074,6 +6449,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -4961,7 +5055,7 @@ index 914d2da..f03ba1e 100644 const getMaxScrollOffset = React3.useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6094,8 +6432,153 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,8 +6478,180 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -4972,12 +5066,13 @@ index 914d2da..f03ba1e 100644 + const animatedPaddingReleaseRef = React3.useRef(void 0); + const withReachableExtent = React3.useCallback( + (offset, maxOffset, animated, run) => { -+ var _a4; ++ var _a4, _b2, _c2; + const contentNode = contentRef.current; + (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); + const committedMaxOffset = getCommittedMaxScrollOffset(getMaxScrollOffset()); ++ const modelMaxOffset = Math.max(0, ((_b2 = ctx.state.totalSize) != null ? _b2 : 0) - ((_c2 = ctx.state.scrollLength) != null ? _c2 : 0)); + const viewportExtent = getViewportExtent(); -+ const reachLimit = viewportExtent > 0 ? committedMaxOffset + MAX_BORROW_VIEWPORTS * viewportExtent : Number.POSITIVE_INFINITY; ++ const reachLimit = modelMaxOffset > 0 || viewportExtent > 0 ? Math.max(modelMaxOffset, committedMaxOffset + MAX_BORROW_VIEWPORTS * viewportExtent) + SCROLL_EXTENT_EPSILON : Number.POSITIVE_INFINITY; + if (!contentNode || !Number.isFinite(offset) || offset > reachLimit) { + run(clampOffset(offset, maxOffset)); + return; @@ -4994,6 +5089,10 @@ index 914d2da..f03ba1e 100644 + } + releases.push(addTemporaryEndPadding(contentNode, paddingEndProp, shortfall + SCROLL_EXTENT_EPSILON)); + } ++ if (releases.length === 0) { ++ run(offset); ++ return; ++ } + const borrowId = ++borrowIdRef.current; + const release = () => { + for (const releaseOne of releases) { @@ -5026,13 +5125,12 @@ index 914d2da..f03ba1e 100644 + finish(); + } + }; -+ let framesUntilCheck = 0; ++ let framesSeen = 0; + const releaseWhenContentCommits = () => { + if (animatedPaddingReleaseRef.current !== finish) { + return; + } -+ if (framesUntilCheck-- <= 0) { -+ framesUntilCheck = BORROW_WATCH_FRAME_INTERVAL; ++ if (++framesSeen % BORROW_WATCH_FRAME_INTERVAL === 0) { + if (getCommittedMaxScrollOffset(getMaxScrollOffset()) + SCROLL_EXTENT_EPSILON >= offset) { + finish(); + return; @@ -5071,28 +5169,51 @@ index 914d2da..f03ba1e 100644 + ); + const interactionArmedAtRef = React3.useRef(0); + const dragOriginRef = React3.useRef(void 0); ++ const ownsEvent = React3.useCallback((event) => { ++ const scroller = scrollRef.current; ++ const target = event.target; ++ if (!scroller || !(target == null ? void 0 : target.closest)) { ++ return true; ++ } ++ return target.closest(`[${SCROLLER_MARKER_ATTRIBUTE}]`) === scroller; ++ }, []); + const reportUserInteraction = React3.useCallback(() => { + dragOriginRef.current = void 0; + releaseScrollTargetForUserInteraction(ctx.state); + }, [ctx]); + const onWheel = React3.useCallback( + (event) => { -+ if (event.timeStamp - interactionArmedAtRef.current < WHEEL_MOMENTUM_GRACE_MS) { ++ if (!ownsEvent(event)) { ++ return; ++ } ++ if (now() - interactionArmedAtRef.current < WHEEL_MOMENTUM_GRACE_MS) { + return; + } + reportUserInteraction(); + }, -+ [reportUserInteraction] ++ [ownsEvent, reportUserInteraction] ++ ); ++ const onPointerDown = React3.useCallback( ++ (event) => { ++ dragOriginRef.current = ownsEvent(event) ? pointerPosition(event, void 0) : void 0; ++ }, ++ [ownsEvent] + ); -+ const onPointerDown = React3.useCallback((event) => { -+ const point = pointerPosition(event); -+ dragOriginRef.current = point; ++ const onPointerUp = React3.useCallback(() => { ++ dragOriginRef.current = void 0; + }, []); + const onPointerMove = React3.useCallback( + (event) => { + const origin = dragOriginRef.current; -+ const point = origin && pointerPosition(event); -+ if (!origin || !point) { ++ if (!origin) { ++ return; ++ } ++ if (isHover(event)) { ++ dragOriginRef.current = void 0; ++ return; ++ } ++ const point = pointerPosition(event, origin.id); ++ if (!point) { + return; + } + if (Math.abs(point.x - origin.x) > USER_DRAG_SLOP || Math.abs(point.y - origin.y) > USER_DRAG_SLOP) { @@ -5111,11 +5232,11 @@ index 914d2da..f03ba1e 100644 + ); const scrollToLocalOffset = React3.useCallback( (offset, animated) => { -+ interactionArmedAtRef.current = typeof performance !== "undefined" ? performance.now() : 0; ++ interactionArmedAtRef.current = now(); const scrollElement = scrollRef.current; const target = getScrollTarget(); if (!target || typeof target.scrollTo !== "function") { -@@ -6116,14 +6599,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6116,14 +6672,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); options.left = left; options.top = top; @@ -5142,7 +5263,7 @@ index 914d2da..f03ba1e 100644 ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6639,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6149,8 +6712,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -5152,7 +5273,7 @@ index 914d2da..f03ba1e 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6163,7 +6652,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6163,7 +6725,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView if (!onScroll2 || !scrollRef.current) { return; } @@ -5169,34 +5290,37 @@ index 914d2da..f03ba1e 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6221,11 +6718,26 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6218,14 +6788,29 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] + ); + React3.useLayoutEffect(() => { ++ var _a4; const target = getScrollTarget(); if (!target) return; target.addEventListener("scroll", handleScroll, { passive: true }); + const listenerOptions = { capture: true, passive: true }; + const removeOptions = { capture: true }; -+ const interactionTarget = scrollRef.current; ++ const interactionTarget = (_a4 = scrollRef.current) != null ? _a4 : void 0; ++ const keyTarget = isWindowScroll ? target : interactionTarget; + target.addEventListener("wheel", onWheel, listenerOptions); -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener("pointerdown", onPointerDown, listenerOptions); -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener("pointermove", onPointerMove, listenerOptions); -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener("touchstart", onPointerDown, listenerOptions); -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener("touchmove", onPointerMove, listenerOptions); -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener("keydown", onKeyDown, listenerOptions); ++ keyTarget == null ? void 0 : keyTarget.addEventListener("keydown", onKeyDown, listenerOptions); ++ for (const [type, handler] of POINTER_EVENT_HANDLERS(onPointerDown, onPointerMove, onPointerUp)) { ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener(type, handler, listenerOptions); ++ } if ("onscrollend" in target) { target.addEventListener("scrollend", emitScrollEnd); } return () => { target.removeEventListener("scroll", handleScroll); + target.removeEventListener("wheel", onWheel, removeOptions); -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("pointerdown", onPointerDown, removeOptions); -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("pointermove", onPointerMove, removeOptions); -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("touchstart", onPointerDown, removeOptions); -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("touchmove", onPointerMove, removeOptions); -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("keydown", onKeyDown, removeOptions); ++ keyTarget == null ? void 0 : keyTarget.removeEventListener("keydown", onKeyDown, removeOptions); ++ for (const [type, handler] of POINTER_EVENT_HANDLERS(onPointerDown, onPointerMove, onPointerUp)) { ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener(type, handler, removeOptions); ++ } if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6236,7 +6748,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6236,7 +6821,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } scrollEventCoalescer.cancel(); }; @@ -5214,7 +5338,15 @@ index 914d2da..f03ba1e 100644 React3.useEffect(() => { const doScroll = () => { if (contentOffset) { -@@ -6379,21 +6900,6 @@ function useValueListener$(key, callback) { +@@ -6341,6 +6935,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + { + className: scrollViewClassName, + ref: scrollRef, ++ ...{ [SCROLLER_MARKER_ATTRIBUTE]: "" }, + ...webProps, + style: scrollViewStyle + }, +@@ -6379,21 +6974,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -5236,7 +5368,7 @@ index 914d2da..f03ba1e 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6411,8 +6917,6 @@ function ScrollAdjust() { +@@ -6411,8 +6991,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3__namespace.useRef(0); const lastScrollAdjustUserOffsetRef = React3__namespace.useRef(0); @@ -5245,7 +5377,7 @@ index 914d2da..f03ba1e 100644 const contentNodeRef = React3__namespace.useRef(null); const callback = React3__namespace.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6423,7 +6927,7 @@ function ScrollAdjust() { +@@ -6423,7 +7001,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -5254,7 +5386,7 @@ index 914d2da..f03ba1e 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6438,29 +6942,10 @@ function ScrollAdjust() { +@@ -6438,29 +7016,10 @@ function ScrollAdjust() { const nextScroll = currentScroll + scrollDelta; const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; if (needsTemporaryPadding) { @@ -5286,7 +5418,30 @@ index 914d2da..f03ba1e 100644 } else { scrollBy(); } -@@ -8288,6 +8773,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7645,10 +8204,10 @@ function useThrottleDebounce(mode) { + const execute = React3.useCallback( + (callback, delay, ...args) => { + { +- const now = Date.now(); ++ const now2 = Date.now(); + lastArgsRef.current = args; +- if (now - lastCallTimeRef.current >= delay) { +- lastCallTimeRef.current = now; ++ if (now2 - lastCallTimeRef.current >= delay) { ++ lastCallTimeRef.current = now2; + callback(...args); + clearTimeoutRef(); + } else { +@@ -7662,7 +8221,7 @@ function useThrottleDebounce(mode) { + lastArgsRef.current = null; + } + }, +- delay - (now - lastCallTimeRef.current) ++ delay - (now2 - lastCallTimeRef.current) + ); + } + } +@@ -8288,6 +8847,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -5295,7 +5450,7 @@ index 914d2da..f03ba1e 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs -index 95465f2..ea937e4 100644 +index 95465f2..0005501 100644 --- a/node_modules/@legendapp/list/react-native.web.mjs +++ b/node_modules/@legendapp/list/react-native.web.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -5904,25 +6059,15 @@ index 95465f2..ea937e4 100644 // src/core/getStartOffsetAdjustment.ts function getStartOffsetAdjustment(ctx) { const { state } = ctx; -@@ -1213,58 +614,273 @@ function getItemSizeAtIndex(ctx, index) { - if (index === void 0 || index < 0) { - return void 0; +@@ -1226,45 +627,260 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { + if (viewOffset) { + offset -= viewOffset; } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+ const targetId = getId(ctx.state, index); -+ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+} -+ -+// src/core/calculateOffsetWithOffsetPosition.ts -+function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -+ var _a3; -+ const state = ctx.state; -+ const { index, viewOffset, viewPosition } = params; -+ let offset = offsetParam; -+ if (viewOffset) { -+ offset -= viewOffset; -+ } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } + if (index !== void 0) { + const startOffsetAdjustment = getStartOffsetAdjustment(ctx); + if (startOffsetAdjustment) { @@ -6132,7 +6277,22 @@ index 95465f2..ea937e4 100644 + if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { + state.isStartReached = false; + state.startReachedSnapshot = void 0; -+ } + } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } + set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); + set$(ctx, "isNearStart", scroll <= threshold); + const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; @@ -6159,50 +6319,19 @@ index 95465f2..ea937e4 100644 + state.startReachedSnapshot = snapshot; + } + ); -+ } + } +- return offset; } --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; -- } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } -- } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } -- } -- return offset; +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); - } - --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { ++} ++ +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { + var _a3, _b; @@ -6416,6 +6545,68 @@ index 95465f2..ea937e4 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; +@@ -1504,7 +1296,7 @@ var MVCP_POSITION_EPSILON = 0.1; + var MVCP_ANCHOR_LOCK_TTL_MS = 300; + var MVCP_ANCHOR_LOCK_QUIET_PASSES_TO_RELEASE = 2; + var NATIVE_END_CLAMP_EPSILON = 1; +-function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { ++function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now2) { + if (!enableMVCPAnchorLock) { + state.mvcpAnchorLock = void 0; + return void 0; +@@ -1513,7 +1305,7 @@ function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { + if (!lock) { + return void 0; + } +- const isExpired = now > lock.expiresAt; ++ const isExpired = now2 > lock.expiresAt; + const isMissing = state.indexByKey.get(lock.id) === void 0; + if (isExpired || isMissing || !mvcpData) { + state.mvcpAnchorLock = void 0; +@@ -1523,7 +1315,7 @@ function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { + } + function updateAnchorLock(state, params) { + { +- const { anchorId, anchorPosition, dataChanged, now, positionDiff } = params; ++ const { anchorId, anchorPosition, dataChanged, now: now2, positionDiff } = params; + const enableMVCPAnchorLock = !!dataChanged || !!state.mvcpAnchorLock; + const mvcpData = state.props.maintainVisibleContentPosition.data; + if (!enableMVCPAnchorLock || !mvcpData || state.scrollingTo || !anchorId || anchorPosition === void 0) { +@@ -1536,7 +1328,7 @@ function updateAnchorLock(state, params) { + return; + } + state.mvcpAnchorLock = { +- expiresAt: now + MVCP_ANCHOR_LOCK_TTL_MS, ++ expiresAt: now2 + MVCP_ANCHOR_LOCK_TTL_MS, + id: anchorId, + position: anchorPosition, + quietPasses +@@ -1641,14 +1433,14 @@ function prepareMVCP(ctx, dataChanged) { + const { + maintainVisibleContentPosition: { data: mvcpData, size: mvcpScroll, shouldRestorePosition } + } = props; +- const now = Date.now(); ++ const now2 = Date.now(); + const enableMVCPAnchorLock = (!!dataChanged || !!state.mvcpAnchorLock); + const scrollingTo = state.scrollingTo; + if (dataChanged && state.pendingScrollToEnd && scrollingTo === void 0) { + state.mvcpAnchorLock = void 0; + return void 0; + } +- const anchorLock = resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) ; ++ const anchorLock = resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now2) ; + let prevPosition; + let targetId; + const idsInViewWithPositions = []; +@@ -1765,7 +1557,7 @@ function prepareMVCP(ctx, dataChanged) { + anchorId: anchorIdForLock, + anchorPosition: anchorPositionForLock, + dataChanged, +- now, ++ now: now2, + positionDiff + }); + if (shouldQueueNativeMVCPAdjust()) { @@ -1788,6 +1580,27 @@ function prepareMVCP(ctx, dataChanged) { } } @@ -6444,34 +6635,12 @@ index 95465f2..ea937e4 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2034,70 +1847,395 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { - if (nextTop === void 0 || nextTop > viewportEnd) { - break; - } -- end++; -- } -- return { end, start }; --} --function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -- if (range) { -- ctx.state.scrollTargetPinnedRange = range; -- ctx.state.scrollForNextCalculateItemsInView = void 0; -- } else { -- ctx.state.scrollTargetPinnedRange = void 0; -+ end++; -+ } -+ return { end, start }; -+} -+function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -+ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -+ if (range) { -+ ctx.state.scrollTargetPinnedRange = range; -+ ctx.state.scrollForNextCalculateItemsInView = void 0; -+ } else { -+ ctx.state.scrollTargetPinnedRange = void 0; -+ } -+} +@@ -2047,57 +1860,389 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + ctx.state.scrollTargetPinnedRange = void 0; + } + } +-function scrollTo(ctx, params) { +- var _a3, _b; +function scrollTo(ctx, params) { + var _a3, _b, _c; + const state = ctx.state; @@ -6541,9 +6710,7 @@ index 95465f2..ea937e4 100644 +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; +function releaseScrollTargetForUserInteraction(state) { -+ if (state.scrollTargetSettle) { -+ clearScrollTargetSettle(state); -+ } ++ clearScrollTargetSettle(state); +} +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; @@ -6560,11 +6727,11 @@ index 95465f2..ea937e4 100644 + return; + } + clearScrollTargetSettle(state); -+ const now = Date.now(); ++ const now2 = Date.now(); + state.scrollTargetSettle = { + corrections: 0, -+ deadline: now + SETTLE_MAX_MS, -+ expiresAt: now + SETTLE_TTL_MS, ++ deadline: now2 + SETTLE_MAX_MS, ++ expiresAt: now2 + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, + quietPasses: 0, @@ -6608,6 +6775,12 @@ index 95465f2..ea937e4 100644 + viewOffset: settle.viewOffset, + viewPosition: settle.viewPosition + }); ++ const scrollingTo = state.scrollingTo; ++ if (scrollingTo && scrollingTo.index === index) { ++ const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); ++ scrollingTo.targetOffset = correctedOffset; ++ scrollingTo.offset = position; ++ } + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +} +function settleScrollTarget(ctx, options) { @@ -6620,7 +6793,10 @@ index 95465f2..ea937e4 100644 + const measured = options == null ? void 0 : options.minIndexSizeChanged; + if (measured !== void 0) { + settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); -+ settle.expiresAt = Date.now() + SETTLE_TTL_MS; ++ const targetIndex = state.indexByKey.get(settle.id); ++ if (targetIndex !== void 0 && measured <= targetIndex) { ++ settle.expiresAt = Date.now() + SETTLE_TTL_MS; ++ } + } + if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { @@ -6637,8 +6813,8 @@ index 95465f2..ea937e4 100644 + clearScrollTargetSettle(state); + return false; + } -+ const now = Date.now(); -+ if (now > settle.expiresAt || now > settle.deadline) { ++ const now2 = Date.now(); ++ if (now2 > settle.expiresAt || now2 > settle.deadline) { + clearScrollTargetSettle(state); + return false; + } @@ -6656,7 +6832,7 @@ index 95465f2..ea937e4 100644 + return false; + } + settle.quietPasses = 0; -+ settle.expiresAt = now + SETTLE_TTL_MS; ++ settle.expiresAt = now2 + SETTLE_TTL_MS; + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} @@ -6790,10 +6966,8 @@ index 95465f2..ea937e4 100644 + initialScrollCompletion.resetFlags(state); + setInitialScrollSession(state, { bootstrap: null }); + finishInitialScroll(ctx); - } - } --function scrollTo(ctx, params) { -- var _a3, _b; ++ } ++} +function finishInitialScroll(ctx, options) { + var _a3, _b, _c; const state = ctx.state; @@ -6895,7 +7069,7 @@ index 95465f2..ea937e4 100644 } // src/core/scrollToIndex.ts -@@ -4329,6 +4467,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4329,6 +4474,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -6903,7 +7077,7 @@ index 95465f2..ea937e4 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4346,8 +4485,17 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4346,8 +4492,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -6912,18 +7086,19 @@ index 95465f2..ea937e4 100644 + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); + const mvcp = state.props.maintainVisibleContentPosition; -+ if (dataChanged && !mvcp.data && !mvcp.size) { -+ clearScrollTargetSettle(state); -+ } ++ const isUnanchoredDataChange = dataChanged && !mvcp.data; + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { -+ settleScrollTarget(ctx, { isCompensating, minIndexSizeChanged: minIndexSizeChangedThisPass }); ++ settleScrollTarget(ctx, { ++ isCompensating, ++ minIndexSizeChanged: isUnanchoredDataChange ? 0 : minIndexSizeChangedThisPass ++ }); + } + if (didMVCPAdjust) { updateScroll2(state.scroll); updateScrollRange(); } -@@ -5906,6 +6054,21 @@ function getDocumentScrollerNode() { +@@ -5906,6 +6062,21 @@ function getDocumentScrollerNode() { } return document.scrollingElement || document.documentElement || document.body; } @@ -6945,13 +7120,14 @@ index 95465f2..ea937e4 100644 function getWindowScrollPosition() { var _a3, _b, _c, _d; if (typeof window === "undefined") { -@@ -5982,9 +6145,175 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -5982,9 +6153,213 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll }; } +// src/components/webTemporaryEndPadding.ts +var entriesByNode = /* @__PURE__ */ new WeakMap(); +var nextRequestId = 1; ++var RELEASE_ALL_MAX_PASSES = 5; +function readResolvedPadding(node, prop) { + return Number.parseFloat(window.getComputedStyle(node)[prop]) || 0; +} @@ -7062,22 +7238,30 @@ index 95465f2..ea937e4 100644 + if (!entries) { + return; + } -+ for (const prop of Object.keys(entries)) { -+ const entry = entries[prop]; -+ if (!entry) { -+ continue; ++ for (let pass = 0; pass < RELEASE_ALL_MAX_PASSES; pass++) { ++ const props = Object.keys(entries); ++ if (props.length === 0) { ++ break; + } -+ if (isOwnedByUs(node, prop, entry)) { -+ node.style[prop] = entry.baseline; ++ for (const prop of props) { ++ const entry = entries[prop]; ++ if (!entry) { ++ continue; ++ } ++ if (isOwnedByUs(node, prop, entry)) { ++ node.style[prop] = entry.baseline; ++ } ++ delete entries[prop]; ++ entry.requests.clear(); ++ drainPendingReleases(entry); + } -+ delete entries[prop]; -+ entry.requests.clear(); -+ drainPendingReleases(entry); + } ++ entriesByNode.delete(node); +} + // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; ++var SCROLLER_MARKER_ATTRIBUTE = "data-legend-list-scroller"; var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; +var SMOOTH_SCROLL_MAX_MS = 2e3; @@ -7097,20 +7281,49 @@ index 95465f2..ea937e4 100644 + "PageDown", + "PageUp" +]); ++var NON_SCROLLING_KEY_TARGETS = "input,textarea,select,button,[contenteditable],[role=textbox],[role=button]"; +function isTextEntryTarget(target) { + var _a3; + const element = target; -+ const tag = (_a3 = element == null ? void 0 : element.tagName) == null ? void 0 : _a3.toLowerCase(); -+ return tag === "input" || tag === "textarea" || tag === "select" || !!(element == null ? void 0 : element.isContentEditable); ++ if (!element) { ++ return false; ++ } ++ if (element.isContentEditable) { ++ return true; ++ } ++ return !!((_a3 = element.closest) == null ? void 0 : _a3.call(element, NON_SCROLLING_KEY_TARGETS)); +} -+function pointerPosition(event) { -+ var _a3; -+ const touch = (_a3 = event.touches) == null ? void 0 : _a3[0]; -+ if (touch) { -+ return { x: touch.clientX, y: touch.clientY }; ++function pointerPosition(event, id) { ++ const touches = event.touches; ++ if (touches == null ? void 0 : touches.length) { ++ const touch = id === void 0 ? touches[0] : [...touches].find((t) => t.identifier === id); ++ return touch ? { id: touch.identifier, x: touch.clientX, y: touch.clientY } : void 0; + } + const pointer = event; -+ return typeof pointer.clientX === "number" ? { x: pointer.clientX, y: pointer.clientY } : void 0; ++ if (typeof pointer.clientX !== "number") { ++ return void 0; ++ } ++ if (id !== void 0 && pointer.pointerId !== void 0 && pointer.pointerId !== id) { ++ return void 0; ++ } ++ return { id: pointer.pointerId, x: pointer.clientX, y: pointer.clientY }; ++} ++var POINTER_EVENT_HANDLERS = (onDown, onMove, onUp) => [ ++ ["pointerdown", onDown], ++ ["pointermove", onMove], ++ ["pointerup", onUp], ++ ["pointercancel", onUp], ++ ["touchstart", onDown], ++ ["touchmove", onMove], ++ ["touchend", onUp], ++ ["touchcancel", onUp] ++]; ++function now() { ++ return typeof performance !== "undefined" ? performance.now() : 0; ++} ++function isHover(event) { ++ const pointer = event; ++ return pointer.pointerType === "mouse" && pointer.buttons === 0; +} +function isScrollKey(event) { + if (event.altKey || event.ctrlKey || event.metaKey) { @@ -7121,7 +7334,7 @@ index 95465f2..ea937e4 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6053,6 +6382,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6053,6 +6428,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -7137,7 +7350,7 @@ index 95465f2..ea937e4 100644 const getMaxScrollOffset = useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6073,8 +6411,153 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,8 +6457,180 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -7148,12 +7361,13 @@ index 95465f2..ea937e4 100644 + const animatedPaddingReleaseRef = useRef(void 0); + const withReachableExtent = useCallback( + (offset, maxOffset, animated, run) => { -+ var _a4; ++ var _a4, _b2, _c2; + const contentNode = contentRef.current; + (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); + const committedMaxOffset = getCommittedMaxScrollOffset(getMaxScrollOffset()); ++ const modelMaxOffset = Math.max(0, ((_b2 = ctx.state.totalSize) != null ? _b2 : 0) - ((_c2 = ctx.state.scrollLength) != null ? _c2 : 0)); + const viewportExtent = getViewportExtent(); -+ const reachLimit = viewportExtent > 0 ? committedMaxOffset + MAX_BORROW_VIEWPORTS * viewportExtent : Number.POSITIVE_INFINITY; ++ const reachLimit = modelMaxOffset > 0 || viewportExtent > 0 ? Math.max(modelMaxOffset, committedMaxOffset + MAX_BORROW_VIEWPORTS * viewportExtent) + SCROLL_EXTENT_EPSILON : Number.POSITIVE_INFINITY; + if (!contentNode || !Number.isFinite(offset) || offset > reachLimit) { + run(clampOffset(offset, maxOffset)); + return; @@ -7170,6 +7384,10 @@ index 95465f2..ea937e4 100644 + } + releases.push(addTemporaryEndPadding(contentNode, paddingEndProp, shortfall + SCROLL_EXTENT_EPSILON)); + } ++ if (releases.length === 0) { ++ run(offset); ++ return; ++ } + const borrowId = ++borrowIdRef.current; + const release = () => { + for (const releaseOne of releases) { @@ -7202,13 +7420,12 @@ index 95465f2..ea937e4 100644 + finish(); + } + }; -+ let framesUntilCheck = 0; ++ let framesSeen = 0; + const releaseWhenContentCommits = () => { + if (animatedPaddingReleaseRef.current !== finish) { + return; + } -+ if (framesUntilCheck-- <= 0) { -+ framesUntilCheck = BORROW_WATCH_FRAME_INTERVAL; ++ if (++framesSeen % BORROW_WATCH_FRAME_INTERVAL === 0) { + if (getCommittedMaxScrollOffset(getMaxScrollOffset()) + SCROLL_EXTENT_EPSILON >= offset) { + finish(); + return; @@ -7247,28 +7464,51 @@ index 95465f2..ea937e4 100644 + ); + const interactionArmedAtRef = useRef(0); + const dragOriginRef = useRef(void 0); ++ const ownsEvent = useCallback((event) => { ++ const scroller = scrollRef.current; ++ const target = event.target; ++ if (!scroller || !(target == null ? void 0 : target.closest)) { ++ return true; ++ } ++ return target.closest(`[${SCROLLER_MARKER_ATTRIBUTE}]`) === scroller; ++ }, []); + const reportUserInteraction = useCallback(() => { + dragOriginRef.current = void 0; + releaseScrollTargetForUserInteraction(ctx.state); + }, [ctx]); + const onWheel = useCallback( + (event) => { -+ if (event.timeStamp - interactionArmedAtRef.current < WHEEL_MOMENTUM_GRACE_MS) { ++ if (!ownsEvent(event)) { ++ return; ++ } ++ if (now() - interactionArmedAtRef.current < WHEEL_MOMENTUM_GRACE_MS) { + return; + } + reportUserInteraction(); + }, -+ [reportUserInteraction] ++ [ownsEvent, reportUserInteraction] ++ ); ++ const onPointerDown = useCallback( ++ (event) => { ++ dragOriginRef.current = ownsEvent(event) ? pointerPosition(event, void 0) : void 0; ++ }, ++ [ownsEvent] + ); -+ const onPointerDown = useCallback((event) => { -+ const point = pointerPosition(event); -+ dragOriginRef.current = point; ++ const onPointerUp = useCallback(() => { ++ dragOriginRef.current = void 0; + }, []); + const onPointerMove = useCallback( + (event) => { + const origin = dragOriginRef.current; -+ const point = origin && pointerPosition(event); -+ if (!origin || !point) { ++ if (!origin) { ++ return; ++ } ++ if (isHover(event)) { ++ dragOriginRef.current = void 0; ++ return; ++ } ++ const point = pointerPosition(event, origin.id); ++ if (!point) { + return; + } + if (Math.abs(point.x - origin.x) > USER_DRAG_SLOP || Math.abs(point.y - origin.y) > USER_DRAG_SLOP) { @@ -7287,11 +7527,11 @@ index 95465f2..ea937e4 100644 + ); const scrollToLocalOffset = useCallback( (offset, animated) => { -+ interactionArmedAtRef.current = typeof performance !== "undefined" ? performance.now() : 0; ++ interactionArmedAtRef.current = now(); const scrollElement = scrollRef.current; const target = getScrollTarget(); if (!target || typeof target.scrollTo !== "function") { -@@ -6095,14 +6578,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6095,14 +6651,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); options.left = left; options.top = top; @@ -7318,7 +7558,7 @@ index 95465f2..ea937e4 100644 ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6618,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6128,8 +6691,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -7328,7 +7568,7 @@ index 95465f2..ea937e4 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6142,7 +6631,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6142,7 +6704,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ if (!onScroll2 || !scrollRef.current) { return; } @@ -7345,34 +7585,37 @@ index 95465f2..ea937e4 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6200,11 +6697,26 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6197,14 +6767,29 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] + ); + useLayoutEffect(() => { ++ var _a4; const target = getScrollTarget(); if (!target) return; target.addEventListener("scroll", handleScroll, { passive: true }); + const listenerOptions = { capture: true, passive: true }; + const removeOptions = { capture: true }; -+ const interactionTarget = scrollRef.current; ++ const interactionTarget = (_a4 = scrollRef.current) != null ? _a4 : void 0; ++ const keyTarget = isWindowScroll ? target : interactionTarget; + target.addEventListener("wheel", onWheel, listenerOptions); -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener("pointerdown", onPointerDown, listenerOptions); -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener("pointermove", onPointerMove, listenerOptions); -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener("touchstart", onPointerDown, listenerOptions); -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener("touchmove", onPointerMove, listenerOptions); -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener("keydown", onKeyDown, listenerOptions); ++ keyTarget == null ? void 0 : keyTarget.addEventListener("keydown", onKeyDown, listenerOptions); ++ for (const [type, handler] of POINTER_EVENT_HANDLERS(onPointerDown, onPointerMove, onPointerUp)) { ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener(type, handler, listenerOptions); ++ } if ("onscrollend" in target) { target.addEventListener("scrollend", emitScrollEnd); } return () => { target.removeEventListener("scroll", handleScroll); + target.removeEventListener("wheel", onWheel, removeOptions); -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("pointerdown", onPointerDown, removeOptions); -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("pointermove", onPointerMove, removeOptions); -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("touchstart", onPointerDown, removeOptions); -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("touchmove", onPointerMove, removeOptions); -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("keydown", onKeyDown, removeOptions); ++ keyTarget == null ? void 0 : keyTarget.removeEventListener("keydown", onKeyDown, removeOptions); ++ for (const [type, handler] of POINTER_EVENT_HANDLERS(onPointerDown, onPointerMove, onPointerUp)) { ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener(type, handler, removeOptions); ++ } if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6215,7 +6727,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6215,7 +6800,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } scrollEventCoalescer.cancel(); }; @@ -7390,7 +7633,15 @@ index 95465f2..ea937e4 100644 useEffect(() => { const doScroll = () => { if (contentOffset) { -@@ -6358,21 +6879,6 @@ function useValueListener$(key, callback) { +@@ -6320,6 +6914,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + { + className: scrollViewClassName, + ref: scrollRef, ++ ...{ [SCROLLER_MARKER_ATTRIBUTE]: "" }, + ...webProps, + style: scrollViewStyle + }, +@@ -6358,21 +6953,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -7412,7 +7663,7 @@ index 95465f2..ea937e4 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6390,8 +6896,6 @@ function ScrollAdjust() { +@@ -6390,8 +6970,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3.useRef(0); const lastScrollAdjustUserOffsetRef = React3.useRef(0); @@ -7421,7 +7672,7 @@ index 95465f2..ea937e4 100644 const contentNodeRef = React3.useRef(null); const callback = React3.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6402,7 +6906,7 @@ function ScrollAdjust() { +@@ -6402,7 +6980,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -7430,7 +7681,7 @@ index 95465f2..ea937e4 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6417,29 +6921,10 @@ function ScrollAdjust() { +@@ -6417,29 +6995,10 @@ function ScrollAdjust() { const nextScroll = currentScroll + scrollDelta; const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; if (needsTemporaryPadding) { @@ -7462,7 +7713,30 @@ index 95465f2..ea937e4 100644 } else { scrollBy(); } -@@ -8267,6 +8752,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7624,10 +8183,10 @@ function useThrottleDebounce(mode) { + const execute = useCallback( + (callback, delay, ...args) => { + { +- const now = Date.now(); ++ const now2 = Date.now(); + lastArgsRef.current = args; +- if (now - lastCallTimeRef.current >= delay) { +- lastCallTimeRef.current = now; ++ if (now2 - lastCallTimeRef.current >= delay) { ++ lastCallTimeRef.current = now2; + callback(...args); + clearTimeoutRef(); + } else { +@@ -7641,7 +8200,7 @@ function useThrottleDebounce(mode) { + lastArgsRef.current = null; + } + }, +- delay - (now - lastCallTimeRef.current) ++ delay - (now2 - lastCallTimeRef.current) + ); + } + } +@@ -8267,6 +8826,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -7471,7 +7745,7 @@ index 95465f2..ea937e4 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js -index 914d2da..f03ba1e 100644 +index 914d2da..6240e13 100644 --- a/node_modules/@legendapp/list/react.js +++ b/node_modules/@legendapp/list/react.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -8080,25 +8354,15 @@ index 914d2da..f03ba1e 100644 // src/core/getStartOffsetAdjustment.ts function getStartOffsetAdjustment(ctx) { const { state } = ctx; -@@ -1234,58 +635,273 @@ function getItemSizeAtIndex(ctx, index) { - if (index === void 0 || index < 0) { - return void 0; +@@ -1247,45 +648,260 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { + if (viewOffset) { + offset -= viewOffset; } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+ const targetId = getId(ctx.state, index); -+ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+} -+ -+// src/core/calculateOffsetWithOffsetPosition.ts -+function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -+ var _a3; -+ const state = ctx.state; -+ const { index, viewOffset, viewPosition } = params; -+ let offset = offsetParam; -+ if (viewOffset) { -+ offset -= viewOffset; -+ } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } + if (index !== void 0) { + const startOffsetAdjustment = getStartOffsetAdjustment(ctx); + if (startOffsetAdjustment) { @@ -8308,7 +8572,22 @@ index 914d2da..f03ba1e 100644 + if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { + state.isStartReached = false; + state.startReachedSnapshot = void 0; -+ } + } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } + set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); + set$(ctx, "isNearStart", scroll <= threshold); + const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; @@ -8335,50 +8614,19 @@ index 914d2da..f03ba1e 100644 + state.startReachedSnapshot = snapshot; + } + ); -+ } + } +- return offset; } --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; -- } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } -- } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } -- } -- return offset; +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); - } - --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { ++} ++ +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { + var _a3, _b; @@ -8592,6 +8840,68 @@ index 914d2da..f03ba1e 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; +@@ -1525,7 +1317,7 @@ var MVCP_POSITION_EPSILON = 0.1; + var MVCP_ANCHOR_LOCK_TTL_MS = 300; + var MVCP_ANCHOR_LOCK_QUIET_PASSES_TO_RELEASE = 2; + var NATIVE_END_CLAMP_EPSILON = 1; +-function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { ++function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now2) { + if (!enableMVCPAnchorLock) { + state.mvcpAnchorLock = void 0; + return void 0; +@@ -1534,7 +1326,7 @@ function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { + if (!lock) { + return void 0; + } +- const isExpired = now > lock.expiresAt; ++ const isExpired = now2 > lock.expiresAt; + const isMissing = state.indexByKey.get(lock.id) === void 0; + if (isExpired || isMissing || !mvcpData) { + state.mvcpAnchorLock = void 0; +@@ -1544,7 +1336,7 @@ function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { + } + function updateAnchorLock(state, params) { + { +- const { anchorId, anchorPosition, dataChanged, now, positionDiff } = params; ++ const { anchorId, anchorPosition, dataChanged, now: now2, positionDiff } = params; + const enableMVCPAnchorLock = !!dataChanged || !!state.mvcpAnchorLock; + const mvcpData = state.props.maintainVisibleContentPosition.data; + if (!enableMVCPAnchorLock || !mvcpData || state.scrollingTo || !anchorId || anchorPosition === void 0) { +@@ -1557,7 +1349,7 @@ function updateAnchorLock(state, params) { + return; + } + state.mvcpAnchorLock = { +- expiresAt: now + MVCP_ANCHOR_LOCK_TTL_MS, ++ expiresAt: now2 + MVCP_ANCHOR_LOCK_TTL_MS, + id: anchorId, + position: anchorPosition, + quietPasses +@@ -1662,14 +1454,14 @@ function prepareMVCP(ctx, dataChanged) { + const { + maintainVisibleContentPosition: { data: mvcpData, size: mvcpScroll, shouldRestorePosition } + } = props; +- const now = Date.now(); ++ const now2 = Date.now(); + const enableMVCPAnchorLock = (!!dataChanged || !!state.mvcpAnchorLock); + const scrollingTo = state.scrollingTo; + if (dataChanged && state.pendingScrollToEnd && scrollingTo === void 0) { + state.mvcpAnchorLock = void 0; + return void 0; + } +- const anchorLock = resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) ; ++ const anchorLock = resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now2) ; + let prevPosition; + let targetId; + const idsInViewWithPositions = []; +@@ -1786,7 +1578,7 @@ function prepareMVCP(ctx, dataChanged) { + anchorId: anchorIdForLock, + anchorPosition: anchorPositionForLock, + dataChanged, +- now, ++ now: now2, + positionDiff + }); + if (shouldQueueNativeMVCPAdjust()) { @@ -1809,6 +1601,27 @@ function prepareMVCP(ctx, dataChanged) { } } @@ -8620,34 +8930,12 @@ index 914d2da..f03ba1e 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2055,70 +1868,395 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { - if (nextTop === void 0 || nextTop > viewportEnd) { - break; - } -- end++; -- } -- return { end, start }; --} --function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -- if (range) { -- ctx.state.scrollTargetPinnedRange = range; -- ctx.state.scrollForNextCalculateItemsInView = void 0; -- } else { -- ctx.state.scrollTargetPinnedRange = void 0; -+ end++; -+ } -+ return { end, start }; -+} -+function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -+ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -+ if (range) { -+ ctx.state.scrollTargetPinnedRange = range; -+ ctx.state.scrollForNextCalculateItemsInView = void 0; -+ } else { -+ ctx.state.scrollTargetPinnedRange = void 0; -+ } -+} +@@ -2068,57 +1881,389 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + ctx.state.scrollTargetPinnedRange = void 0; + } + } +-function scrollTo(ctx, params) { +- var _a3, _b; +function scrollTo(ctx, params) { + var _a3, _b, _c; + const state = ctx.state; @@ -8717,9 +9005,7 @@ index 914d2da..f03ba1e 100644 +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; +function releaseScrollTargetForUserInteraction(state) { -+ if (state.scrollTargetSettle) { -+ clearScrollTargetSettle(state); -+ } ++ clearScrollTargetSettle(state); +} +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; @@ -8736,11 +9022,11 @@ index 914d2da..f03ba1e 100644 + return; + } + clearScrollTargetSettle(state); -+ const now = Date.now(); ++ const now2 = Date.now(); + state.scrollTargetSettle = { + corrections: 0, -+ deadline: now + SETTLE_MAX_MS, -+ expiresAt: now + SETTLE_TTL_MS, ++ deadline: now2 + SETTLE_MAX_MS, ++ expiresAt: now2 + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, + quietPasses: 0, @@ -8784,6 +9070,12 @@ index 914d2da..f03ba1e 100644 + viewOffset: settle.viewOffset, + viewPosition: settle.viewPosition + }); ++ const scrollingTo = state.scrollingTo; ++ if (scrollingTo && scrollingTo.index === index) { ++ const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); ++ scrollingTo.targetOffset = correctedOffset; ++ scrollingTo.offset = position; ++ } + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +} +function settleScrollTarget(ctx, options) { @@ -8796,7 +9088,10 @@ index 914d2da..f03ba1e 100644 + const measured = options == null ? void 0 : options.minIndexSizeChanged; + if (measured !== void 0) { + settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); -+ settle.expiresAt = Date.now() + SETTLE_TTL_MS; ++ const targetIndex = state.indexByKey.get(settle.id); ++ if (targetIndex !== void 0 && measured <= targetIndex) { ++ settle.expiresAt = Date.now() + SETTLE_TTL_MS; ++ } + } + if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { @@ -8813,8 +9108,8 @@ index 914d2da..f03ba1e 100644 + clearScrollTargetSettle(state); + return false; + } -+ const now = Date.now(); -+ if (now > settle.expiresAt || now > settle.deadline) { ++ const now2 = Date.now(); ++ if (now2 > settle.expiresAt || now2 > settle.deadline) { + clearScrollTargetSettle(state); + return false; + } @@ -8832,7 +9127,7 @@ index 914d2da..f03ba1e 100644 + return false; + } + settle.quietPasses = 0; -+ settle.expiresAt = now + SETTLE_TTL_MS; ++ settle.expiresAt = now2 + SETTLE_TTL_MS; + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} @@ -8966,10 +9261,8 @@ index 914d2da..f03ba1e 100644 + initialScrollCompletion.resetFlags(state); + setInitialScrollSession(state, { bootstrap: null }); + finishInitialScroll(ctx); - } - } --function scrollTo(ctx, params) { -- var _a3, _b; ++ } ++} +function finishInitialScroll(ctx, options) { + var _a3, _b, _c; const state = ctx.state; @@ -9071,7 +9364,7 @@ index 914d2da..f03ba1e 100644 } // src/core/scrollToIndex.ts -@@ -4350,6 +4488,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4350,6 +4495,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -9079,7 +9372,7 @@ index 914d2da..f03ba1e 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4367,8 +4506,17 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4367,8 +4513,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -9088,18 +9381,19 @@ index 914d2da..f03ba1e 100644 + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); + const mvcp = state.props.maintainVisibleContentPosition; -+ if (dataChanged && !mvcp.data && !mvcp.size) { -+ clearScrollTargetSettle(state); -+ } ++ const isUnanchoredDataChange = dataChanged && !mvcp.data; + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { -+ settleScrollTarget(ctx, { isCompensating, minIndexSizeChanged: minIndexSizeChangedThisPass }); ++ settleScrollTarget(ctx, { ++ isCompensating, ++ minIndexSizeChanged: isUnanchoredDataChange ? 0 : minIndexSizeChangedThisPass ++ }); + } + if (didMVCPAdjust) { updateScroll2(state.scroll); updateScrollRange(); } -@@ -5927,6 +6075,21 @@ function getDocumentScrollerNode() { +@@ -5927,6 +6083,21 @@ function getDocumentScrollerNode() { } return document.scrollingElement || document.documentElement || document.body; } @@ -9121,13 +9415,14 @@ index 914d2da..f03ba1e 100644 function getWindowScrollPosition() { var _a3, _b, _c, _d; if (typeof window === "undefined") { -@@ -6003,9 +6166,175 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -6003,9 +6174,213 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll }; } +// src/components/webTemporaryEndPadding.ts +var entriesByNode = /* @__PURE__ */ new WeakMap(); +var nextRequestId = 1; ++var RELEASE_ALL_MAX_PASSES = 5; +function readResolvedPadding(node, prop) { + return Number.parseFloat(window.getComputedStyle(node)[prop]) || 0; +} @@ -9238,22 +9533,30 @@ index 914d2da..f03ba1e 100644 + if (!entries) { + return; + } -+ for (const prop of Object.keys(entries)) { -+ const entry = entries[prop]; -+ if (!entry) { -+ continue; ++ for (let pass = 0; pass < RELEASE_ALL_MAX_PASSES; pass++) { ++ const props = Object.keys(entries); ++ if (props.length === 0) { ++ break; + } -+ if (isOwnedByUs(node, prop, entry)) { -+ node.style[prop] = entry.baseline; ++ for (const prop of props) { ++ const entry = entries[prop]; ++ if (!entry) { ++ continue; ++ } ++ if (isOwnedByUs(node, prop, entry)) { ++ node.style[prop] = entry.baseline; ++ } ++ delete entries[prop]; ++ entry.requests.clear(); ++ drainPendingReleases(entry); + } -+ delete entries[prop]; -+ entry.requests.clear(); -+ drainPendingReleases(entry); + } ++ entriesByNode.delete(node); +} + // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; ++var SCROLLER_MARKER_ATTRIBUTE = "data-legend-list-scroller"; var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; +var SMOOTH_SCROLL_MAX_MS = 2e3; @@ -9273,20 +9576,49 @@ index 914d2da..f03ba1e 100644 + "PageDown", + "PageUp" +]); ++var NON_SCROLLING_KEY_TARGETS = "input,textarea,select,button,[contenteditable],[role=textbox],[role=button]"; +function isTextEntryTarget(target) { + var _a3; + const element = target; -+ const tag = (_a3 = element == null ? void 0 : element.tagName) == null ? void 0 : _a3.toLowerCase(); -+ return tag === "input" || tag === "textarea" || tag === "select" || !!(element == null ? void 0 : element.isContentEditable); ++ if (!element) { ++ return false; ++ } ++ if (element.isContentEditable) { ++ return true; ++ } ++ return !!((_a3 = element.closest) == null ? void 0 : _a3.call(element, NON_SCROLLING_KEY_TARGETS)); +} -+function pointerPosition(event) { -+ var _a3; -+ const touch = (_a3 = event.touches) == null ? void 0 : _a3[0]; -+ if (touch) { -+ return { x: touch.clientX, y: touch.clientY }; ++function pointerPosition(event, id) { ++ const touches = event.touches; ++ if (touches == null ? void 0 : touches.length) { ++ const touch = id === void 0 ? touches[0] : [...touches].find((t) => t.identifier === id); ++ return touch ? { id: touch.identifier, x: touch.clientX, y: touch.clientY } : void 0; + } + const pointer = event; -+ return typeof pointer.clientX === "number" ? { x: pointer.clientX, y: pointer.clientY } : void 0; ++ if (typeof pointer.clientX !== "number") { ++ return void 0; ++ } ++ if (id !== void 0 && pointer.pointerId !== void 0 && pointer.pointerId !== id) { ++ return void 0; ++ } ++ return { id: pointer.pointerId, x: pointer.clientX, y: pointer.clientY }; ++} ++var POINTER_EVENT_HANDLERS = (onDown, onMove, onUp) => [ ++ ["pointerdown", onDown], ++ ["pointermove", onMove], ++ ["pointerup", onUp], ++ ["pointercancel", onUp], ++ ["touchstart", onDown], ++ ["touchmove", onMove], ++ ["touchend", onUp], ++ ["touchcancel", onUp] ++]; ++function now() { ++ return typeof performance !== "undefined" ? performance.now() : 0; ++} ++function isHover(event) { ++ const pointer = event; ++ return pointer.pointerType === "mouse" && pointer.buttons === 0; +} +function isScrollKey(event) { + if (event.altKey || event.ctrlKey || event.metaKey) { @@ -9297,7 +9629,7 @@ index 914d2da..f03ba1e 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6074,6 +6403,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6074,6 +6449,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -9313,7 +9645,7 @@ index 914d2da..f03ba1e 100644 const getMaxScrollOffset = React3.useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6094,8 +6432,153 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,8 +6478,180 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -9324,12 +9656,13 @@ index 914d2da..f03ba1e 100644 + const animatedPaddingReleaseRef = React3.useRef(void 0); + const withReachableExtent = React3.useCallback( + (offset, maxOffset, animated, run) => { -+ var _a4; ++ var _a4, _b2, _c2; + const contentNode = contentRef.current; + (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); + const committedMaxOffset = getCommittedMaxScrollOffset(getMaxScrollOffset()); ++ const modelMaxOffset = Math.max(0, ((_b2 = ctx.state.totalSize) != null ? _b2 : 0) - ((_c2 = ctx.state.scrollLength) != null ? _c2 : 0)); + const viewportExtent = getViewportExtent(); -+ const reachLimit = viewportExtent > 0 ? committedMaxOffset + MAX_BORROW_VIEWPORTS * viewportExtent : Number.POSITIVE_INFINITY; ++ const reachLimit = modelMaxOffset > 0 || viewportExtent > 0 ? Math.max(modelMaxOffset, committedMaxOffset + MAX_BORROW_VIEWPORTS * viewportExtent) + SCROLL_EXTENT_EPSILON : Number.POSITIVE_INFINITY; + if (!contentNode || !Number.isFinite(offset) || offset > reachLimit) { + run(clampOffset(offset, maxOffset)); + return; @@ -9346,6 +9679,10 @@ index 914d2da..f03ba1e 100644 + } + releases.push(addTemporaryEndPadding(contentNode, paddingEndProp, shortfall + SCROLL_EXTENT_EPSILON)); + } ++ if (releases.length === 0) { ++ run(offset); ++ return; ++ } + const borrowId = ++borrowIdRef.current; + const release = () => { + for (const releaseOne of releases) { @@ -9378,13 +9715,12 @@ index 914d2da..f03ba1e 100644 + finish(); + } + }; -+ let framesUntilCheck = 0; ++ let framesSeen = 0; + const releaseWhenContentCommits = () => { + if (animatedPaddingReleaseRef.current !== finish) { + return; + } -+ if (framesUntilCheck-- <= 0) { -+ framesUntilCheck = BORROW_WATCH_FRAME_INTERVAL; ++ if (++framesSeen % BORROW_WATCH_FRAME_INTERVAL === 0) { + if (getCommittedMaxScrollOffset(getMaxScrollOffset()) + SCROLL_EXTENT_EPSILON >= offset) { + finish(); + return; @@ -9423,28 +9759,51 @@ index 914d2da..f03ba1e 100644 + ); + const interactionArmedAtRef = React3.useRef(0); + const dragOriginRef = React3.useRef(void 0); ++ const ownsEvent = React3.useCallback((event) => { ++ const scroller = scrollRef.current; ++ const target = event.target; ++ if (!scroller || !(target == null ? void 0 : target.closest)) { ++ return true; ++ } ++ return target.closest(`[${SCROLLER_MARKER_ATTRIBUTE}]`) === scroller; ++ }, []); + const reportUserInteraction = React3.useCallback(() => { + dragOriginRef.current = void 0; + releaseScrollTargetForUserInteraction(ctx.state); + }, [ctx]); + const onWheel = React3.useCallback( + (event) => { -+ if (event.timeStamp - interactionArmedAtRef.current < WHEEL_MOMENTUM_GRACE_MS) { ++ if (!ownsEvent(event)) { ++ return; ++ } ++ if (now() - interactionArmedAtRef.current < WHEEL_MOMENTUM_GRACE_MS) { + return; + } + reportUserInteraction(); + }, -+ [reportUserInteraction] ++ [ownsEvent, reportUserInteraction] + ); -+ const onPointerDown = React3.useCallback((event) => { -+ const point = pointerPosition(event); -+ dragOriginRef.current = point; ++ const onPointerDown = React3.useCallback( ++ (event) => { ++ dragOriginRef.current = ownsEvent(event) ? pointerPosition(event, void 0) : void 0; ++ }, ++ [ownsEvent] ++ ); ++ const onPointerUp = React3.useCallback(() => { ++ dragOriginRef.current = void 0; + }, []); + const onPointerMove = React3.useCallback( + (event) => { + const origin = dragOriginRef.current; -+ const point = origin && pointerPosition(event); -+ if (!origin || !point) { ++ if (!origin) { ++ return; ++ } ++ if (isHover(event)) { ++ dragOriginRef.current = void 0; ++ return; ++ } ++ const point = pointerPosition(event, origin.id); ++ if (!point) { + return; + } + if (Math.abs(point.x - origin.x) > USER_DRAG_SLOP || Math.abs(point.y - origin.y) > USER_DRAG_SLOP) { @@ -9463,11 +9822,11 @@ index 914d2da..f03ba1e 100644 + ); const scrollToLocalOffset = React3.useCallback( (offset, animated) => { -+ interactionArmedAtRef.current = typeof performance !== "undefined" ? performance.now() : 0; ++ interactionArmedAtRef.current = now(); const scrollElement = scrollRef.current; const target = getScrollTarget(); if (!target || typeof target.scrollTo !== "function") { -@@ -6116,14 +6599,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6116,14 +6672,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); options.left = left; options.top = top; @@ -9494,7 +9853,7 @@ index 914d2da..f03ba1e 100644 ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6639,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6149,8 +6712,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -9504,7 +9863,7 @@ index 914d2da..f03ba1e 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6163,7 +6652,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6163,7 +6725,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView if (!onScroll2 || !scrollRef.current) { return; } @@ -9521,34 +9880,37 @@ index 914d2da..f03ba1e 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6221,11 +6718,26 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6218,14 +6788,29 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] + ); + React3.useLayoutEffect(() => { ++ var _a4; const target = getScrollTarget(); if (!target) return; target.addEventListener("scroll", handleScroll, { passive: true }); + const listenerOptions = { capture: true, passive: true }; + const removeOptions = { capture: true }; -+ const interactionTarget = scrollRef.current; ++ const interactionTarget = (_a4 = scrollRef.current) != null ? _a4 : void 0; ++ const keyTarget = isWindowScroll ? target : interactionTarget; + target.addEventListener("wheel", onWheel, listenerOptions); -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener("pointerdown", onPointerDown, listenerOptions); -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener("pointermove", onPointerMove, listenerOptions); -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener("touchstart", onPointerDown, listenerOptions); -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener("touchmove", onPointerMove, listenerOptions); -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener("keydown", onKeyDown, listenerOptions); ++ keyTarget == null ? void 0 : keyTarget.addEventListener("keydown", onKeyDown, listenerOptions); ++ for (const [type, handler] of POINTER_EVENT_HANDLERS(onPointerDown, onPointerMove, onPointerUp)) { ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener(type, handler, listenerOptions); ++ } if ("onscrollend" in target) { target.addEventListener("scrollend", emitScrollEnd); } return () => { target.removeEventListener("scroll", handleScroll); + target.removeEventListener("wheel", onWheel, removeOptions); -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("pointerdown", onPointerDown, removeOptions); -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("pointermove", onPointerMove, removeOptions); -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("touchstart", onPointerDown, removeOptions); -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("touchmove", onPointerMove, removeOptions); -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("keydown", onKeyDown, removeOptions); ++ keyTarget == null ? void 0 : keyTarget.removeEventListener("keydown", onKeyDown, removeOptions); ++ for (const [type, handler] of POINTER_EVENT_HANDLERS(onPointerDown, onPointerMove, onPointerUp)) { ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener(type, handler, removeOptions); ++ } if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6236,7 +6748,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6236,7 +6821,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } scrollEventCoalescer.cancel(); }; @@ -9566,7 +9928,15 @@ index 914d2da..f03ba1e 100644 React3.useEffect(() => { const doScroll = () => { if (contentOffset) { -@@ -6379,21 +6900,6 @@ function useValueListener$(key, callback) { +@@ -6341,6 +6935,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + { + className: scrollViewClassName, + ref: scrollRef, ++ ...{ [SCROLLER_MARKER_ATTRIBUTE]: "" }, + ...webProps, + style: scrollViewStyle + }, +@@ -6379,21 +6974,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -9588,7 +9958,7 @@ index 914d2da..f03ba1e 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6411,8 +6917,6 @@ function ScrollAdjust() { +@@ -6411,8 +6991,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3__namespace.useRef(0); const lastScrollAdjustUserOffsetRef = React3__namespace.useRef(0); @@ -9597,7 +9967,7 @@ index 914d2da..f03ba1e 100644 const contentNodeRef = React3__namespace.useRef(null); const callback = React3__namespace.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6423,7 +6927,7 @@ function ScrollAdjust() { +@@ -6423,7 +7001,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -9606,7 +9976,7 @@ index 914d2da..f03ba1e 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6438,29 +6942,10 @@ function ScrollAdjust() { +@@ -6438,29 +7016,10 @@ function ScrollAdjust() { const nextScroll = currentScroll + scrollDelta; const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; if (needsTemporaryPadding) { @@ -9638,7 +10008,30 @@ index 914d2da..f03ba1e 100644 } else { scrollBy(); } -@@ -8288,6 +8773,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7645,10 +8204,10 @@ function useThrottleDebounce(mode) { + const execute = React3.useCallback( + (callback, delay, ...args) => { + { +- const now = Date.now(); ++ const now2 = Date.now(); + lastArgsRef.current = args; +- if (now - lastCallTimeRef.current >= delay) { +- lastCallTimeRef.current = now; ++ if (now2 - lastCallTimeRef.current >= delay) { ++ lastCallTimeRef.current = now2; + callback(...args); + clearTimeoutRef(); + } else { +@@ -7662,7 +8221,7 @@ function useThrottleDebounce(mode) { + lastArgsRef.current = null; + } + }, +- delay - (now - lastCallTimeRef.current) ++ delay - (now2 - lastCallTimeRef.current) + ); + } + } +@@ -8288,6 +8847,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -9647,7 +10040,7 @@ index 914d2da..f03ba1e 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs -index 95465f2..ea937e4 100644 +index 95465f2..0005501 100644 --- a/node_modules/@legendapp/list/react.mjs +++ b/node_modules/@legendapp/list/react.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -10256,25 +10649,15 @@ index 95465f2..ea937e4 100644 // src/core/getStartOffsetAdjustment.ts function getStartOffsetAdjustment(ctx) { const { state } = ctx; -@@ -1213,58 +614,273 @@ function getItemSizeAtIndex(ctx, index) { - if (index === void 0 || index < 0) { - return void 0; +@@ -1226,45 +627,260 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { + if (viewOffset) { + offset -= viewOffset; } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+ const targetId = getId(ctx.state, index); -+ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+} -+ -+// src/core/calculateOffsetWithOffsetPosition.ts -+function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -+ var _a3; -+ const state = ctx.state; -+ const { index, viewOffset, viewPosition } = params; -+ let offset = offsetParam; -+ if (viewOffset) { -+ offset -= viewOffset; -+ } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } + if (index !== void 0) { + const startOffsetAdjustment = getStartOffsetAdjustment(ctx); + if (startOffsetAdjustment) { @@ -10484,7 +10867,22 @@ index 95465f2..ea937e4 100644 + if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { + state.isStartReached = false; + state.startReachedSnapshot = void 0; -+ } + } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } + set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); + set$(ctx, "isNearStart", scroll <= threshold); + const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; @@ -10511,50 +10909,19 @@ index 95465f2..ea937e4 100644 + state.startReachedSnapshot = snapshot; + } + ); -+ } + } +- return offset; } --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; -- } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } -- } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } -- } -- return offset; +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); - } - --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { ++} ++ +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { + var _a3, _b; @@ -10768,6 +11135,68 @@ index 95465f2..ea937e4 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; +@@ -1504,7 +1296,7 @@ var MVCP_POSITION_EPSILON = 0.1; + var MVCP_ANCHOR_LOCK_TTL_MS = 300; + var MVCP_ANCHOR_LOCK_QUIET_PASSES_TO_RELEASE = 2; + var NATIVE_END_CLAMP_EPSILON = 1; +-function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { ++function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now2) { + if (!enableMVCPAnchorLock) { + state.mvcpAnchorLock = void 0; + return void 0; +@@ -1513,7 +1305,7 @@ function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { + if (!lock) { + return void 0; + } +- const isExpired = now > lock.expiresAt; ++ const isExpired = now2 > lock.expiresAt; + const isMissing = state.indexByKey.get(lock.id) === void 0; + if (isExpired || isMissing || !mvcpData) { + state.mvcpAnchorLock = void 0; +@@ -1523,7 +1315,7 @@ function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { + } + function updateAnchorLock(state, params) { + { +- const { anchorId, anchorPosition, dataChanged, now, positionDiff } = params; ++ const { anchorId, anchorPosition, dataChanged, now: now2, positionDiff } = params; + const enableMVCPAnchorLock = !!dataChanged || !!state.mvcpAnchorLock; + const mvcpData = state.props.maintainVisibleContentPosition.data; + if (!enableMVCPAnchorLock || !mvcpData || state.scrollingTo || !anchorId || anchorPosition === void 0) { +@@ -1536,7 +1328,7 @@ function updateAnchorLock(state, params) { + return; + } + state.mvcpAnchorLock = { +- expiresAt: now + MVCP_ANCHOR_LOCK_TTL_MS, ++ expiresAt: now2 + MVCP_ANCHOR_LOCK_TTL_MS, + id: anchorId, + position: anchorPosition, + quietPasses +@@ -1641,14 +1433,14 @@ function prepareMVCP(ctx, dataChanged) { + const { + maintainVisibleContentPosition: { data: mvcpData, size: mvcpScroll, shouldRestorePosition } + } = props; +- const now = Date.now(); ++ const now2 = Date.now(); + const enableMVCPAnchorLock = (!!dataChanged || !!state.mvcpAnchorLock); + const scrollingTo = state.scrollingTo; + if (dataChanged && state.pendingScrollToEnd && scrollingTo === void 0) { + state.mvcpAnchorLock = void 0; + return void 0; + } +- const anchorLock = resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) ; ++ const anchorLock = resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now2) ; + let prevPosition; + let targetId; + const idsInViewWithPositions = []; +@@ -1765,7 +1557,7 @@ function prepareMVCP(ctx, dataChanged) { + anchorId: anchorIdForLock, + anchorPosition: anchorPositionForLock, + dataChanged, +- now, ++ now: now2, + positionDiff + }); + if (shouldQueueNativeMVCPAdjust()) { @@ -1788,6 +1580,27 @@ function prepareMVCP(ctx, dataChanged) { } } @@ -10796,34 +11225,12 @@ index 95465f2..ea937e4 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2034,70 +1847,395 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { - if (nextTop === void 0 || nextTop > viewportEnd) { - break; - } -- end++; -- } -- return { end, start }; --} --function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -- if (range) { -- ctx.state.scrollTargetPinnedRange = range; -- ctx.state.scrollForNextCalculateItemsInView = void 0; -- } else { -- ctx.state.scrollTargetPinnedRange = void 0; -+ end++; -+ } -+ return { end, start }; -+} -+function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -+ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -+ if (range) { -+ ctx.state.scrollTargetPinnedRange = range; -+ ctx.state.scrollForNextCalculateItemsInView = void 0; -+ } else { -+ ctx.state.scrollTargetPinnedRange = void 0; -+ } -+} +@@ -2047,57 +1860,389 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + ctx.state.scrollTargetPinnedRange = void 0; + } + } +-function scrollTo(ctx, params) { +- var _a3, _b; +function scrollTo(ctx, params) { + var _a3, _b, _c; + const state = ctx.state; @@ -10893,9 +11300,7 @@ index 95465f2..ea937e4 100644 +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; +function releaseScrollTargetForUserInteraction(state) { -+ if (state.scrollTargetSettle) { -+ clearScrollTargetSettle(state); -+ } ++ clearScrollTargetSettle(state); +} +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; @@ -10912,11 +11317,11 @@ index 95465f2..ea937e4 100644 + return; + } + clearScrollTargetSettle(state); -+ const now = Date.now(); ++ const now2 = Date.now(); + state.scrollTargetSettle = { + corrections: 0, -+ deadline: now + SETTLE_MAX_MS, -+ expiresAt: now + SETTLE_TTL_MS, ++ deadline: now2 + SETTLE_MAX_MS, ++ expiresAt: now2 + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, + quietPasses: 0, @@ -10960,6 +11365,12 @@ index 95465f2..ea937e4 100644 + viewOffset: settle.viewOffset, + viewPosition: settle.viewPosition + }); ++ const scrollingTo = state.scrollingTo; ++ if (scrollingTo && scrollingTo.index === index) { ++ const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); ++ scrollingTo.targetOffset = correctedOffset; ++ scrollingTo.offset = position; ++ } + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +} +function settleScrollTarget(ctx, options) { @@ -10972,7 +11383,10 @@ index 95465f2..ea937e4 100644 + const measured = options == null ? void 0 : options.minIndexSizeChanged; + if (measured !== void 0) { + settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); -+ settle.expiresAt = Date.now() + SETTLE_TTL_MS; ++ const targetIndex = state.indexByKey.get(settle.id); ++ if (targetIndex !== void 0 && measured <= targetIndex) { ++ settle.expiresAt = Date.now() + SETTLE_TTL_MS; ++ } + } + if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { @@ -10989,8 +11403,8 @@ index 95465f2..ea937e4 100644 + clearScrollTargetSettle(state); + return false; + } -+ const now = Date.now(); -+ if (now > settle.expiresAt || now > settle.deadline) { ++ const now2 = Date.now(); ++ if (now2 > settle.expiresAt || now2 > settle.deadline) { + clearScrollTargetSettle(state); + return false; + } @@ -11008,7 +11422,7 @@ index 95465f2..ea937e4 100644 + return false; + } + settle.quietPasses = 0; -+ settle.expiresAt = now + SETTLE_TTL_MS; ++ settle.expiresAt = now2 + SETTLE_TTL_MS; + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} @@ -11142,10 +11556,8 @@ index 95465f2..ea937e4 100644 + initialScrollCompletion.resetFlags(state); + setInitialScrollSession(state, { bootstrap: null }); + finishInitialScroll(ctx); - } - } --function scrollTo(ctx, params) { -- var _a3, _b; ++ } ++} +function finishInitialScroll(ctx, options) { + var _a3, _b, _c; const state = ctx.state; @@ -11247,7 +11659,7 @@ index 95465f2..ea937e4 100644 } // src/core/scrollToIndex.ts -@@ -4329,6 +4467,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4329,6 +4474,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -11255,7 +11667,7 @@ index 95465f2..ea937e4 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4346,8 +4485,17 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4346,8 +4492,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -11264,18 +11676,19 @@ index 95465f2..ea937e4 100644 + const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; + const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); + const mvcp = state.props.maintainVisibleContentPosition; -+ if (dataChanged && !mvcp.data && !mvcp.size) { -+ clearScrollTargetSettle(state); -+ } ++ const isUnanchoredDataChange = dataChanged && !mvcp.data; + const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; + if (!suppressInitialScrollSideEffects) { -+ settleScrollTarget(ctx, { isCompensating, minIndexSizeChanged: minIndexSizeChangedThisPass }); ++ settleScrollTarget(ctx, { ++ isCompensating, ++ minIndexSizeChanged: isUnanchoredDataChange ? 0 : minIndexSizeChangedThisPass ++ }); + } + if (didMVCPAdjust) { updateScroll2(state.scroll); updateScrollRange(); } -@@ -5906,6 +6054,21 @@ function getDocumentScrollerNode() { +@@ -5906,6 +6062,21 @@ function getDocumentScrollerNode() { } return document.scrollingElement || document.documentElement || document.body; } @@ -11297,13 +11710,14 @@ index 95465f2..ea937e4 100644 function getWindowScrollPosition() { var _a3, _b, _c, _d; if (typeof window === "undefined") { -@@ -5982,9 +6145,175 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -5982,9 +6153,213 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll }; } +// src/components/webTemporaryEndPadding.ts +var entriesByNode = /* @__PURE__ */ new WeakMap(); +var nextRequestId = 1; ++var RELEASE_ALL_MAX_PASSES = 5; +function readResolvedPadding(node, prop) { + return Number.parseFloat(window.getComputedStyle(node)[prop]) || 0; +} @@ -11414,22 +11828,30 @@ index 95465f2..ea937e4 100644 + if (!entries) { + return; + } -+ for (const prop of Object.keys(entries)) { -+ const entry = entries[prop]; -+ if (!entry) { -+ continue; ++ for (let pass = 0; pass < RELEASE_ALL_MAX_PASSES; pass++) { ++ const props = Object.keys(entries); ++ if (props.length === 0) { ++ break; + } -+ if (isOwnedByUs(node, prop, entry)) { -+ node.style[prop] = entry.baseline; ++ for (const prop of props) { ++ const entry = entries[prop]; ++ if (!entry) { ++ continue; ++ } ++ if (isOwnedByUs(node, prop, entry)) { ++ node.style[prop] = entry.baseline; ++ } ++ delete entries[prop]; ++ entry.requests.clear(); ++ drainPendingReleases(entry); + } -+ delete entries[prop]; -+ entry.requests.clear(); -+ drainPendingReleases(entry); + } ++ entriesByNode.delete(node); +} + // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; ++var SCROLLER_MARKER_ATTRIBUTE = "data-legend-list-scroller"; var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; +var SMOOTH_SCROLL_MAX_MS = 2e3; @@ -11449,20 +11871,49 @@ index 95465f2..ea937e4 100644 + "PageDown", + "PageUp" +]); ++var NON_SCROLLING_KEY_TARGETS = "input,textarea,select,button,[contenteditable],[role=textbox],[role=button]"; +function isTextEntryTarget(target) { + var _a3; + const element = target; -+ const tag = (_a3 = element == null ? void 0 : element.tagName) == null ? void 0 : _a3.toLowerCase(); -+ return tag === "input" || tag === "textarea" || tag === "select" || !!(element == null ? void 0 : element.isContentEditable); ++ if (!element) { ++ return false; ++ } ++ if (element.isContentEditable) { ++ return true; ++ } ++ return !!((_a3 = element.closest) == null ? void 0 : _a3.call(element, NON_SCROLLING_KEY_TARGETS)); +} -+function pointerPosition(event) { -+ var _a3; -+ const touch = (_a3 = event.touches) == null ? void 0 : _a3[0]; -+ if (touch) { -+ return { x: touch.clientX, y: touch.clientY }; ++function pointerPosition(event, id) { ++ const touches = event.touches; ++ if (touches == null ? void 0 : touches.length) { ++ const touch = id === void 0 ? touches[0] : [...touches].find((t) => t.identifier === id); ++ return touch ? { id: touch.identifier, x: touch.clientX, y: touch.clientY } : void 0; ++ } ++ const pointer = event; ++ if (typeof pointer.clientX !== "number") { ++ return void 0; ++ } ++ if (id !== void 0 && pointer.pointerId !== void 0 && pointer.pointerId !== id) { ++ return void 0; + } ++ return { id: pointer.pointerId, x: pointer.clientX, y: pointer.clientY }; ++} ++var POINTER_EVENT_HANDLERS = (onDown, onMove, onUp) => [ ++ ["pointerdown", onDown], ++ ["pointermove", onMove], ++ ["pointerup", onUp], ++ ["pointercancel", onUp], ++ ["touchstart", onDown], ++ ["touchmove", onMove], ++ ["touchend", onUp], ++ ["touchcancel", onUp] ++]; ++function now() { ++ return typeof performance !== "undefined" ? performance.now() : 0; ++} ++function isHover(event) { + const pointer = event; -+ return typeof pointer.clientX === "number" ? { x: pointer.clientX, y: pointer.clientY } : void 0; ++ return pointer.pointerType === "mouse" && pointer.buttons === 0; +} +function isScrollKey(event) { + if (event.altKey || event.ctrlKey || event.metaKey) { @@ -11473,7 +11924,7 @@ index 95465f2..ea937e4 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6053,6 +6382,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6053,6 +6428,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -11489,7 +11940,7 @@ index 95465f2..ea937e4 100644 const getMaxScrollOffset = useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6073,8 +6411,153 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,8 +6457,180 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -11500,12 +11951,13 @@ index 95465f2..ea937e4 100644 + const animatedPaddingReleaseRef = useRef(void 0); + const withReachableExtent = useCallback( + (offset, maxOffset, animated, run) => { -+ var _a4; ++ var _a4, _b2, _c2; + const contentNode = contentRef.current; + (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); + const committedMaxOffset = getCommittedMaxScrollOffset(getMaxScrollOffset()); ++ const modelMaxOffset = Math.max(0, ((_b2 = ctx.state.totalSize) != null ? _b2 : 0) - ((_c2 = ctx.state.scrollLength) != null ? _c2 : 0)); + const viewportExtent = getViewportExtent(); -+ const reachLimit = viewportExtent > 0 ? committedMaxOffset + MAX_BORROW_VIEWPORTS * viewportExtent : Number.POSITIVE_INFINITY; ++ const reachLimit = modelMaxOffset > 0 || viewportExtent > 0 ? Math.max(modelMaxOffset, committedMaxOffset + MAX_BORROW_VIEWPORTS * viewportExtent) + SCROLL_EXTENT_EPSILON : Number.POSITIVE_INFINITY; + if (!contentNode || !Number.isFinite(offset) || offset > reachLimit) { + run(clampOffset(offset, maxOffset)); + return; @@ -11522,6 +11974,10 @@ index 95465f2..ea937e4 100644 + } + releases.push(addTemporaryEndPadding(contentNode, paddingEndProp, shortfall + SCROLL_EXTENT_EPSILON)); + } ++ if (releases.length === 0) { ++ run(offset); ++ return; ++ } + const borrowId = ++borrowIdRef.current; + const release = () => { + for (const releaseOne of releases) { @@ -11554,13 +12010,12 @@ index 95465f2..ea937e4 100644 + finish(); + } + }; -+ let framesUntilCheck = 0; ++ let framesSeen = 0; + const releaseWhenContentCommits = () => { + if (animatedPaddingReleaseRef.current !== finish) { + return; + } -+ if (framesUntilCheck-- <= 0) { -+ framesUntilCheck = BORROW_WATCH_FRAME_INTERVAL; ++ if (++framesSeen % BORROW_WATCH_FRAME_INTERVAL === 0) { + if (getCommittedMaxScrollOffset(getMaxScrollOffset()) + SCROLL_EXTENT_EPSILON >= offset) { + finish(); + return; @@ -11599,28 +12054,51 @@ index 95465f2..ea937e4 100644 + ); + const interactionArmedAtRef = useRef(0); + const dragOriginRef = useRef(void 0); ++ const ownsEvent = useCallback((event) => { ++ const scroller = scrollRef.current; ++ const target = event.target; ++ if (!scroller || !(target == null ? void 0 : target.closest)) { ++ return true; ++ } ++ return target.closest(`[${SCROLLER_MARKER_ATTRIBUTE}]`) === scroller; ++ }, []); + const reportUserInteraction = useCallback(() => { + dragOriginRef.current = void 0; + releaseScrollTargetForUserInteraction(ctx.state); + }, [ctx]); + const onWheel = useCallback( + (event) => { -+ if (event.timeStamp - interactionArmedAtRef.current < WHEEL_MOMENTUM_GRACE_MS) { ++ if (!ownsEvent(event)) { ++ return; ++ } ++ if (now() - interactionArmedAtRef.current < WHEEL_MOMENTUM_GRACE_MS) { + return; + } + reportUserInteraction(); + }, -+ [reportUserInteraction] ++ [ownsEvent, reportUserInteraction] + ); -+ const onPointerDown = useCallback((event) => { -+ const point = pointerPosition(event); -+ dragOriginRef.current = point; ++ const onPointerDown = useCallback( ++ (event) => { ++ dragOriginRef.current = ownsEvent(event) ? pointerPosition(event, void 0) : void 0; ++ }, ++ [ownsEvent] ++ ); ++ const onPointerUp = useCallback(() => { ++ dragOriginRef.current = void 0; + }, []); + const onPointerMove = useCallback( + (event) => { + const origin = dragOriginRef.current; -+ const point = origin && pointerPosition(event); -+ if (!origin || !point) { ++ if (!origin) { ++ return; ++ } ++ if (isHover(event)) { ++ dragOriginRef.current = void 0; ++ return; ++ } ++ const point = pointerPosition(event, origin.id); ++ if (!point) { + return; + } + if (Math.abs(point.x - origin.x) > USER_DRAG_SLOP || Math.abs(point.y - origin.y) > USER_DRAG_SLOP) { @@ -11639,11 +12117,11 @@ index 95465f2..ea937e4 100644 + ); const scrollToLocalOffset = useCallback( (offset, animated) => { -+ interactionArmedAtRef.current = typeof performance !== "undefined" ? performance.now() : 0; ++ interactionArmedAtRef.current = now(); const scrollElement = scrollRef.current; const target = getScrollTarget(); if (!target || typeof target.scrollTo !== "function") { -@@ -6095,14 +6578,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6095,14 +6651,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); options.left = left; options.top = top; @@ -11670,7 +12148,7 @@ index 95465f2..ea937e4 100644 ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6618,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6128,8 +6691,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -11680,7 +12158,7 @@ index 95465f2..ea937e4 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6142,7 +6631,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6142,7 +6704,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ if (!onScroll2 || !scrollRef.current) { return; } @@ -11697,34 +12175,37 @@ index 95465f2..ea937e4 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6200,11 +6697,26 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6197,14 +6767,29 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] + ); + useLayoutEffect(() => { ++ var _a4; const target = getScrollTarget(); if (!target) return; target.addEventListener("scroll", handleScroll, { passive: true }); + const listenerOptions = { capture: true, passive: true }; + const removeOptions = { capture: true }; -+ const interactionTarget = scrollRef.current; ++ const interactionTarget = (_a4 = scrollRef.current) != null ? _a4 : void 0; ++ const keyTarget = isWindowScroll ? target : interactionTarget; + target.addEventListener("wheel", onWheel, listenerOptions); -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener("pointerdown", onPointerDown, listenerOptions); -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener("pointermove", onPointerMove, listenerOptions); -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener("touchstart", onPointerDown, listenerOptions); -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener("touchmove", onPointerMove, listenerOptions); -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener("keydown", onKeyDown, listenerOptions); ++ keyTarget == null ? void 0 : keyTarget.addEventListener("keydown", onKeyDown, listenerOptions); ++ for (const [type, handler] of POINTER_EVENT_HANDLERS(onPointerDown, onPointerMove, onPointerUp)) { ++ interactionTarget == null ? void 0 : interactionTarget.addEventListener(type, handler, listenerOptions); ++ } if ("onscrollend" in target) { target.addEventListener("scrollend", emitScrollEnd); } return () => { target.removeEventListener("scroll", handleScroll); + target.removeEventListener("wheel", onWheel, removeOptions); -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("pointerdown", onPointerDown, removeOptions); -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("pointermove", onPointerMove, removeOptions); -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("touchstart", onPointerDown, removeOptions); -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("touchmove", onPointerMove, removeOptions); -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener("keydown", onKeyDown, removeOptions); ++ keyTarget == null ? void 0 : keyTarget.removeEventListener("keydown", onKeyDown, removeOptions); ++ for (const [type, handler] of POINTER_EVENT_HANDLERS(onPointerDown, onPointerMove, onPointerUp)) { ++ interactionTarget == null ? void 0 : interactionTarget.removeEventListener(type, handler, removeOptions); ++ } if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6215,7 +6727,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6215,7 +6800,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } scrollEventCoalescer.cancel(); }; @@ -11742,7 +12223,15 @@ index 95465f2..ea937e4 100644 useEffect(() => { const doScroll = () => { if (contentOffset) { -@@ -6358,21 +6879,6 @@ function useValueListener$(key, callback) { +@@ -6320,6 +6914,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + { + className: scrollViewClassName, + ref: scrollRef, ++ ...{ [SCROLLER_MARKER_ATTRIBUTE]: "" }, + ...webProps, + style: scrollViewStyle + }, +@@ -6358,21 +6953,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -11764,7 +12253,7 @@ index 95465f2..ea937e4 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6390,8 +6896,6 @@ function ScrollAdjust() { +@@ -6390,8 +6970,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3.useRef(0); const lastScrollAdjustUserOffsetRef = React3.useRef(0); @@ -11773,7 +12262,7 @@ index 95465f2..ea937e4 100644 const contentNodeRef = React3.useRef(null); const callback = React3.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6402,7 +6906,7 @@ function ScrollAdjust() { +@@ -6402,7 +6980,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -11782,7 +12271,7 @@ index 95465f2..ea937e4 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6417,29 +6921,10 @@ function ScrollAdjust() { +@@ -6417,29 +6995,10 @@ function ScrollAdjust() { const nextScroll = currentScroll + scrollDelta; const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; if (needsTemporaryPadding) { @@ -11814,7 +12303,30 @@ index 95465f2..ea937e4 100644 } else { scrollBy(); } -@@ -8267,6 +8752,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7624,10 +8183,10 @@ function useThrottleDebounce(mode) { + const execute = useCallback( + (callback, delay, ...args) => { + { +- const now = Date.now(); ++ const now2 = Date.now(); + lastArgsRef.current = args; +- if (now - lastCallTimeRef.current >= delay) { +- lastCallTimeRef.current = now; ++ if (now2 - lastCallTimeRef.current >= delay) { ++ lastCallTimeRef.current = now2; + callback(...args); + clearTimeoutRef(); + } else { +@@ -7641,7 +8200,7 @@ function useThrottleDebounce(mode) { + lastArgsRef.current = null; + } + }, +- delay - (now - lastCallTimeRef.current) ++ delay - (now2 - lastCallTimeRef.current) + ); + } + } +@@ -8267,6 +8826,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); diff --git a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts index 5c332481f515..4ca651ebb46c 100644 --- a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts +++ b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts @@ -1,106 +1,150 @@ import type {ChainablePromiseElement} from 'webdriverio' import {expect} from '@wdio/globals' -import {anyExist, byText, el, els, tab, waitForTestID} from '../helpers/elements' -import {escapeToTabs} from '../helpers/navigate' +import {requireSmokeUser} from '../helpers/app' +import {anyExist, el, els, tab, waitForTestID, enterText} from '../helpers/elements' +import {dismissKeyboard, escapeToTabs} from '../helpers/navigate' import * as T from '../../shared/test-ids' -// More steps than the thread has hits, so the search wraps around and lands on hits it has already -// visited from a different scroll position - a case that used to leave the hit off screen. -const WRAPPING_STEPS = 20 // A word common enough to match throughout the thread, so hits span messages of different heights. const QUERY = 'one' // A word whose hit sits among the messages already on screen: jumping a few rows is the case where // the list has nothing to load and the scroll lands against a content size that has not caught up. const SAME_SCREEN_QUERY = 'working' -// Flings back through the thread until the top of the loaded page is on screen. +// Flings back through the thread until a page of older messages arrives. const MAX_FLINGS = 20 -const FLING_DISTANCE = 420 -// A fling moves the top-of-thread marker toward the viewport; only a prepend moves it away, and by -// far more than a fling's worth. +// A row has to be visible by more than a hairline to count as landed on. +const MIN_VISIBLE_HEIGHT = 24 +// How far the top of the thread has to jump away from the viewport to be a page of older messages +// arriving rather than the fling that provoked it. const PREPEND_MIN_SHIFT = 600 +// The thread is watched for this long after the drag, in samples, rather than looked at once at the +// end: a jump back to the hit can be brief. +const SNAP_BACK_SAMPLES = 8 +const SNAP_BACK_SAMPLE_MS = 400 -// iOS 26 puts the conversation's header actions in a native overflow menu (one glass pill), so -// there is no React view to carry a testID - the bar button and its menu item are addressed by the -// accessibility labels the platform exposes. Other platforms render the search control directly. +// iOS puts the conversation's header actions in a native overflow menu, so there is no React view +// to carry a testID - the bar button and its menu item are addressed by the accessibility labels +// the platform exposes, scoped to the navigation bar and the menu so they cannot match the "More" +// tab or the inbox's own search field. Other platforms render the search control directly. const openThreadSearch = async () => { if (browser.isIOS) { - await browser.$('~More').waitForExist({timeout: 5000, timeoutMsg: 'header overflow menu never appeared'}) - await browser.$('~More').click() - await browser.$('~Search').waitForExist({timeout: 5000, timeoutMsg: 'search menu item never appeared'}) - await browser.$('~Search').click() + const moreButton = browser.$('-ios class chain:**/XCUIElementTypeNavigationBar/**/XCUIElementTypeButton[`name == "More"`]') + await moreButton.waitForExist({timeout: 5000, timeoutMsg: 'header overflow menu never appeared'}) + await moreButton.click() + const searchItem = browser.$( + '-ios predicate string:(type == "XCUIElementTypeMenuItem" OR type == "XCUIElementTypeButton") AND name == "Search"' + ) + await searchItem.waitForExist({timeout: 5000, timeoutMsg: 'search menu item never appeared'}) + await searchItem.click() return } await waitForTestID(T.CHAT_HEADER_SEARCH_BUTTON, 5000) await el(T.CHAT_HEADER_SEARCH_BUTTON).click() } -type Bounds = {height: number; y: number} +type Bounds = {height: number; width: number; x: number; y: number} const boundsOfElement = async (element: ChainablePromiseElement): Promise => { const [location, size] = await Promise.all([element.getLocation(), element.getSize()]) - return {height: size.height, y: location.y} + return {height: size.height, width: size.width, x: location.x, y: location.y} } const boundsOf = async (id: string): Promise => boundsOfElement(el(id)) -// Off-screen rows are pruned from the accessibility tree, so a missing element is a real answer -// ("not on screen"), not an error - the caller decides what that means. +// A row outside the render window is not in the accessibility tree at all, and that is an answer +// ("not on screen") rather than an error. Anything else - a dead session, a driver fault - is not, +// so only a missing element is swallowed here. const maybeBoundsOf = async (element: ChainablePromiseElement): Promise => { - if (!(await element.isExisting().catch(() => false))) return undefined - return boundsOfElement(element).catch(() => undefined) + if (!(await element.isExisting())) return undefined + try { + return await boundsOfElement(element) + } catch (error) { + if (/no such element|stale element|not found/i.test(String(error))) return undefined + throw error + } +} + +// What the reader can actually see of the thread: the list's frame less the search bar, which +// overlays the bottom of it rather than shrinking it. +const visibleThreadBounds = async (): Promise => { + const list = await boundsOf(T.CHAT_MESSAGE_LIST) + const bar = await maybeBoundsOf(el(T.CHAT_THREAD_SEARCH_CANCEL)) + if (!bar) return list + return {...list, height: Math.max(0, bar.y - list.y)} } -const overlapsViewport = (thing: Bounds, list: Bounds): boolean => - Math.min(thing.y + thing.height, list.y + list.height) - Math.max(thing.y, list.y) > 0 +const visibleHeight = (thing: Bounds, viewport: Bounds): number => + Math.min(thing.y + thing.height, viewport.y + viewport.height) - Math.max(thing.y, viewport.y) // The row keeps its marker while it is the selected hit, but a virtualised list renders rows // outside the viewport too - so the marker existing says nothing about whether it can be seen. -// Compare where the row is against where the list is. -const hitOverlapsViewport = async (): Promise => { - const [hit, list] = await Promise.all([boundsOf(T.CHAT_SEARCH_HIT), boundsOf(T.CHAT_MESSAGE_LIST)]) - const overlaps = overlapsViewport(hit, list) - if (!overlaps) { - console.log(`hit off screen: row ${hit.y}..${hit.y + hit.height}, list ${list.y}..${list.y + list.height}`) - } - return overlaps +// Compare where the row is against where the thread is, and require more than a sliver. +const hitOnScreen = async (): Promise => { + const hit = await maybeBoundsOf(el(T.CHAT_SEARCH_HIT)) + if (!hit) return undefined + const viewport = await visibleThreadBounds() + return visibleHeight(hit, viewport) >= Math.min(hit.height, MIN_VISIBLE_HEIGHT) ? hit : undefined } -// The selected hit once the thread has been dragged away from it: undefined when the row has left -// the viewport entirely, which is the state the drag is meant to produce. -const hitIfOnScreen = async (): Promise => { +const describeHit = async (): Promise => { const hit = await maybeBoundsOf(el(T.CHAT_SEARCH_HIT)) - if (!hit) return undefined - const list = await boundsOf(T.CHAT_MESSAGE_LIST) - return overlapsViewport(hit, list) ? hit : undefined + const viewport = await visibleThreadBounds() + if (!hit) return `no highlighted row is rendered; thread ${viewport.y}..${viewport.y + viewport.height}` + return `row ${hit.y}..${hit.y + hit.height}, thread ${viewport.y}..${viewport.y + viewport.height}` } -// The thread's top-of-loaded-window marker. It stays in the render window while off screen, so its -// position is readable throughout - and a page-in is visible in that position directly: a fling -// moves the marker back toward the viewport, while a prepend pushes it thousands of pixels away. -const loadingOlderPosition = async (): Promise => - (await maybeBoundsOf(byText('Digging ancient')))?.y +// The header above the oldest loaded message. It stays mounted while off screen, so its position is +// readable throughout - and a page-in is visible in that position directly: a fling moves it toward +// the viewport, while a prepend pushes it a page's worth further away. +const topOfThreadPosition = async (): Promise => (await maybeBoundsOf(el(T.CHAT_THREAD_TOP)))?.y -const runSearch = async (query: string, steps: number) => { +// "3 of 18" in the search bar. The count is what makes the wrap-around case honest: stepping a +// fixed number of times proves nothing if the thread happens to have more hits than that. +const readHitCount = async (): Promise => { + const bar = browser.$('-ios predicate string:name CONTAINS " of "') + if (browser.isAndroid) { + const label = await browser.$('//*[contains(@text, " of ")]').getText().catch(() => '') + return Number(/of (\d+)/.exec(label)?.[1] ?? 0) + } + const label = await bar.getAttribute('name').catch(() => '') + return Number(/of (\d+)/.exec(label ?? '')?.[1] ?? 0) +} + +const startSearch = async (query: string): Promise => { await openThreadSearch() - await waitForTestID(T.CHAT_THREAD_SEARCH_NEXT, 5000) - // The search bar focuses itself on mount, so the query goes straight to the keyboard. - await browser.keys(query.split('')) + await waitForTestID(T.CHAT_THREAD_SEARCH_INPUT, 5000) + // The field focuses itself a beat after mounting, so type into it rather than sending keys at + // whatever happens to be focused. enterText also pastes where per-key injection is unsafe. + await enterText(T.CHAT_THREAD_SEARCH_INPUT, query) await browser.keys(['\n']) // Results stream in from the server; the first hit is selected once they arrive. await waitForTestID(T.CHAT_SEARCH_HIT, 15000) - await browser.pause(1200) - expect(await hitOverlapsViewport()).toBe(true) + const landed = await browser + .waitUntil(async () => (await hitOnScreen()) !== undefined, {timeout: 5000}) + .then(() => true) + .catch(() => false) + if (!landed) throw new Error(`the first hit never came on screen: ${await describeHit()}`) + const hits = await readHitCount() + if (hits === 0) throw new Error(`"${query}" found no hits in this conversation`) + return hits +} +const stepThroughHits = async (steps: number) => { for (let step = 0; step < steps; step++) { await el(T.CHAT_THREAD_SEARCH_PREV).click() - // Give the jump, and the measurements that follow it, time to settle before looking. A hit that - // lands and then drifts off screen is exactly the failure this is watching for. - await browser.pause(1200) - - await expect(el(T.CHAT_SEARCH_HIT)).toExist() - expect(await hitOverlapsViewport()).toBe(true) + // Give the jump, and the measurements that follow it, time to land. + const landed = await browser + .waitUntil(async () => (await hitOnScreen()) !== undefined, {timeout: 5000}) + .then(() => true) + .catch(() => false) + if (!landed) throw new Error(`hit ${step + 1} never came on screen: ${await describeHit()}`) + // A hit that lands and then drifts off screen is the other half of what this watches for. + await browser.pause(700) + if (!(await hitOnScreen())) { + throw new Error(`hit ${step + 1} landed and then drifted off screen: ${await describeHit()}`) + } } } @@ -109,37 +153,40 @@ const closeThreadSearch = async () => { await browser.pause(500) } +// Gestures are measured from the thread itself: on a tablet the inbox sits beside it, so a fixed x +// would scroll the wrong list, and the search bar and keyboard cover part of the thread's frame. +const gesturePoints = async (distance: number) => { + const viewport = await visibleThreadBounds() + const x = Math.round(viewport.x + viewport.width / 2) + const middle = viewport.y + viewport.height / 2 + const half = Math.min(distance, viewport.height - 80) / 2 + return {from: Math.round(middle - half), to: Math.round(middle + half), x} +} + // A fast flick that coasts - used only to travel back through the thread, never to establish the -// position the assertion depends on. -const flingThread = async (distance: number) => { - const list = await boundsOf(T.CHAT_MESSAGE_LIST) - const midY = Math.round(list.y + list.height / 2) - await browser - .action('pointer') - .move({x: 200, y: midY - distance / 2}) - .down() - .move({duration: 120, x: 200, y: midY + distance / 2}) - .up() - .perform() +// position an assertion depends on. +const flingThread = async () => { + const {from, to, x} = await gesturePoints(420) + await browser.action('pointer').move({x, y: from}).down().move({duration: 120, x, y: to}).up().perform() } -// Drag the thread without lifting into a fling, so it ends where it was left rather than coasting. -const dragThread = async (distance: number) => { - const list = await boundsOf(T.CHAT_MESSAGE_LIST) - const midY = Math.round(list.y + list.height / 2) +// A drag that ends where it is left rather than coasting. +const dragThread = async () => { + const {from, to, x} = await gesturePoints(200) await browser .action('pointer') - .move({x: 200, y: midY}) + .move({x, y: from}) .down() .pause(100) - .move({duration: 400, x: 200, y: midY + distance}) + .move({duration: 400, x, y: to}) .pause(100) .up() .perform() } // Each test starts from the tab root: the suite returns there between tests, so a flow cannot -// assume the conversation another one left open. +// assume the conversation another one left open. The conversation needs enough history to page in +// and enough matches for both queries, which is the smoke account's own chat with itself. const openFirstConversation = async (): Promise => { await escapeToTabs() await tab('Teams').click() @@ -149,59 +196,77 @@ const openFirstConversation = async (): Promise => { if (!(await anyExist(T.CHAT_INBOX_ROW))) return false await els(T.CHAT_INBOX_ROW)[0]!.click() await waitForTestID(T.CHAT_MESSAGE_LIST, 5000) + await dismissKeyboard() return true } describe('chat thread search', () => { it('keeps every hit it lands on visible, including wrapping around', async () => { - if (!(await openFirstConversation())) return + requireSmokeUser() + if (!(await openFirstConversation())) throw new Error('no conversations in the inbox') - await runSearch(QUERY, WRAPPING_STEPS) + // Two past the end, so the search wraps and lands on hits it has already visited from a + // different scroll position - the case that used to leave the hit off screen. + const hits = await startSearch(QUERY) + await stepThroughHits(hits + 2) await closeThreadSearch() }) it('lands on a hit that is already on screen', async () => { - if (!(await openFirstConversation())) return - await runSearch(SAME_SCREEN_QUERY, 1) + requireSmokeUser() + if (!(await openFirstConversation())) throw new Error('no conversations in the inbox') + + // No native mutation has been found that makes this case fail on its own - it is here because + // it is the case people report on desktop, where it does fail. Treat a green here as coverage + // of the flow, not proof of the fix. + await startSearch(SAME_SCREEN_QUERY) + await stepThroughHits(1) await closeThreadSearch() }) it('leaves the thread where the user drags it after a hit', async () => { - if (!(await openFirstConversation())) return - await runSearch(SAME_SCREEN_QUERY, 0) + requireSmokeUser() + if (!(await openFirstConversation())) throw new Error('no conversations in the inbox') + await startSearch(SAME_SCREEN_QUERY) // A moment after landing is when the list is still measuring, and where anything holding the // scroll target used to pull the thread back out from under the user. await browser.pause(1000) - expect(await hitIfOnScreen()).toBeDefined() + expect(await hitOnScreen()).toBeDefined() // Travel back toward older messages until a page of them actually arrives. The page-in is the // point: the prepend shifts every row's index, and an index-keyed re-centre reads that as a new // target and yanks the thread back to the hit. - let previousMarker = await loadingOlderPosition() + let previousTop = await topOfThreadPosition() let pagedIn = false for (let fling = 0; fling < MAX_FLINGS && !pagedIn; fling++) { - await flingThread(FLING_DISTANCE) + await flingThread() await browser.pause(200) - const marker = await loadingOlderPosition() - if (marker !== undefined && previousMarker !== undefined && marker < previousMarker - PREPEND_MIN_SHIFT) { - console.log(`page-in: top-of-thread marker moved ${previousMarker} -> ${marker}`) + const top = await topOfThreadPosition() + // A fling moves the top of the thread toward the viewport; only a prepend moves it away, and + // by a page's worth rather than a gesture's. + if (top !== undefined && previousTop !== undefined && top < previousTop - PREPEND_MIN_SHIFT) { + console.log(`page-in: top of thread moved ${previousTop} -> ${top}`) pagedIn = true } - previousMarker = marker + previousTop = top ?? previousTop } // Not provoking a page-in proves nothing about snapping back, so fail instead of passing. - if (!pagedIn) throw new Error('dragging never loaded another page of older messages') + if (!pagedIn) throw new Error('flinging never loaded another page of older messages') // End on a controlled drag so the thread rests where the user left it rather than coasting. - await dragThread(200) + await dragThread() await browser.pause(400) - if (await hitIfOnScreen()) throw new Error('the hit never left the viewport') - - // Long enough for the page to arrive, prepend, and for the list to finish measuring it. - await browser.pause(2500) + if (await hitOnScreen()) throw new Error(`the hit never left the viewport: ${await describeHit()}`) - const snappedBack = await hitIfOnScreen() + // Watch rather than look once. A re-centre lands at whatever moment the page finishes settling, + // and it does not necessarily stay: sampling only at the end lets a thread that jumped back and + // was then dragged on by momentum read as if nothing happened. + let snappedBack: Bounds | undefined + for (let sample = 0; sample < SNAP_BACK_SAMPLES && !snappedBack; sample++) { + await browser.pause(SNAP_BACK_SAMPLE_MS) + snappedBack = await hitOnScreen() + } if (snappedBack) { console.log(`thread snapped back to the hit: row at ${snappedBack.y} after being dragged away`) } diff --git a/shared/tests/e2e/shared/test-ids.ts b/shared/tests/e2e/shared/test-ids.ts index fa39d7fd8cfd..900e59cf8c44 100644 --- a/shared/tests/e2e/shared/test-ids.ts +++ b/shared/tests/e2e/shared/test-ids.ts @@ -33,11 +33,19 @@ export const CHAT_INFO_PANEL_SETTINGS_TAB = 'chat-info-panel-settings-tab' export const CHAT_HEADER_INFO_BUTTON = 'chat-header-info-button' export const CHAT_HEADER_SEARCH_BUTTON = 'chat-header-search-button' export const CHAT_THREAD_SEARCH_CANCEL = 'chat-thread-search-cancel' +// The thread search query field. It focuses itself a beat after mounting, so a test types into it +// rather than sending keys and hoping the focus landed. +export const CHAT_THREAD_SEARCH_INPUT = 'chat-thread-search-input' export const CHAT_THREAD_SEARCH_PREV = 'chat-thread-search-prev' export const CHAT_THREAD_SEARCH_NEXT = 'chat-thread-search-next' -// The message a thread search is currently sitting on. Present only while that row is highlighted, -// so asserting it exists is asserting the hit is on screen. +// The row a thread search is currently sitting on. Really "the centre-highlighted row": pinned +// messages, reply jumps and permalinks highlight one too, so this only means "search hit" inside a +// search flow. Present only while the row is highlighted. export const CHAT_SEARCH_HIT = 'chat-search-hit' +// The header above the oldest loaded message. Mounted in every state - loading, more to load, start +// of the conversation - so its position is readable throughout, which is how a test sees a page of +// older messages arrive. +export const CHAT_THREAD_TOP = 'chat-thread-top' // Files export const FILES_BROWSER = 'files-browser' From 0aeeded11a9d4bad726c20730a18ab4ae4e1b91a Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 11 Aug 2026 20:10:16 -0400 Subject: [PATCH 10/38] test(e2e): provoke the page-in after the reader moves, not before The rewrite in the previous commit lost the drag case's teeth: the app mutation that used to fail it three times out of three failed it about one run in three. Restoring the previous version of the file and running it against the current code failed 3/3, which placed the fault in the test rather than in the library. The cause was the order. The flow provoked a page-in and *then* dragged, so the prepend - and the re-centre it triggers - usually landed while the reader was still scrolling, where a thread that jumps back is indistinguishable from one being flung. The reported failure is the other way round: search, stop, then scroll, and the page that arrives underneath yanks the thread back. So the reader moves off the hit first, then keeps reading back through older messages until a page arrives, and the hit must not return at any point during that or in the window after it settles. The final check also watches the row's position rather than only its visibility, since whether the row can be *seen* depends on the search bar and the keyboard, while whether the thread travelled back toward it does not. Verified both directions against a freshly bundled app: 3/3 failures with the index-shift re-centre restored, reporting where the row and the thread were, and a clean pass without it. --- .../ios-appium/flows/chat-search-hit.test.ts | 53 ++++++++++++++----- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts index 4ca651ebb46c..b56f8434b8ed 100644 --- a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts +++ b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts @@ -21,6 +21,11 @@ const PREPEND_MIN_SHIFT = 600 // end: a jump back to the hit can be brief. const SNAP_BACK_SAMPLES = 8 const SNAP_BACK_SAMPLE_MS = 400 +// How far the hit row may travel back toward the viewport after the drag before that is the thread +// scrolling itself rather than settling. maintainVisibleContentPosition holds the visible content +// in place across the page-in, so a row that marches back by this much moved because something +// scrolled the list. +const SNAP_BACK_TOLERANCE = 150 // iOS puts the conversation's header actions in a native overflow menu, so there is no React view // to carry a testID - the bar button and its menu item are addressed by the accessibility labels @@ -234,14 +239,24 @@ describe('chat thread search', () => { await browser.pause(1000) expect(await hitOnScreen()).toBeDefined() - // Travel back toward older messages until a page of them actually arrives. The page-in is the - // point: the prepend shifts every row's index, and an index-keyed re-centre reads that as a new - // target and yanks the thread back to the hit. - let previousTop = await topOfThreadPosition() + // The reader moves away from the hit. + await dragThread() + await browser.pause(400) + if (await hitOnScreen()) throw new Error(`the hit never left the viewport: ${await describeHit()}`) + + // ...and keeps reading back through older messages, which is what asks the thread for another + // page. The order matters: the prepend has to arrive *after* the reader has moved, because the + // reported failure is "search, wait, scroll, and it pops back". Every index shifts when the page + // lands, and an index-keyed re-centre reads that as a new target. let pagedIn = false + let previousTop = await topOfThreadPosition() for (let fling = 0; fling < MAX_FLINGS && !pagedIn; fling++) { await flingThread() await browser.pause(200) + // At no point during this should the thread take itself back to the hit. + const returned = await hitOnScreen() + if (returned) throw new Error(`the thread scrolled back to the hit while reading: ${await describeHit()}`) + const top = await topOfThreadPosition() // A fling moves the top of the thread toward the viewport; only a prepend moves it away, and // by a page's worth rather than a gesture's. @@ -254,22 +269,32 @@ describe('chat thread search', () => { // Not provoking a page-in proves nothing about snapping back, so fail instead of passing. if (!pagedIn) throw new Error('flinging never loaded another page of older messages') - // End on a controlled drag so the thread rests where the user left it rather than coasting. + // Settle where the reader left it, and watch rather than look once: a re-centre lands whenever + // the page finishes measuring, and it does not necessarily stay. await dragThread() await browser.pause(400) - if (await hitOnScreen()) throw new Error(`the hit never left the viewport: ${await describeHit()}`) + const restingPosition = (await maybeBoundsOf(el(T.CHAT_SEARCH_HIT)))?.y - // Watch rather than look once. A re-centre lands at whatever moment the page finishes settling, - // and it does not necessarily stay: sampling only at the end lets a thread that jumped back and - // was then dragged on by momentum read as if nothing happened. - let snappedBack: Bounds | undefined + let snappedBack: string | undefined for (let sample = 0; sample < SNAP_BACK_SAMPLES && !snappedBack; sample++) { await browser.pause(SNAP_BACK_SAMPLE_MS) - snappedBack = await hitOnScreen() - } - if (snappedBack) { - console.log(`thread snapped back to the hit: row at ${snappedBack.y} after being dragged away`) + const visible = await hitOnScreen() + if (visible) { + snappedBack = `the hit is back on screen at ${visible.y}` + break + } + // Whether the hit is *visible* depends on the search bar and the keyboard; whether the thread + // travelled back toward it does not. maintainVisibleContentPosition holds the visible content + // in place across a page-in, so a row that marches back moved because something scrolled. + const position = (await maybeBoundsOf(el(T.CHAT_SEARCH_HIT)))?.y + if (restingPosition !== undefined && position !== undefined) { + const travelled = Math.abs(position - restingPosition) + if (travelled > SNAP_BACK_TOLERANCE) { + snappedBack = `the hit moved ${Math.round(travelled)} back toward the viewport (${restingPosition} -> ${position})` + } + } } + if (snappedBack) console.log(`thread scrolled itself after the drag: ${snappedBack}`) expect(snappedBack).toBeUndefined() await closeThreadSearch() From 6bb595d54ba133fd33408ca49962415085977c3e Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 11 Aug 2026 20:41:35 -0400 Subject: [PATCH 11/38] test(e2e): drag until the hit is gone, rather than a fixed number of times Measured the drag case over four back-to-back runs on unmodified code: three failed, all with the same message and the same geometry - "row 731..785, thread 116..785". Not flake. One 200px drag moves a centred hit to the bottom edge of the viewport and no further, so whether the row cleared the edge came down to its height and the screen's. Worse, that is the same message the mutation runs produced, so the "3/3 detection" in the previous commit was partly this defect firing rather than the regression being caught. Both readings were wrong for the same reason. Drag until the row is actually off screen, up to a bound. Then the state the rest of the case depends on is a fact rather than a hope, and a thread that re-centres itself still fails - because the drag can never get rid of the hit. Measured after the change, freshly bundled each time: - unmodified: 3 runs, 3 passing, no retries - index-shift re-centre restored: 2 runs, both failing at "the thread scrolled back to the hit while reading", with the row and thread bounds in the message Two runs of the suite fit in the time one used to take with its retries. --- .../ios-appium/flows/chat-search-hit.test.ts | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts index b56f8434b8ed..656799cfae50 100644 --- a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts +++ b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts @@ -12,6 +12,9 @@ const QUERY = 'one' const SAME_SCREEN_QUERY = 'working' // Flings back through the thread until a page of older messages arrives. const MAX_FLINGS = 20 +// Drags needed to move a hit clear of the viewport. One drag moves about a third of a screen, and +// the hit starts centred, so this is "enough to be sure" rather than a tuned number. +const MAX_DRAGS_AWAY = 6 // A row has to be visible by more than a hairline to count as landed on. const MIN_VISIBLE_HEIGHT = 24 // How far the top of the thread has to jump away from the viewport to be a page of older messages @@ -189,6 +192,19 @@ const dragThread = async () => { .perform() } +// Drag until the hit is off screen, which is the state the rest of the case depends on. Failing here +// is a real failure: with the thread re-centring itself on the hit, this is where that shows up. +const dragUntilHitLeaves = async () => { + for (let attempt = 0; attempt < MAX_DRAGS_AWAY; attempt++) { + await dragThread() + await browser.pause(400) + if (!(await hitOnScreen())) return + } + throw new Error( + `the hit never left the viewport after ${MAX_DRAGS_AWAY} drags: ${await describeHit()}` + ) +} + // Each test starts from the tab root: the suite returns there between tests, so a flow cannot // assume the conversation another one left open. The conversation needs enough history to page in // and enough matches for both queries, which is the smoke account's own chat with itself. @@ -239,10 +255,10 @@ describe('chat thread search', () => { await browser.pause(1000) expect(await hitOnScreen()).toBeDefined() - // The reader moves away from the hit. - await dragThread() - await browser.pause(400) - if (await hitOnScreen()) throw new Error(`the hit never left the viewport: ${await describeHit()}`) + // The reader moves away from the hit. Dragged until the row is genuinely gone rather than a + // fixed number of times: how far one drag carries a hit depends on the row's height and the + // screen's, and a hit still clinging to the bottom edge is not the state this test is about. + await dragUntilHitLeaves() // ...and keeps reading back through older messages, which is what asks the thread for another // page. The order matters: the prepend has to arrive *after* the reader has moved, because the @@ -271,8 +287,7 @@ describe('chat thread search', () => { // Settle where the reader left it, and watch rather than look once: a re-centre lands whenever // the page finishes measuring, and it does not necessarily stay. - await dragThread() - await browser.pause(400) + await dragUntilHitLeaves() const restingPosition = (await maybeBoundsOf(el(T.CHAT_SEARCH_HIT)))?.y let snappedBack: string | undefined From 71ff88083fcb4e41ade5230e164c5bae377a97f8 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 11 Aug 2026 21:43:53 -0400 Subject: [PATCH 12/38] test(e2e): stop retrying the thread-search flow The suite retries twice by default to absorb load on a long single-session run. That is defensible for flows where a failure means "the sim was busy", but this one exists to catch a thread that scrolls itself, and a retry cannot tell that apart from a slow simulator - it just runs again and reports green. It hid a real 3-in-4 failure rate from me while I was measuring this flow: the run that "passed" needed two retries to do it. Measured stable without them across four runs on unmodified code, and it still fails on the first attempt when the regression is present. --- shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts index 656799cfae50..809ad4c3c841 100644 --- a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts +++ b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts @@ -221,7 +221,13 @@ const openFirstConversation = async (): Promise => { return true } -describe('chat thread search', () => { +describe('chat thread search', function () { + // No retries here. The suite retries by default to absorb load on a long single-session run, but + // a re-run cannot tell a slow sim from a thread that scrolled itself: the failures this flow + // exists to catch are exactly the ones a retry papers over. It has been measured stable without + // them - see the commit that dragged until the hit is gone. + this.retries(0) + it('keeps every hit it lands on visible, including wrapping around', async () => { requireSmokeUser() if (!(await openFirstConversation())) throw new Error('no conversations in the inbox') From 05e5bbca1e81adadbf9ac0fd2af10307df2d4695 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 11 Aug 2026 22:32:52 -0400 Subject: [PATCH 13/38] test(e2e): make retries opt-in per flow rather than the default The config claimed "a real break fails all attempts". That is not true of every flow: a chat-search regression that failed three runs in four was reported green, because a retry cannot tell a slow simulator from a thread that scrolled itself. It cost a full round of measurement to notice. So the default is 0 on both iOS and Android, and a flow that is genuinely load-sensitive asks for retries in its own describe, where the reason sits next to the test it excuses. Measured a full iPhone suite run to decide who gets one. Exactly two tests owed their pass to a retry: people-profile's feed render, which waits on a network load, and one visual-states case. people-profile keeps retries; visual-states does not, because it is currently failing for an unrelated reason (below) and a retry there would hide it rather than absorb it. Unrelated finding, not addressed here: 20 of 23 visual-states tests fail, in isolation as well as in the full suite, all at tab navigation ("testID teams-list/files-browser/chat-message-list never appeared"). They cascade from 'new chat team builder', after which escapeToTabs cannot get the app out of the New Chat modal. Not caused by the testIDs in this branch - it takes out Files, Teams and Settings equally. --- .../e2e/ios-appium/flows/chat-search-hit.test.ts | 7 +++---- .../e2e/ios-appium/flows/people-profile.test.ts | 7 ++++++- shared/tests/e2e/ios-appium/wdio.android.conf.ts | 8 ++++---- shared/tests/e2e/ios-appium/wdio.conf.ts | 16 +++++++++------- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts index 809ad4c3c841..4deb4d311e12 100644 --- a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts +++ b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts @@ -222,10 +222,9 @@ const openFirstConversation = async (): Promise => { } describe('chat thread search', function () { - // No retries here. The suite retries by default to absorb load on a long single-session run, but - // a re-run cannot tell a slow sim from a thread that scrolled itself: the failures this flow - // exists to catch are exactly the ones a retry papers over. It has been measured stable without - // them - see the commit that dragged until the hit is gone. + // Stated rather than inherited: a re-run cannot tell a slow sim from a thread that scrolled + // itself, so the failures this flow exists to catch are exactly the ones a retry would paper + // over. If the suite default ever goes back to retrying, this flow still must not. this.retries(0) it('keeps every hit it lands on visible, including wrapping around', async () => { diff --git a/shared/tests/e2e/ios-appium/flows/people-profile.test.ts b/shared/tests/e2e/ios-appium/flows/people-profile.test.ts index 59d0b9d1efc1..b7f3ac854aac 100644 --- a/shared/tests/e2e/ios-appium/flows/people-profile.test.ts +++ b/shared/tests/e2e/ios-appium/flows/people-profile.test.ts @@ -4,7 +4,12 @@ import {escapeToTabs, navigateToPeople} from '../helpers/navigate' import {byText, el, waitForTestID} from '../helpers/elements' import * as T from '../../shared/test-ids' -describe('people profile', () => { +describe('people profile', function () { + // The feed renders from a network load that is slow often enough to miss the wait on a busy + // machine, and a re-run is a fair way to tell that apart from a broken feed - nothing here + // depends on state the first attempt left behind. + this.retries(2) + it('renders the feed and opens own profile when visible', async () => { const smokeUser = requireSmokeUser() await escapeToTabs() diff --git a/shared/tests/e2e/ios-appium/wdio.android.conf.ts b/shared/tests/e2e/ios-appium/wdio.android.conf.ts index ae9980a1862e..c4ea79562031 100644 --- a/shared/tests/e2e/ios-appium/wdio.android.conf.ts +++ b/shared/tests/e2e/ios-appium/wdio.android.conf.ts @@ -27,10 +27,10 @@ export const config: WebdriverIO.Config = { capabilities: [androidCapabilities(serial)], logLevel: 'warn', framework: 'mocha', - // Mirrors the iOS config: the one-session suite accumulates load over many - // flows; retries: 2 (each with a fresh escapeToTabs reset) absorbs transient - // nav/list flake without masking real failures. Retries run ONLY on failure. - mochaOpts: {ui: 'bdd', timeout: 120000, retries: 2}, + // retries: 0, matching the iOS config - see wdio.conf.ts for why. A flow that + // genuinely needs a retry asks for one in its own describe, where the reason is + // visible to whoever reads the failure. + mochaOpts: {ui: 'bdd', timeout: 120000, retries: 0}, reporters: ['spec'], // relaxedSecurity lets Appium run privileged commands such as `mobile: shell`. // android-activity-restart.test.ts needs it (pidof) to prove the app process diff --git a/shared/tests/e2e/ios-appium/wdio.conf.ts b/shared/tests/e2e/ios-appium/wdio.conf.ts index f2b3b85b48a1..ecac835b2606 100644 --- a/shared/tests/e2e/ios-appium/wdio.conf.ts +++ b/shared/tests/e2e/ios-appium/wdio.conf.ts @@ -46,13 +46,15 @@ export const config: WebdriverIO.Config = { logLevel: 'warn', framework: 'mocha', // 120s: the tablet settings-subpages flow can run long; phone tests finish well - // under this. retries: 2 — the one-session suite accumulates load over 16 flows - // (KBFS/list loads, transient nav), and the old iOS-16.4 sims are slower/flakier - // still (paste-menu summon, list timing), so a flow can intermittently fail; up - // to two retries (each with a fresh escapeToTabs reset) absorbs that without - // masking real failures (a real break fails all attempts). Retries run ONLY on - // failure, so passing tests cost nothing. - mochaOpts: {ui: 'bdd', timeout: 120000, retries: 2}, + // under this. + // + // retries: 0. The claim this used to carry — "a real break fails all attempts" — + // is not true of every flow: a chat-search regression that failed three runs in + // four was reported green here, because a retry cannot tell a slow simulator + // from a thread that scrolled itself. Retries are opt-in per flow now: a flow + // that is genuinely load-sensitive calls this.retries(n) in its describe and + // says why, which keeps the cost visible where someone can judge it. + mochaOpts: {ui: 'bdd', timeout: 120000, retries: 0}, reporters: ['spec'], services: [['appium', {args: {basePath: '/', port}}]], // Set device orientation once at session start (e.g. iPad in landscape). From f7b514cf62c0ad0dd8883ba792f2b150266ed759 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 12 Aug 2026 08:50:11 -0400 Subject: [PATCH 14/38] test(e2e): let the reset escape a keyboard and a modal it could not before visual-states was failing 20 of 23 on this branch. Two independent holes in the reset between tests, both of which also outlive a run, because the app restores its last screen on launch and the after-hook uses the same reset. The keyboard. WDA answers "Did not know how to dismiss the keyboard" for the chat composer - no Done key, no accessory to press - so dismissKeyboard silently gave up. That is not cosmetic: while the keyboard is up the screen's own controls stop reporting as hittable, so the back chevron is invisible to tapNavBack and the left-edge pop does not take. escapeToTabs then spent its whole budget in a conversation it could not leave, ~50s per test, and every flow after it started from the wrong screen. Instrumenting the exhaustion path is what found this: it reported people=0, nav=[Back|chatConversation|More], and visible buttons that were all keyboard keys. Now it blurs the composer by tapping the content above it, the way a person would. The modal. A modal presented over the tabs leaves the tab bar - and the whole screen behind it - in the accessibility tree, so atTabs read "already at the root" while a New chat or account-switcher modal was still up, and the reset returned with it still there. The next flow then tapped what it thought were inbox rows and got the modal's people list. Measured while the New chat sheet was up: People present but not visible, five inbox rows, and a visible Cancel, all at once. The reset now dismisses what is on top before asking whether it is home. Both fixes are in the reset rather than in the tests, because this suite has tests deliberately end on the state they capture - the after-hook comment says so - which makes cleaning up the reset's job. visual-states: 23 passing, three runs in a row, having been 3 passing / 20 failing. No retries anywhere. --- .../tests/e2e/ios-appium/helpers/navigate.ts | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/shared/tests/e2e/ios-appium/helpers/navigate.ts b/shared/tests/e2e/ios-appium/helpers/navigate.ts index 2733c1b725e2..38c54343f9f4 100644 --- a/shared/tests/e2e/ios-appium/helpers/navigate.ts +++ b/shared/tests/e2e/ios-appium/helpers/navigate.ts @@ -85,10 +85,33 @@ async function atTabs(): Promise { export async function dismissKeyboard(): Promise { // isKeyboardShown is a direct Appium endpoint (fast) — avoid an // //XCUIElementTypeKeyboard xpath, which is a slow full-tree search per call. - if (await browser.isKeyboardShown().catch(() => false)) { - if (browser.isAndroid) await browser.hideKeyboard().catch(() => {}) - else await browser.execute('mobile: hideKeyboard').catch(() => {}) + if (!(await browser.isKeyboardShown().catch(() => false))) return + if (browser.isAndroid) { + await browser.hideKeyboard().catch(() => {}) + return } + await browser.execute('mobile: hideKeyboard').catch(() => {}) + if (!(await browser.isKeyboardShown().catch(() => false))) return + + // WDA answers "Did not know how to dismiss the keyboard" for the chat composer — it has no Done + // key and no accessory to press. Blur it the way a person would, by tapping the content above it. + // This is not cosmetic: while the keyboard is up the screen's own controls stop reporting as + // hittable, so the back chevron is invisible to tapNavBack and the left-edge pop does not take, + // and escapeToTabs burns its whole budget on a conversation it cannot leave. Every flow after it + // then starts from the wrong screen. + const {height, width} = await browser.getWindowRect() + await browser + .action('pointer') + .move({x: Math.round(width / 2), y: Math.round(height * 0.3)}) + .down() + .up() + .perform() + .catch(() => {}) + await browser.waitUntil(async () => !(await browser.isKeyboardShown().catch(() => false)), { + interval: 100, + timeout: 2000, + }) + .catch(() => {}) } // Tap the leading (leftmost) button of a native NavigationBar — the back @@ -177,6 +200,23 @@ export async function escapeToTabs(): Promise { } throw new Error('escapeToTabs(android): root tab bar not reached after 12 attempts') } + // Dismiss anything presented over the tabs BEFORE asking whether we are at the root. A modal + // leaves the tab bar - and the whole screen behind it - in the accessibility tree, so atTabs reads + // "already home" while a New chat or account-switcher modal is still up; the reset then returns + // with it still there, the next flow taps rows belonging to the modal, and every test after it + // fails somewhere unrelated. It outlives the run too: the app restores its last screen, so a + // leaked modal wedges the NEXT run from its first test. Bounded, and only ever clicks a control + // that is on screen - at a real root there is nothing to click and this costs one query. + for (let i = 0; i < 3; i++) { + const controls = browser.$$(DISMISS_PRED) + if ((await controls.length) === 0) break + const ctrl = controls[0]! + await ctrl.click().catch(() => {}) + await settleAfter(ctrl) + // A control that survives its own click is not a modal dismiss - leave it to the loop below + // rather than clicking it forever. + if (await ctrl.isExisting().catch(() => false)) break + } for (let i = 0; i < 10; i++) { if (await atTabs()) return // Past the first few hops something is off — narrate each step so a stall From 7a522809ecff4fe3fd2f8ccc1c36c51858f4747f Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 12 Aug 2026 09:22:52 -0400 Subject: [PATCH 15/38] test(e2e): click the feed row, not the header avatar people-profile matched its own username anywhere on screen. The People header's avatar carries that username too, so the tap could land there instead of on a feed row - which opens the account switcher, fails this test on a missing profile page, and leaves a modal up for whatever runs next. It had retries, so it had been passing on the second attempt and reading green. With retries opt-in and this flow's reason for keeping them being network slowness, it started failing all three attempts and showed what it was really doing. Scoped to the feed. Passes twice in a row now without consuming a retry. --- .../tests/e2e/ios-appium/flows/people-profile.test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/shared/tests/e2e/ios-appium/flows/people-profile.test.ts b/shared/tests/e2e/ios-appium/flows/people-profile.test.ts index b7f3ac854aac..4f1e56174f0c 100644 --- a/shared/tests/e2e/ios-appium/flows/people-profile.test.ts +++ b/shared/tests/e2e/ios-appium/flows/people-profile.test.ts @@ -1,7 +1,7 @@ import {expect} from '@wdio/globals' import {requireSmokeUser} from '../helpers/app' import {escapeToTabs, navigateToPeople} from '../helpers/navigate' -import {byText, el, waitForTestID} from '../helpers/elements' +import {el, waitForTestID} from '../helpers/elements' import * as T from '../../shared/test-ids' describe('people profile', function () { @@ -18,7 +18,13 @@ describe('people profile', function () { // Your own username appearing in your own feed is genuinely conditional // (the feed surfaces others' activity), so guard rather than hard-wait. - const userEl = byText(smokeUser) + // + // Scoped to the feed, not matched across the screen: the People header's avatar carries the + // username too, and tapping that opens the account switcher rather than a profile - which then + // fails here on a missing profile page, and leaves a modal up for whatever runs next. + const userEl = el(T.PEOPLE_FEED).$( + `-ios predicate string:label CONTAINS "${smokeUser}" OR name CONTAINS "${smokeUser}"` + ) if (!(await userEl.isExisting())) return await userEl.click() await waitForTestID(T.PROFILE_PAGE, 10000) From 850f575fbc8c3e5380ac64010705c09439786829 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 12 Aug 2026 10:19:41 -0400 Subject: [PATCH 16/38] fix(chat): update legend-list patch, which had stopped reaching search hits Validating on Electron caught a regression I introduced in the fork: searching a thread scrolled nowhere, leaving the hit off screen with the list at the top - the exact defect this whole effort set out to fix. The cause was a ceiling I added on how far a scroll may reach past the committed content, on review advice, to avoid materialising blank scrollable space for a nonsense request. It refuses the case the feature exists for. Traced in the app: offset 9047, committed DOM extent 0, totalSize-derived bound 8932 - so the request was clamped to 0 and the list never moved. The offset is resolved from positions that include estimates, so it can sit past the list's own totalSize while the DOM reports nothing yet; neither number bounds it. The ceiling is gone. Only a non-finite offset falls back to clamping now, and the fork carries a test built from those measured numbers. Verified after the change: - Electron: 'working' lands with scrollTop == maxScroll and the row on screen; 21 hits of 'one' stepped through the wrap-around, none off screen; scrolling away from a hit stays put. - iOS: chat-search-hit 3/3, page-in provoked. Worth recording: the desktop app imports @legendapp/list/react, and I spent an hour instrumenting the react-native.web build before noticing. --- shared/patches/@legendapp+list+3.3.5.patch | 680 ++++++++++++--------- 1 file changed, 396 insertions(+), 284 deletions(-) diff --git a/shared/patches/@legendapp+list+3.3.5.patch b/shared/patches/@legendapp+list+3.3.5.patch index 1ae42174f758..0ef795031e79 100644 --- a/shared/patches/@legendapp+list+3.3.5.patch +++ b/shared/patches/@legendapp+list+3.3.5.patch @@ -3155,7 +3155,7 @@ index 40e87cd..b9895df 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js -index 914d2da..6240e13 100644 +index 914d2da..da93b27 100644 --- a/node_modules/@legendapp/list/react-native.web.js +++ b/node_modules/@legendapp/list/react-native.web.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -3764,15 +3764,25 @@ index 914d2da..6240e13 100644 // src/core/getStartOffsetAdjustment.ts function getStartOffsetAdjustment(ctx) { const { state } = ctx; -@@ -1247,45 +648,260 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { - if (viewOffset) { - offset -= viewOffset; +@@ -1234,58 +635,273 @@ function getItemSizeAtIndex(ctx, index) { + if (index === void 0 || index < 0) { + return void 0; } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; ++ } + if (index !== void 0) { + const startOffsetAdjustment = getStartOffsetAdjustment(ctx); + if (startOffsetAdjustment) { @@ -3982,22 +3992,7 @@ index 914d2da..6240e13 100644 + if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { + state.isStartReached = false; + state.startReachedSnapshot = void 0; - } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } ++ } + set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); + set$(ctx, "isNearStart", scroll <= threshold); + const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; @@ -4024,19 +4019,50 @@ index 914d2da..6240e13 100644 + state.startReachedSnapshot = snapshot; + } + ); - } -- return offset; ++ } } --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +- } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } +- } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } +- } +- return offset; +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+} -+ + } + +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { + var _a3, _b; @@ -4058,11 +4084,27 @@ index 914d2da..6240e13 100644 } // src/core/finishScrollTo.ts -@@ -1422,7 +1038,182 @@ function listenForScrollEnd(ctx, params) { - } else { - idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); - } -- scheduledWork.register("platformScrollCompletion", cancel); +@@ -1413,16 +1029,191 @@ function listenForScrollEnd(ctx, params) { + if (idleTimeout !== void 0) { + clearTimeout(idleTimeout); + } +- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); +- }; +- const onScrollEnd = () => finish("scrollend"); +- target.addEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.addEventListener("scrollend", onScrollEnd); +- } else { +- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); ++ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); ++ }; ++ const onScrollEnd = () => finish("scrollend"); ++ target.addEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.addEventListener("scrollend", onScrollEnd); ++ } else { ++ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); ++ } + scheduledWork.register("platformScrollCompletion", cancel); +} + @@ -4238,7 +4280,8 @@ index 914d2da..6240e13 100644 + } else { + resetAdaptiveRender(ctx); + } -+ } + } +- scheduledWork.register("platformScrollCompletion", cancel); } // src/core/doMaintainScrollAtEnd.ts @@ -4825,7 +4868,7 @@ index 914d2da..6240e13 100644 function getWindowScrollPosition() { var _a3, _b, _c, _d; if (typeof window === "undefined") { -@@ -6003,9 +6174,213 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -6003,9 +6174,212 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll }; } @@ -4970,7 +5013,6 @@ index 914d2da..6240e13 100644 var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; +var SMOOTH_SCROLL_MAX_MS = 2e3; -+var MAX_BORROW_VIEWPORTS = 3; +var BORROW_WATCH_FRAME_INTERVAL = 3; +var BORROW_MEASURE_ATTEMPTS = 2; +var USER_DRAG_SLOP = 8; @@ -5039,7 +5081,7 @@ index 914d2da..6240e13 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6074,6 +6449,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6074,6 +6448,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -5048,14 +5090,10 @@ index 914d2da..6240e13 100644 + (maxOffset) => Math.max(0, maxOffset - getTemporaryEndPadding(contentRef.current, paddingEndProp)), + [paddingEndProp] + ); -+ const getViewportExtent = React3.useCallback(() => { -+ const layout = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); -+ return horizontal ? layout.width : layout.height; -+ }, [horizontal, isWindowScroll]); const getMaxScrollOffset = React3.useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6094,8 +6478,180 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,8 +6473,170 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -5066,14 +5104,11 @@ index 914d2da..6240e13 100644 + const animatedPaddingReleaseRef = React3.useRef(void 0); + const withReachableExtent = React3.useCallback( + (offset, maxOffset, animated, run) => { -+ var _a4, _b2, _c2; ++ var _a4; + const contentNode = contentRef.current; + (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); + const committedMaxOffset = getCommittedMaxScrollOffset(getMaxScrollOffset()); -+ const modelMaxOffset = Math.max(0, ((_b2 = ctx.state.totalSize) != null ? _b2 : 0) - ((_c2 = ctx.state.scrollLength) != null ? _c2 : 0)); -+ const viewportExtent = getViewportExtent(); -+ const reachLimit = modelMaxOffset > 0 || viewportExtent > 0 ? Math.max(modelMaxOffset, committedMaxOffset + MAX_BORROW_VIEWPORTS * viewportExtent) + SCROLL_EXTENT_EPSILON : Number.POSITIVE_INFINITY; -+ if (!contentNode || !Number.isFinite(offset) || offset > reachLimit) { ++ if (!contentNode || !Number.isFinite(offset)) { + run(clampOffset(offset, maxOffset)); + return; + } @@ -5146,14 +5181,7 @@ index 914d2da..6240e13 100644 + scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finishIfArrived); + } + }, -+ [ -+ getCommittedMaxScrollOffset, -+ getCurrentScrollOffset, -+ getMaxScrollOffset, -+ getScrollTarget, -+ getViewportExtent, -+ paddingEndProp -+ ] ++ [getCommittedMaxScrollOffset, getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, paddingEndProp] + ); + React3.useEffect( + () => () => { @@ -5236,7 +5264,7 @@ index 914d2da..6240e13 100644 const scrollElement = scrollRef.current; const target = getScrollTarget(); if (!target || typeof target.scrollTo !== "function") { -@@ -6116,14 +6672,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6116,14 +6657,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); options.left = left; options.top = top; @@ -5263,7 +5291,7 @@ index 914d2da..6240e13 100644 ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6712,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6149,8 +6697,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -5273,7 +5301,7 @@ index 914d2da..6240e13 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6163,7 +6725,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6163,7 +6710,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView if (!onScroll2 || !scrollRef.current) { return; } @@ -5290,7 +5318,7 @@ index 914d2da..6240e13 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6218,14 +6788,29 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6218,14 +6773,29 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] ); React3.useLayoutEffect(() => { @@ -5320,7 +5348,7 @@ index 914d2da..6240e13 100644 if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6236,7 +6821,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6236,7 +6806,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } scrollEventCoalescer.cancel(); }; @@ -5338,7 +5366,7 @@ index 914d2da..6240e13 100644 React3.useEffect(() => { const doScroll = () => { if (contentOffset) { -@@ -6341,6 +6935,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6341,6 +6920,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView { className: scrollViewClassName, ref: scrollRef, @@ -5346,7 +5374,7 @@ index 914d2da..6240e13 100644 ...webProps, style: scrollViewStyle }, -@@ -6379,21 +6974,6 @@ function useValueListener$(key, callback) { +@@ -6379,21 +6959,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -5368,7 +5396,7 @@ index 914d2da..6240e13 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6411,8 +6991,6 @@ function ScrollAdjust() { +@@ -6411,8 +6976,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3__namespace.useRef(0); const lastScrollAdjustUserOffsetRef = React3__namespace.useRef(0); @@ -5377,7 +5405,7 @@ index 914d2da..6240e13 100644 const contentNodeRef = React3__namespace.useRef(null); const callback = React3__namespace.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6423,7 +7001,7 @@ function ScrollAdjust() { +@@ -6423,7 +6986,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -5386,7 +5414,7 @@ index 914d2da..6240e13 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6438,29 +7016,10 @@ function ScrollAdjust() { +@@ -6438,29 +7001,10 @@ function ScrollAdjust() { const nextScroll = currentScroll + scrollDelta; const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; if (needsTemporaryPadding) { @@ -5418,7 +5446,7 @@ index 914d2da..6240e13 100644 } else { scrollBy(); } -@@ -7645,10 +8204,10 @@ function useThrottleDebounce(mode) { +@@ -7645,10 +8189,10 @@ function useThrottleDebounce(mode) { const execute = React3.useCallback( (callback, delay, ...args) => { { @@ -5432,7 +5460,7 @@ index 914d2da..6240e13 100644 callback(...args); clearTimeoutRef(); } else { -@@ -7662,7 +8221,7 @@ function useThrottleDebounce(mode) { +@@ -7662,7 +8206,7 @@ function useThrottleDebounce(mode) { lastArgsRef.current = null; } }, @@ -5441,7 +5469,7 @@ index 914d2da..6240e13 100644 ); } } -@@ -8288,6 +8847,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8288,6 +8832,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -5450,7 +5478,7 @@ index 914d2da..6240e13 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs -index 95465f2..0005501 100644 +index 95465f2..b23f1f5 100644 --- a/node_modules/@legendapp/list/react-native.web.mjs +++ b/node_modules/@legendapp/list/react-native.web.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -6059,15 +6087,25 @@ index 95465f2..0005501 100644 // src/core/getStartOffsetAdjustment.ts function getStartOffsetAdjustment(ctx) { const { state } = ctx; -@@ -1226,45 +627,260 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { - if (viewOffset) { - offset -= viewOffset; +@@ -1213,58 +614,273 @@ function getItemSizeAtIndex(ctx, index) { + if (index === void 0 || index < 0) { + return void 0; } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; ++ } + if (index !== void 0) { + const startOffsetAdjustment = getStartOffsetAdjustment(ctx); + if (startOffsetAdjustment) { @@ -6277,22 +6315,7 @@ index 95465f2..0005501 100644 + if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { + state.isStartReached = false; + state.startReachedSnapshot = void 0; - } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } ++ } + set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); + set$(ctx, "isNearStart", scroll <= threshold); + const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; @@ -6319,19 +6342,50 @@ index 95465f2..0005501 100644 + state.startReachedSnapshot = snapshot; + } + ); - } -- return offset; ++ } } --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +- } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } +- } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } +- } +- return offset; +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+} -+ + } + +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { + var _a3, _b; @@ -6353,11 +6407,27 @@ index 95465f2..0005501 100644 } // src/core/finishScrollTo.ts -@@ -1401,7 +1017,182 @@ function listenForScrollEnd(ctx, params) { - } else { - idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); - } -- scheduledWork.register("platformScrollCompletion", cancel); +@@ -1392,16 +1008,191 @@ function listenForScrollEnd(ctx, params) { + if (idleTimeout !== void 0) { + clearTimeout(idleTimeout); + } +- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); +- }; +- const onScrollEnd = () => finish("scrollend"); +- target.addEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.addEventListener("scrollend", onScrollEnd); +- } else { +- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); ++ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); ++ }; ++ const onScrollEnd = () => finish("scrollend"); ++ target.addEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.addEventListener("scrollend", onScrollEnd); ++ } else { ++ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); ++ } + scheduledWork.register("platformScrollCompletion", cancel); +} + @@ -6533,7 +6603,8 @@ index 95465f2..0005501 100644 + } else { + resetAdaptiveRender(ctx); + } -+ } + } +- scheduledWork.register("platformScrollCompletion", cancel); } // src/core/doMaintainScrollAtEnd.ts @@ -7120,7 +7191,7 @@ index 95465f2..0005501 100644 function getWindowScrollPosition() { var _a3, _b, _c, _d; if (typeof window === "undefined") { -@@ -5982,9 +6153,213 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -5982,9 +6153,212 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll }; } @@ -7265,7 +7336,6 @@ index 95465f2..0005501 100644 var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; +var SMOOTH_SCROLL_MAX_MS = 2e3; -+var MAX_BORROW_VIEWPORTS = 3; +var BORROW_WATCH_FRAME_INTERVAL = 3; +var BORROW_MEASURE_ATTEMPTS = 2; +var USER_DRAG_SLOP = 8; @@ -7334,7 +7404,7 @@ index 95465f2..0005501 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6053,6 +6428,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6053,6 +6427,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -7343,14 +7413,10 @@ index 95465f2..0005501 100644 + (maxOffset) => Math.max(0, maxOffset - getTemporaryEndPadding(contentRef.current, paddingEndProp)), + [paddingEndProp] + ); -+ const getViewportExtent = useCallback(() => { -+ const layout = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); -+ return horizontal ? layout.width : layout.height; -+ }, [horizontal, isWindowScroll]); const getMaxScrollOffset = useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6073,8 +6457,180 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,8 +6452,170 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -7361,14 +7427,11 @@ index 95465f2..0005501 100644 + const animatedPaddingReleaseRef = useRef(void 0); + const withReachableExtent = useCallback( + (offset, maxOffset, animated, run) => { -+ var _a4, _b2, _c2; ++ var _a4; + const contentNode = contentRef.current; + (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); + const committedMaxOffset = getCommittedMaxScrollOffset(getMaxScrollOffset()); -+ const modelMaxOffset = Math.max(0, ((_b2 = ctx.state.totalSize) != null ? _b2 : 0) - ((_c2 = ctx.state.scrollLength) != null ? _c2 : 0)); -+ const viewportExtent = getViewportExtent(); -+ const reachLimit = modelMaxOffset > 0 || viewportExtent > 0 ? Math.max(modelMaxOffset, committedMaxOffset + MAX_BORROW_VIEWPORTS * viewportExtent) + SCROLL_EXTENT_EPSILON : Number.POSITIVE_INFINITY; -+ if (!contentNode || !Number.isFinite(offset) || offset > reachLimit) { ++ if (!contentNode || !Number.isFinite(offset)) { + run(clampOffset(offset, maxOffset)); + return; + } @@ -7441,14 +7504,7 @@ index 95465f2..0005501 100644 + scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finishIfArrived); + } + }, -+ [ -+ getCommittedMaxScrollOffset, -+ getCurrentScrollOffset, -+ getMaxScrollOffset, -+ getScrollTarget, -+ getViewportExtent, -+ paddingEndProp -+ ] ++ [getCommittedMaxScrollOffset, getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, paddingEndProp] + ); + useEffect( + () => () => { @@ -7531,7 +7587,7 @@ index 95465f2..0005501 100644 const scrollElement = scrollRef.current; const target = getScrollTarget(); if (!target || typeof target.scrollTo !== "function") { -@@ -6095,14 +6651,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6095,14 +6636,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); options.left = left; options.top = top; @@ -7558,7 +7614,7 @@ index 95465f2..0005501 100644 ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6691,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6128,8 +6676,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -7568,7 +7624,7 @@ index 95465f2..0005501 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6142,7 +6704,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6142,7 +6689,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ if (!onScroll2 || !scrollRef.current) { return; } @@ -7585,7 +7641,7 @@ index 95465f2..0005501 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6197,14 +6767,29 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6197,14 +6752,29 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] ); useLayoutEffect(() => { @@ -7615,7 +7671,7 @@ index 95465f2..0005501 100644 if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6215,7 +6800,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6215,7 +6785,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } scrollEventCoalescer.cancel(); }; @@ -7633,7 +7689,7 @@ index 95465f2..0005501 100644 useEffect(() => { const doScroll = () => { if (contentOffset) { -@@ -6320,6 +6914,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6320,6 +6899,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ { className: scrollViewClassName, ref: scrollRef, @@ -7641,7 +7697,7 @@ index 95465f2..0005501 100644 ...webProps, style: scrollViewStyle }, -@@ -6358,21 +6953,6 @@ function useValueListener$(key, callback) { +@@ -6358,21 +6938,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -7663,7 +7719,7 @@ index 95465f2..0005501 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6390,8 +6970,6 @@ function ScrollAdjust() { +@@ -6390,8 +6955,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3.useRef(0); const lastScrollAdjustUserOffsetRef = React3.useRef(0); @@ -7672,7 +7728,7 @@ index 95465f2..0005501 100644 const contentNodeRef = React3.useRef(null); const callback = React3.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6402,7 +6980,7 @@ function ScrollAdjust() { +@@ -6402,7 +6965,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -7681,7 +7737,7 @@ index 95465f2..0005501 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6417,29 +6995,10 @@ function ScrollAdjust() { +@@ -6417,29 +6980,10 @@ function ScrollAdjust() { const nextScroll = currentScroll + scrollDelta; const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; if (needsTemporaryPadding) { @@ -7713,7 +7769,7 @@ index 95465f2..0005501 100644 } else { scrollBy(); } -@@ -7624,10 +8183,10 @@ function useThrottleDebounce(mode) { +@@ -7624,10 +8168,10 @@ function useThrottleDebounce(mode) { const execute = useCallback( (callback, delay, ...args) => { { @@ -7727,7 +7783,7 @@ index 95465f2..0005501 100644 callback(...args); clearTimeoutRef(); } else { -@@ -7641,7 +8200,7 @@ function useThrottleDebounce(mode) { +@@ -7641,7 +8185,7 @@ function useThrottleDebounce(mode) { lastArgsRef.current = null; } }, @@ -7736,7 +7792,7 @@ index 95465f2..0005501 100644 ); } } -@@ -8267,6 +8826,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8267,6 +8811,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -7745,7 +7801,7 @@ index 95465f2..0005501 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js -index 914d2da..6240e13 100644 +index 914d2da..da93b27 100644 --- a/node_modules/@legendapp/list/react.js +++ b/node_modules/@legendapp/list/react.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -8354,15 +8410,25 @@ index 914d2da..6240e13 100644 // src/core/getStartOffsetAdjustment.ts function getStartOffsetAdjustment(ctx) { const { state } = ctx; -@@ -1247,45 +648,260 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { - if (viewOffset) { - offset -= viewOffset; +@@ -1234,58 +635,273 @@ function getItemSizeAtIndex(ctx, index) { + if (index === void 0 || index < 0) { + return void 0; } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; ++ } + if (index !== void 0) { + const startOffsetAdjustment = getStartOffsetAdjustment(ctx); + if (startOffsetAdjustment) { @@ -8572,22 +8638,7 @@ index 914d2da..6240e13 100644 + if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { + state.isStartReached = false; + state.startReachedSnapshot = void 0; - } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } ++ } + set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); + set$(ctx, "isNearStart", scroll <= threshold); + const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; @@ -8614,19 +8665,50 @@ index 914d2da..6240e13 100644 + state.startReachedSnapshot = snapshot; + } + ); - } -- return offset; ++ } } --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +- } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } +- } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } +- } +- return offset; +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+} -+ + } + +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { + var _a3, _b; @@ -8648,11 +8730,27 @@ index 914d2da..6240e13 100644 } // src/core/finishScrollTo.ts -@@ -1422,7 +1038,182 @@ function listenForScrollEnd(ctx, params) { - } else { - idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); - } -- scheduledWork.register("platformScrollCompletion", cancel); +@@ -1413,16 +1029,191 @@ function listenForScrollEnd(ctx, params) { + if (idleTimeout !== void 0) { + clearTimeout(idleTimeout); + } +- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); +- }; +- const onScrollEnd = () => finish("scrollend"); +- target.addEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.addEventListener("scrollend", onScrollEnd); +- } else { +- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); ++ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); ++ }; ++ const onScrollEnd = () => finish("scrollend"); ++ target.addEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.addEventListener("scrollend", onScrollEnd); ++ } else { ++ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); ++ } + scheduledWork.register("platformScrollCompletion", cancel); +} + @@ -8828,7 +8926,8 @@ index 914d2da..6240e13 100644 + } else { + resetAdaptiveRender(ctx); + } -+ } + } +- scheduledWork.register("platformScrollCompletion", cancel); } // src/core/doMaintainScrollAtEnd.ts @@ -9415,7 +9514,7 @@ index 914d2da..6240e13 100644 function getWindowScrollPosition() { var _a3, _b, _c, _d; if (typeof window === "undefined") { -@@ -6003,9 +6174,213 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -6003,9 +6174,212 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll }; } @@ -9560,7 +9659,6 @@ index 914d2da..6240e13 100644 var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; +var SMOOTH_SCROLL_MAX_MS = 2e3; -+var MAX_BORROW_VIEWPORTS = 3; +var BORROW_WATCH_FRAME_INTERVAL = 3; +var BORROW_MEASURE_ATTEMPTS = 2; +var USER_DRAG_SLOP = 8; @@ -9629,7 +9727,7 @@ index 914d2da..6240e13 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6074,6 +6449,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6074,6 +6448,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -9638,14 +9736,10 @@ index 914d2da..6240e13 100644 + (maxOffset) => Math.max(0, maxOffset - getTemporaryEndPadding(contentRef.current, paddingEndProp)), + [paddingEndProp] + ); -+ const getViewportExtent = React3.useCallback(() => { -+ const layout = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); -+ return horizontal ? layout.width : layout.height; -+ }, [horizontal, isWindowScroll]); const getMaxScrollOffset = React3.useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6094,8 +6478,180 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,8 +6473,170 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -9656,14 +9750,11 @@ index 914d2da..6240e13 100644 + const animatedPaddingReleaseRef = React3.useRef(void 0); + const withReachableExtent = React3.useCallback( + (offset, maxOffset, animated, run) => { -+ var _a4, _b2, _c2; ++ var _a4; + const contentNode = contentRef.current; + (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); + const committedMaxOffset = getCommittedMaxScrollOffset(getMaxScrollOffset()); -+ const modelMaxOffset = Math.max(0, ((_b2 = ctx.state.totalSize) != null ? _b2 : 0) - ((_c2 = ctx.state.scrollLength) != null ? _c2 : 0)); -+ const viewportExtent = getViewportExtent(); -+ const reachLimit = modelMaxOffset > 0 || viewportExtent > 0 ? Math.max(modelMaxOffset, committedMaxOffset + MAX_BORROW_VIEWPORTS * viewportExtent) + SCROLL_EXTENT_EPSILON : Number.POSITIVE_INFINITY; -+ if (!contentNode || !Number.isFinite(offset) || offset > reachLimit) { ++ if (!contentNode || !Number.isFinite(offset)) { + run(clampOffset(offset, maxOffset)); + return; + } @@ -9736,14 +9827,7 @@ index 914d2da..6240e13 100644 + scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finishIfArrived); + } + }, -+ [ -+ getCommittedMaxScrollOffset, -+ getCurrentScrollOffset, -+ getMaxScrollOffset, -+ getScrollTarget, -+ getViewportExtent, -+ paddingEndProp -+ ] ++ [getCommittedMaxScrollOffset, getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, paddingEndProp] + ); + React3.useEffect( + () => () => { @@ -9826,7 +9910,7 @@ index 914d2da..6240e13 100644 const scrollElement = scrollRef.current; const target = getScrollTarget(); if (!target || typeof target.scrollTo !== "function") { -@@ -6116,14 +6672,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6116,14 +6657,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); options.left = left; options.top = top; @@ -9853,7 +9937,7 @@ index 914d2da..6240e13 100644 ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6712,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6149,8 +6697,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -9863,7 +9947,7 @@ index 914d2da..6240e13 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6163,7 +6725,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6163,7 +6710,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView if (!onScroll2 || !scrollRef.current) { return; } @@ -9880,7 +9964,7 @@ index 914d2da..6240e13 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6218,14 +6788,29 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6218,14 +6773,29 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] ); React3.useLayoutEffect(() => { @@ -9910,7 +9994,7 @@ index 914d2da..6240e13 100644 if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6236,7 +6821,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6236,7 +6806,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } scrollEventCoalescer.cancel(); }; @@ -9928,7 +10012,7 @@ index 914d2da..6240e13 100644 React3.useEffect(() => { const doScroll = () => { if (contentOffset) { -@@ -6341,6 +6935,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6341,6 +6920,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView { className: scrollViewClassName, ref: scrollRef, @@ -9936,7 +10020,7 @@ index 914d2da..6240e13 100644 ...webProps, style: scrollViewStyle }, -@@ -6379,21 +6974,6 @@ function useValueListener$(key, callback) { +@@ -6379,21 +6959,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -9958,7 +10042,7 @@ index 914d2da..6240e13 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6411,8 +6991,6 @@ function ScrollAdjust() { +@@ -6411,8 +6976,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3__namespace.useRef(0); const lastScrollAdjustUserOffsetRef = React3__namespace.useRef(0); @@ -9967,7 +10051,7 @@ index 914d2da..6240e13 100644 const contentNodeRef = React3__namespace.useRef(null); const callback = React3__namespace.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6423,7 +7001,7 @@ function ScrollAdjust() { +@@ -6423,7 +6986,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -9976,7 +10060,7 @@ index 914d2da..6240e13 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6438,29 +7016,10 @@ function ScrollAdjust() { +@@ -6438,29 +7001,10 @@ function ScrollAdjust() { const nextScroll = currentScroll + scrollDelta; const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; if (needsTemporaryPadding) { @@ -10008,7 +10092,7 @@ index 914d2da..6240e13 100644 } else { scrollBy(); } -@@ -7645,10 +8204,10 @@ function useThrottleDebounce(mode) { +@@ -7645,10 +8189,10 @@ function useThrottleDebounce(mode) { const execute = React3.useCallback( (callback, delay, ...args) => { { @@ -10022,7 +10106,7 @@ index 914d2da..6240e13 100644 callback(...args); clearTimeoutRef(); } else { -@@ -7662,7 +8221,7 @@ function useThrottleDebounce(mode) { +@@ -7662,7 +8206,7 @@ function useThrottleDebounce(mode) { lastArgsRef.current = null; } }, @@ -10031,7 +10115,7 @@ index 914d2da..6240e13 100644 ); } } -@@ -8288,6 +8847,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8288,6 +8832,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -10040,7 +10124,7 @@ index 914d2da..6240e13 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs -index 95465f2..0005501 100644 +index 95465f2..b23f1f5 100644 --- a/node_modules/@legendapp/list/react.mjs +++ b/node_modules/@legendapp/list/react.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -10649,15 +10733,25 @@ index 95465f2..0005501 100644 // src/core/getStartOffsetAdjustment.ts function getStartOffsetAdjustment(ctx) { const { state } = ctx; -@@ -1226,45 +627,260 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { - if (viewOffset) { - offset -= viewOffset; +@@ -1213,58 +614,273 @@ function getItemSizeAtIndex(ctx, index) { + if (index === void 0 || index < 0) { + return void 0; } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; ++ } + if (index !== void 0) { + const startOffsetAdjustment = getStartOffsetAdjustment(ctx); + if (startOffsetAdjustment) { @@ -10867,22 +10961,7 @@ index 95465f2..0005501 100644 + if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { + state.isStartReached = false; + state.startReachedSnapshot = void 0; - } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } ++ } + set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); + set$(ctx, "isNearStart", scroll <= threshold); + const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; @@ -10909,19 +10988,50 @@ index 95465f2..0005501 100644 + state.startReachedSnapshot = snapshot; + } + ); - } -- return offset; ++ } } --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +- } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } +- } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } +- } +- return offset; +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+} -+ + } + +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { + var _a3, _b; @@ -10943,11 +11053,27 @@ index 95465f2..0005501 100644 } // src/core/finishScrollTo.ts -@@ -1401,7 +1017,182 @@ function listenForScrollEnd(ctx, params) { - } else { - idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); - } -- scheduledWork.register("platformScrollCompletion", cancel); +@@ -1392,16 +1008,191 @@ function listenForScrollEnd(ctx, params) { + if (idleTimeout !== void 0) { + clearTimeout(idleTimeout); + } +- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); +- }; +- const onScrollEnd = () => finish("scrollend"); +- target.addEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.addEventListener("scrollend", onScrollEnd); +- } else { +- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); ++ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); ++ }; ++ const onScrollEnd = () => finish("scrollend"); ++ target.addEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.addEventListener("scrollend", onScrollEnd); ++ } else { ++ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); ++ } + scheduledWork.register("platformScrollCompletion", cancel); +} + @@ -11123,7 +11249,8 @@ index 95465f2..0005501 100644 + } else { + resetAdaptiveRender(ctx); + } -+ } + } +- scheduledWork.register("platformScrollCompletion", cancel); } // src/core/doMaintainScrollAtEnd.ts @@ -11710,7 +11837,7 @@ index 95465f2..0005501 100644 function getWindowScrollPosition() { var _a3, _b, _c, _d; if (typeof window === "undefined") { -@@ -5982,9 +6153,213 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -5982,9 +6153,212 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll }; } @@ -11855,7 +11982,6 @@ index 95465f2..0005501 100644 var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; +var SMOOTH_SCROLL_MAX_MS = 2e3; -+var MAX_BORROW_VIEWPORTS = 3; +var BORROW_WATCH_FRAME_INTERVAL = 3; +var BORROW_MEASURE_ATTEMPTS = 2; +var USER_DRAG_SLOP = 8; @@ -11924,7 +12050,7 @@ index 95465f2..0005501 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6053,6 +6428,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6053,6 +6427,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -11933,14 +12059,10 @@ index 95465f2..0005501 100644 + (maxOffset) => Math.max(0, maxOffset - getTemporaryEndPadding(contentRef.current, paddingEndProp)), + [paddingEndProp] + ); -+ const getViewportExtent = useCallback(() => { -+ const layout = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); -+ return horizontal ? layout.width : layout.height; -+ }, [horizontal, isWindowScroll]); const getMaxScrollOffset = useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6073,8 +6457,180 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,8 +6452,170 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -11951,14 +12073,11 @@ index 95465f2..0005501 100644 + const animatedPaddingReleaseRef = useRef(void 0); + const withReachableExtent = useCallback( + (offset, maxOffset, animated, run) => { -+ var _a4, _b2, _c2; ++ var _a4; + const contentNode = contentRef.current; + (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); + const committedMaxOffset = getCommittedMaxScrollOffset(getMaxScrollOffset()); -+ const modelMaxOffset = Math.max(0, ((_b2 = ctx.state.totalSize) != null ? _b2 : 0) - ((_c2 = ctx.state.scrollLength) != null ? _c2 : 0)); -+ const viewportExtent = getViewportExtent(); -+ const reachLimit = modelMaxOffset > 0 || viewportExtent > 0 ? Math.max(modelMaxOffset, committedMaxOffset + MAX_BORROW_VIEWPORTS * viewportExtent) + SCROLL_EXTENT_EPSILON : Number.POSITIVE_INFINITY; -+ if (!contentNode || !Number.isFinite(offset) || offset > reachLimit) { ++ if (!contentNode || !Number.isFinite(offset)) { + run(clampOffset(offset, maxOffset)); + return; + } @@ -12031,14 +12150,7 @@ index 95465f2..0005501 100644 + scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finishIfArrived); + } + }, -+ [ -+ getCommittedMaxScrollOffset, -+ getCurrentScrollOffset, -+ getMaxScrollOffset, -+ getScrollTarget, -+ getViewportExtent, -+ paddingEndProp -+ ] ++ [getCommittedMaxScrollOffset, getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, paddingEndProp] + ); + useEffect( + () => () => { @@ -12121,7 +12233,7 @@ index 95465f2..0005501 100644 const scrollElement = scrollRef.current; const target = getScrollTarget(); if (!target || typeof target.scrollTo !== "function") { -@@ -6095,14 +6651,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6095,14 +6636,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); options.left = left; options.top = top; @@ -12148,7 +12260,7 @@ index 95465f2..0005501 100644 ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6691,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6128,8 +6676,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -12158,7 +12270,7 @@ index 95465f2..0005501 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6142,7 +6704,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6142,7 +6689,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ if (!onScroll2 || !scrollRef.current) { return; } @@ -12175,7 +12287,7 @@ index 95465f2..0005501 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6197,14 +6767,29 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6197,14 +6752,29 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] ); useLayoutEffect(() => { @@ -12205,7 +12317,7 @@ index 95465f2..0005501 100644 if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6215,7 +6800,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6215,7 +6785,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } scrollEventCoalescer.cancel(); }; @@ -12223,7 +12335,7 @@ index 95465f2..0005501 100644 useEffect(() => { const doScroll = () => { if (contentOffset) { -@@ -6320,6 +6914,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6320,6 +6899,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ { className: scrollViewClassName, ref: scrollRef, @@ -12231,7 +12343,7 @@ index 95465f2..0005501 100644 ...webProps, style: scrollViewStyle }, -@@ -6358,21 +6953,6 @@ function useValueListener$(key, callback) { +@@ -6358,21 +6938,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -12253,7 +12365,7 @@ index 95465f2..0005501 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6390,8 +6970,6 @@ function ScrollAdjust() { +@@ -6390,8 +6955,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3.useRef(0); const lastScrollAdjustUserOffsetRef = React3.useRef(0); @@ -12262,7 +12374,7 @@ index 95465f2..0005501 100644 const contentNodeRef = React3.useRef(null); const callback = React3.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6402,7 +6980,7 @@ function ScrollAdjust() { +@@ -6402,7 +6965,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -12271,7 +12383,7 @@ index 95465f2..0005501 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6417,29 +6995,10 @@ function ScrollAdjust() { +@@ -6417,29 +6980,10 @@ function ScrollAdjust() { const nextScroll = currentScroll + scrollDelta; const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; if (needsTemporaryPadding) { @@ -12303,7 +12415,7 @@ index 95465f2..0005501 100644 } else { scrollBy(); } -@@ -7624,10 +8183,10 @@ function useThrottleDebounce(mode) { +@@ -7624,10 +8168,10 @@ function useThrottleDebounce(mode) { const execute = useCallback( (callback, delay, ...args) => { { @@ -12317,7 +12429,7 @@ index 95465f2..0005501 100644 callback(...args); clearTimeoutRef(); } else { -@@ -7641,7 +8200,7 @@ function useThrottleDebounce(mode) { +@@ -7641,7 +8185,7 @@ function useThrottleDebounce(mode) { lastArgsRef.current = null; } }, @@ -12326,7 +12438,7 @@ index 95465f2..0005501 100644 ); } } -@@ -8267,6 +8826,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8267,6 +8811,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); From 00510ec5ab6ba7a3ad2587a2b0edf1807559d3cb Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 12 Aug 2026 10:53:03 -0400 Subject: [PATCH 17/38] test(e2e): fix what review found in the harness, and read the hit count honestly Four findings from a review of the retries and reset work, all cases where a test could mislead rather than fail: people-profile sent an iOS predicate through a scoped query, which uiautomator2 does not understand. On Android that throws into an isExisting() catch, the flow returns early, and the test passes having asserted nothing - a permanent silent green. There is now a byTextWithin helper with the same platform split byText already had, escaping included. Its retries are gone too: the stated reason was a slow feed, but nothing waited on feed content (the container mounts empty and immediately), so slowness never failed - it bailed green. A bounded wait on the row itself replaces two full re-runs, and says in the log when it bails. dismissKeyboard tapped the content above the composer. The chat list sets keyboardShouldPersistTaps="handled", so a tap landing on a row is handled by that row - the keyboard stays up AND the row does whatever it does, which in this account can mean opening an attachment or following a link out of the app. It drags now, which is the gesture keyboardDismissMode="on-drag" listens for and which cannot activate a touchable. It also logs when the keyboard survives, rather than leaving escapeToTabs to burn its budget with nothing in the log. The reset's dismiss-before-atTabs loop waited via settleAfter, which is defined in terms of atTabs - the predicate that lies while a modal is up - so it returned instantly and the loop gave up after one iteration. It waits on the control itself now. It also took the first matching control, but a partly-covering sheet leaves the background's controls earlier in the tree, so the first match can be clicked through the sheet; it takes the last. And its predicate matched Done/Close/Cancel as substrings, which would make a future "Close team" an unattended destructive click in the reset - it requires an exact name on a button or menu item now. chat-search-hit read "N of M" by matching " of " across the whole screen, with the thread's own message text behind the bar and 'one' chosen because it is common. A message body could win the match and either accuse the app of finding no hits or silently stop the wrap-around from being exercised. The counter carries a testID on both platforms now, read through the text node inside it, and says what it saw when it cannot parse a count. Also updates the legend-list patch: ScrollAdjust now measures the content without room the scroll view is borrowing on the same node, and a settling target's corrections no longer refresh the wheel-momentum grace that is meant to let a user interrupt them. iPhone suite: 61 passing, no retries consumed. chat-search-hit 3/3 on device with the new counter path. --- shared/chat/conversation/search.tsx | 18 +- shared/patches/@legendapp+list+3.3.5.patch | 160 +++++++++++------- .../ios-appium/flows/chat-search-hit.test.ts | 26 ++- .../ios-appium/flows/people-profile.test.ts | 26 +-- .../tests/e2e/ios-appium/helpers/elements.ts | 11 ++ .../tests/e2e/ios-appium/helpers/navigate.ts | 69 +++++--- shared/tests/e2e/shared/test-ids.ts | 3 + 7 files changed, 210 insertions(+), 103 deletions(-) diff --git a/shared/chat/conversation/search.tsx b/shared/chat/conversation/search.tsx index cf24eebc89a1..824102862eb1 100644 --- a/shared/chat/conversation/search.tsx +++ b/shared/chat/conversation/search.tsx @@ -435,8 +435,15 @@ const ThreadSearchDesktopInner = function ThreadSearchDesktopInner(p: CommonProp {inProgress && } + {/* collapsable={false}: Android view flattening would drop this testID'd wrapper and + leave the count unreadable to the e2e suite. */} {hasResults && ( - + {noResults ? 'No results' : `${selectedIndex + 1} of ${hits.length}`} @@ -543,8 +550,15 @@ const ThreadSearchMobileInner = function ThreadSearchMobileInner(p: CommonProps) {inProgress && } + {/* collapsable={false}: Android view flattening would drop this testID'd wrapper and + leave the count unreadable to the e2e suite. */} {hasResults && ( - + {status === 'done' && numHits === 0 ? 'No results' : `${selectedIndex + 1} of ${numHits}`} diff --git a/shared/patches/@legendapp+list+3.3.5.patch b/shared/patches/@legendapp+list+3.3.5.patch index 0ef795031e79..98fdb7510fda 100644 --- a/shared/patches/@legendapp+list+3.3.5.patch +++ b/shared/patches/@legendapp+list+3.3.5.patch @@ -3155,7 +3155,7 @@ index 40e87cd..b9895df 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js -index 914d2da..da93b27 100644 +index 914d2da..d73d16b 100644 --- a/node_modules/@legendapp/list/react-native.web.js +++ b/node_modules/@legendapp/list/react-native.web.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -5093,7 +5093,7 @@ index 914d2da..da93b27 100644 const getMaxScrollOffset = React3.useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6094,8 +6473,170 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,8 +6473,172 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -5260,11 +5260,13 @@ index 914d2da..da93b27 100644 + ); const scrollToLocalOffset = React3.useCallback( (offset, animated) => { -+ interactionArmedAtRef.current = now(); ++ if (!ctx.state.scrollTargetSettle) { ++ interactionArmedAtRef.current = now(); ++ } const scrollElement = scrollRef.current; const target = getScrollTarget(); if (!target || typeof target.scrollTo !== "function") { -@@ -6116,14 +6657,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6116,14 +6659,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); options.left = left; options.top = top; @@ -5291,7 +5293,7 @@ index 914d2da..da93b27 100644 ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6697,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6149,8 +6699,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -5301,7 +5303,7 @@ index 914d2da..da93b27 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6163,7 +6710,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6163,7 +6712,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView if (!onScroll2 || !scrollRef.current) { return; } @@ -5318,7 +5320,7 @@ index 914d2da..da93b27 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6218,14 +6773,29 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6218,14 +6775,29 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] ); React3.useLayoutEffect(() => { @@ -5348,7 +5350,7 @@ index 914d2da..da93b27 100644 if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6236,7 +6806,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6236,7 +6808,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } scrollEventCoalescer.cancel(); }; @@ -5366,7 +5368,7 @@ index 914d2da..da93b27 100644 React3.useEffect(() => { const doScroll = () => { if (contentOffset) { -@@ -6341,6 +6920,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6341,6 +6922,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView { className: scrollViewClassName, ref: scrollRef, @@ -5374,7 +5376,7 @@ index 914d2da..da93b27 100644 ...webProps, style: scrollViewStyle }, -@@ -6379,21 +6959,6 @@ function useValueListener$(key, callback) { +@@ -6379,21 +6961,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -5396,7 +5398,7 @@ index 914d2da..da93b27 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6411,8 +6976,6 @@ function ScrollAdjust() { +@@ -6411,8 +6978,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3__namespace.useRef(0); const lastScrollAdjustUserOffsetRef = React3__namespace.useRef(0); @@ -5405,7 +5407,7 @@ index 914d2da..da93b27 100644 const contentNodeRef = React3__namespace.useRef(null); const callback = React3__namespace.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6423,7 +6986,7 @@ function ScrollAdjust() { +@@ -6423,7 +6988,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -5414,7 +5416,13 @@ index 914d2da..da93b27 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6438,29 +7001,10 @@ function ScrollAdjust() { +@@ -6433,34 +6998,15 @@ function ScrollAdjust() { + const scrollBy = () => scrollAdjustBy(el, axis.x * scrollDelta, axis.y * scrollDelta); + contentNodeRef.current = contentNode; + if (shouldScroll && contentNode) { +- const totalSize = contentNode[axis.contentSizeKey]; ++ const totalSize = contentNode[axis.contentSizeKey] - getTemporaryEndPadding(contentNode, axis.paddingEndProp); + const viewportSize = el[axis.viewportSizeKey]; const nextScroll = currentScroll + scrollDelta; const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; if (needsTemporaryPadding) { @@ -5446,7 +5454,7 @@ index 914d2da..da93b27 100644 } else { scrollBy(); } -@@ -7645,10 +8189,10 @@ function useThrottleDebounce(mode) { +@@ -7645,10 +8191,10 @@ function useThrottleDebounce(mode) { const execute = React3.useCallback( (callback, delay, ...args) => { { @@ -5460,7 +5468,7 @@ index 914d2da..da93b27 100644 callback(...args); clearTimeoutRef(); } else { -@@ -7662,7 +8206,7 @@ function useThrottleDebounce(mode) { +@@ -7662,7 +8208,7 @@ function useThrottleDebounce(mode) { lastArgsRef.current = null; } }, @@ -5469,7 +5477,7 @@ index 914d2da..da93b27 100644 ); } } -@@ -8288,6 +8832,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8288,6 +8834,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -5478,7 +5486,7 @@ index 914d2da..da93b27 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs -index 95465f2..b23f1f5 100644 +index 95465f2..5e38834 100644 --- a/node_modules/@legendapp/list/react-native.web.mjs +++ b/node_modules/@legendapp/list/react-native.web.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -7416,7 +7424,7 @@ index 95465f2..b23f1f5 100644 const getMaxScrollOffset = useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6073,8 +6452,170 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,8 +6452,172 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -7583,11 +7591,13 @@ index 95465f2..b23f1f5 100644 + ); const scrollToLocalOffset = useCallback( (offset, animated) => { -+ interactionArmedAtRef.current = now(); ++ if (!ctx.state.scrollTargetSettle) { ++ interactionArmedAtRef.current = now(); ++ } const scrollElement = scrollRef.current; const target = getScrollTarget(); if (!target || typeof target.scrollTo !== "function") { -@@ -6095,14 +6636,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6095,14 +6638,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); options.left = left; options.top = top; @@ -7614,7 +7624,7 @@ index 95465f2..b23f1f5 100644 ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6676,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6128,8 +6678,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -7624,7 +7634,7 @@ index 95465f2..b23f1f5 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6142,7 +6689,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6142,7 +6691,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ if (!onScroll2 || !scrollRef.current) { return; } @@ -7641,7 +7651,7 @@ index 95465f2..b23f1f5 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6197,14 +6752,29 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6197,14 +6754,29 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] ); useLayoutEffect(() => { @@ -7671,7 +7681,7 @@ index 95465f2..b23f1f5 100644 if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6215,7 +6785,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6215,7 +6787,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } scrollEventCoalescer.cancel(); }; @@ -7689,7 +7699,7 @@ index 95465f2..b23f1f5 100644 useEffect(() => { const doScroll = () => { if (contentOffset) { -@@ -6320,6 +6899,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6320,6 +6901,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ { className: scrollViewClassName, ref: scrollRef, @@ -7697,7 +7707,7 @@ index 95465f2..b23f1f5 100644 ...webProps, style: scrollViewStyle }, -@@ -6358,21 +6938,6 @@ function useValueListener$(key, callback) { +@@ -6358,21 +6940,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -7719,7 +7729,7 @@ index 95465f2..b23f1f5 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6390,8 +6955,6 @@ function ScrollAdjust() { +@@ -6390,8 +6957,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3.useRef(0); const lastScrollAdjustUserOffsetRef = React3.useRef(0); @@ -7728,7 +7738,7 @@ index 95465f2..b23f1f5 100644 const contentNodeRef = React3.useRef(null); const callback = React3.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6402,7 +6965,7 @@ function ScrollAdjust() { +@@ -6402,7 +6967,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -7737,7 +7747,13 @@ index 95465f2..b23f1f5 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6417,29 +6980,10 @@ function ScrollAdjust() { +@@ -6412,34 +6977,15 @@ function ScrollAdjust() { + const scrollBy = () => scrollAdjustBy(el, axis.x * scrollDelta, axis.y * scrollDelta); + contentNodeRef.current = contentNode; + if (shouldScroll && contentNode) { +- const totalSize = contentNode[axis.contentSizeKey]; ++ const totalSize = contentNode[axis.contentSizeKey] - getTemporaryEndPadding(contentNode, axis.paddingEndProp); + const viewportSize = el[axis.viewportSizeKey]; const nextScroll = currentScroll + scrollDelta; const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; if (needsTemporaryPadding) { @@ -7769,7 +7785,7 @@ index 95465f2..b23f1f5 100644 } else { scrollBy(); } -@@ -7624,10 +8168,10 @@ function useThrottleDebounce(mode) { +@@ -7624,10 +8170,10 @@ function useThrottleDebounce(mode) { const execute = useCallback( (callback, delay, ...args) => { { @@ -7783,7 +7799,7 @@ index 95465f2..b23f1f5 100644 callback(...args); clearTimeoutRef(); } else { -@@ -7641,7 +8185,7 @@ function useThrottleDebounce(mode) { +@@ -7641,7 +8187,7 @@ function useThrottleDebounce(mode) { lastArgsRef.current = null; } }, @@ -7792,7 +7808,7 @@ index 95465f2..b23f1f5 100644 ); } } -@@ -8267,6 +8811,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8267,6 +8813,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -7801,7 +7817,7 @@ index 95465f2..b23f1f5 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js -index 914d2da..da93b27 100644 +index 914d2da..d73d16b 100644 --- a/node_modules/@legendapp/list/react.js +++ b/node_modules/@legendapp/list/react.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -9739,7 +9755,7 @@ index 914d2da..da93b27 100644 const getMaxScrollOffset = React3.useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6094,8 +6473,170 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,8 +6473,172 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -9906,11 +9922,13 @@ index 914d2da..da93b27 100644 + ); const scrollToLocalOffset = React3.useCallback( (offset, animated) => { -+ interactionArmedAtRef.current = now(); ++ if (!ctx.state.scrollTargetSettle) { ++ interactionArmedAtRef.current = now(); ++ } const scrollElement = scrollRef.current; const target = getScrollTarget(); if (!target || typeof target.scrollTo !== "function") { -@@ -6116,14 +6657,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6116,14 +6659,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); options.left = left; options.top = top; @@ -9937,7 +9955,7 @@ index 914d2da..da93b27 100644 ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6697,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6149,8 +6699,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -9947,7 +9965,7 @@ index 914d2da..da93b27 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6163,7 +6710,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6163,7 +6712,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView if (!onScroll2 || !scrollRef.current) { return; } @@ -9964,7 +9982,7 @@ index 914d2da..da93b27 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6218,14 +6773,29 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6218,14 +6775,29 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] ); React3.useLayoutEffect(() => { @@ -9994,7 +10012,7 @@ index 914d2da..da93b27 100644 if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6236,7 +6806,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6236,7 +6808,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } scrollEventCoalescer.cancel(); }; @@ -10012,7 +10030,7 @@ index 914d2da..da93b27 100644 React3.useEffect(() => { const doScroll = () => { if (contentOffset) { -@@ -6341,6 +6920,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6341,6 +6922,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView { className: scrollViewClassName, ref: scrollRef, @@ -10020,7 +10038,7 @@ index 914d2da..da93b27 100644 ...webProps, style: scrollViewStyle }, -@@ -6379,21 +6959,6 @@ function useValueListener$(key, callback) { +@@ -6379,21 +6961,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -10042,7 +10060,7 @@ index 914d2da..da93b27 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6411,8 +6976,6 @@ function ScrollAdjust() { +@@ -6411,8 +6978,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3__namespace.useRef(0); const lastScrollAdjustUserOffsetRef = React3__namespace.useRef(0); @@ -10051,7 +10069,7 @@ index 914d2da..da93b27 100644 const contentNodeRef = React3__namespace.useRef(null); const callback = React3__namespace.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6423,7 +6986,7 @@ function ScrollAdjust() { +@@ -6423,7 +6988,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -10060,7 +10078,13 @@ index 914d2da..da93b27 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6438,29 +7001,10 @@ function ScrollAdjust() { +@@ -6433,34 +6998,15 @@ function ScrollAdjust() { + const scrollBy = () => scrollAdjustBy(el, axis.x * scrollDelta, axis.y * scrollDelta); + contentNodeRef.current = contentNode; + if (shouldScroll && contentNode) { +- const totalSize = contentNode[axis.contentSizeKey]; ++ const totalSize = contentNode[axis.contentSizeKey] - getTemporaryEndPadding(contentNode, axis.paddingEndProp); + const viewportSize = el[axis.viewportSizeKey]; const nextScroll = currentScroll + scrollDelta; const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; if (needsTemporaryPadding) { @@ -10092,7 +10116,7 @@ index 914d2da..da93b27 100644 } else { scrollBy(); } -@@ -7645,10 +8189,10 @@ function useThrottleDebounce(mode) { +@@ -7645,10 +8191,10 @@ function useThrottleDebounce(mode) { const execute = React3.useCallback( (callback, delay, ...args) => { { @@ -10106,7 +10130,7 @@ index 914d2da..da93b27 100644 callback(...args); clearTimeoutRef(); } else { -@@ -7662,7 +8206,7 @@ function useThrottleDebounce(mode) { +@@ -7662,7 +8208,7 @@ function useThrottleDebounce(mode) { lastArgsRef.current = null; } }, @@ -10115,7 +10139,7 @@ index 914d2da..da93b27 100644 ); } } -@@ -8288,6 +8832,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8288,6 +8834,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -10124,7 +10148,7 @@ index 914d2da..da93b27 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs -index 95465f2..b23f1f5 100644 +index 95465f2..5e38834 100644 --- a/node_modules/@legendapp/list/react.mjs +++ b/node_modules/@legendapp/list/react.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -12062,7 +12086,7 @@ index 95465f2..b23f1f5 100644 const getMaxScrollOffset = useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6073,8 +6452,170 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,8 +6452,172 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -12229,11 +12253,13 @@ index 95465f2..b23f1f5 100644 + ); const scrollToLocalOffset = useCallback( (offset, animated) => { -+ interactionArmedAtRef.current = now(); ++ if (!ctx.state.scrollTargetSettle) { ++ interactionArmedAtRef.current = now(); ++ } const scrollElement = scrollRef.current; const target = getScrollTarget(); if (!target || typeof target.scrollTo !== "function") { -@@ -6095,14 +6636,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6095,14 +6638,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); options.left = left; options.top = top; @@ -12260,7 +12286,7 @@ index 95465f2..b23f1f5 100644 ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6676,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6128,8 +6678,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -12270,7 +12296,7 @@ index 95465f2..b23f1f5 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6142,7 +6689,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6142,7 +6691,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ if (!onScroll2 || !scrollRef.current) { return; } @@ -12287,7 +12313,7 @@ index 95465f2..b23f1f5 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6197,14 +6752,29 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6197,14 +6754,29 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] ); useLayoutEffect(() => { @@ -12317,7 +12343,7 @@ index 95465f2..b23f1f5 100644 if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6215,7 +6785,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6215,7 +6787,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } scrollEventCoalescer.cancel(); }; @@ -12335,7 +12361,7 @@ index 95465f2..b23f1f5 100644 useEffect(() => { const doScroll = () => { if (contentOffset) { -@@ -6320,6 +6899,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6320,6 +6901,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ { className: scrollViewClassName, ref: scrollRef, @@ -12343,7 +12369,7 @@ index 95465f2..b23f1f5 100644 ...webProps, style: scrollViewStyle }, -@@ -6358,21 +6938,6 @@ function useValueListener$(key, callback) { +@@ -6358,21 +6940,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -12365,7 +12391,7 @@ index 95465f2..b23f1f5 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6390,8 +6955,6 @@ function ScrollAdjust() { +@@ -6390,8 +6957,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3.useRef(0); const lastScrollAdjustUserOffsetRef = React3.useRef(0); @@ -12374,7 +12400,7 @@ index 95465f2..b23f1f5 100644 const contentNodeRef = React3.useRef(null); const callback = React3.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6402,7 +6965,7 @@ function ScrollAdjust() { +@@ -6402,7 +6967,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -12383,7 +12409,13 @@ index 95465f2..b23f1f5 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6417,29 +6980,10 @@ function ScrollAdjust() { +@@ -6412,34 +6977,15 @@ function ScrollAdjust() { + const scrollBy = () => scrollAdjustBy(el, axis.x * scrollDelta, axis.y * scrollDelta); + contentNodeRef.current = contentNode; + if (shouldScroll && contentNode) { +- const totalSize = contentNode[axis.contentSizeKey]; ++ const totalSize = contentNode[axis.contentSizeKey] - getTemporaryEndPadding(contentNode, axis.paddingEndProp); + const viewportSize = el[axis.viewportSizeKey]; const nextScroll = currentScroll + scrollDelta; const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; if (needsTemporaryPadding) { @@ -12415,7 +12447,7 @@ index 95465f2..b23f1f5 100644 } else { scrollBy(); } -@@ -7624,10 +8168,10 @@ function useThrottleDebounce(mode) { +@@ -7624,10 +8170,10 @@ function useThrottleDebounce(mode) { const execute = useCallback( (callback, delay, ...args) => { { @@ -12429,7 +12461,7 @@ index 95465f2..b23f1f5 100644 callback(...args); clearTimeoutRef(); } else { -@@ -7641,7 +8185,7 @@ function useThrottleDebounce(mode) { +@@ -7641,7 +8187,7 @@ function useThrottleDebounce(mode) { lastArgsRef.current = null; } }, @@ -12438,7 +12470,7 @@ index 95465f2..b23f1f5 100644 ); } } -@@ -8267,6 +8811,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8267,6 +8813,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); diff --git a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts index 4deb4d311e12..55e7f14d806b 100644 --- a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts +++ b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts @@ -107,15 +107,27 @@ const describeHit = async (): Promise => { const topOfThreadPosition = async (): Promise => (await maybeBoundsOf(el(T.CHAT_THREAD_TOP)))?.y // "3 of 18" in the search bar. The count is what makes the wrap-around case honest: stepping a -// fixed number of times proves nothing if the thread happens to have more hits than that. +// fixed number of times proves nothing if the thread happens to have more hits than that. Read +// through the bar's own testID — matching " of " across the screen finds message text first, and +// the thread behind the bar is full of it. +// +// The testID is on the wrapper, and a wrapper reports no text of its own on iOS, so read the text +// element inside it. const readHitCount = async (): Promise => { - const bar = browser.$('-ios predicate string:name CONTAINS " of "') - if (browser.isAndroid) { - const label = await browser.$('//*[contains(@text, " of ")]').getText().catch(() => '') - return Number(/of (\d+)/.exec(label)?.[1] ?? 0) + const wrapper = el(T.CHAT_THREAD_SEARCH_COUNT) + const inner = browser.isAndroid + ? wrapper.$('.//android.widget.TextView') + : wrapper.$('-ios class chain:**/XCUIElementTypeStaticText') + const label = + (await inner.getText().catch(() => '')) || + (await wrapper.getText().catch(() => '')) || + (await wrapper.getAttribute('label').catch(() => '')) || + '' + const count = Number(/of (\d+)/.exec(label)?.[1] ?? 0) + if (count === 0) { + console.log(`readHitCount: could not read the counter, saw "${label}" at ${new Date().toISOString()}`) } - const label = await bar.getAttribute('name').catch(() => '') - return Number(/of (\d+)/.exec(label ?? '')?.[1] ?? 0) + return count } const startSearch = async (query: string): Promise => { diff --git a/shared/tests/e2e/ios-appium/flows/people-profile.test.ts b/shared/tests/e2e/ios-appium/flows/people-profile.test.ts index 4f1e56174f0c..10142fb3bf6f 100644 --- a/shared/tests/e2e/ios-appium/flows/people-profile.test.ts +++ b/shared/tests/e2e/ios-appium/flows/people-profile.test.ts @@ -1,15 +1,10 @@ import {expect} from '@wdio/globals' import {requireSmokeUser} from '../helpers/app' import {escapeToTabs, navigateToPeople} from '../helpers/navigate' -import {el, waitForTestID} from '../helpers/elements' +import {byTextWithin, el, waitForTestID} from '../helpers/elements' import * as T from '../../shared/test-ids' -describe('people profile', function () { - // The feed renders from a network load that is slow often enough to miss the wait on a busy - // machine, and a re-run is a fair way to tell that apart from a broken feed - nothing here - // depends on state the first attempt left behind. - this.retries(2) - +describe('people profile', () => { it('renders the feed and opens own profile when visible', async () => { const smokeUser = requireSmokeUser() await escapeToTabs() @@ -22,10 +17,19 @@ describe('people profile', function () { // Scoped to the feed, not matched across the screen: the People header's avatar carries the // username too, and tapping that opens the account switcher rather than a profile - which then // fails here on a missing profile page, and leaves a modal up for whatever runs next. - const userEl = el(T.PEOPLE_FEED).$( - `-ios predicate string:label CONTAINS "${smokeUser}" OR name CONTAINS "${smokeUser}"` - ) - if (!(await userEl.isExisting())) return + const userEl = byTextWithin(el(T.PEOPLE_FEED), smokeUser) + // The feed container mounts empty and immediately, so waiting on it says nothing about whether + // the feed has arrived. Wait for the row itself instead - a bounded wait rather than the retries + // this flow used to carry, which re-ran the whole test to buy the same time. + const present = await browser + .waitUntil(async () => userEl.isExisting(), {interval: 250, timeout: 10000}) + .then(() => true) + .catch(() => false) + if (!present) { + // eslint-disable-next-line no-console + console.log(`people profile: ${smokeUser} is not in its own feed, skipping the profile open`) + return + } await userEl.click() await waitForTestID(T.PROFILE_PAGE, 10000) await expect(el(T.PROFILE_PAGE)).toExist() diff --git a/shared/tests/e2e/ios-appium/helpers/elements.ts b/shared/tests/e2e/ios-appium/helpers/elements.ts index 2dff41c035b0..ebfaafcb9ae1 100644 --- a/shared/tests/e2e/ios-appium/helpers/elements.ts +++ b/shared/tests/e2e/ios-appium/helpers/elements.ts @@ -164,6 +164,17 @@ export const byText = (text: string): ChainablePromiseElement => { return browser.$(`-ios predicate string:label CONTAINS "${t}" OR name CONTAINS "${t}"`) } +// byText, scoped to a subtree, with the same platform split. Use it when the text you mean also +// appears in the chrome around the content: the People header's avatar carries the signed-in +// username, and tapping that opens the account switcher rather than a profile. +export const byTextWithin = (root: ChainablePromiseElement, text: string): ChainablePromiseElement => { + const t = escapePredicate(text) + if (browser.isAndroid) { + return root.$(`.//*[contains(@text, "${t}") or contains(@content-desc, "${t}")]`) + } + return root.$(`-ios predicate string:label CONTAINS "${t}" OR name CONTAINS "${t}"`) +} + // Tab-bar buttons. iOS exposes the native UITabBarItem by its title as the // accessibility id. Android's tab bar is a native Material BottomNavigationView: // target the ITEM view via its content-desc — the label, plus an optional badge diff --git a/shared/tests/e2e/ios-appium/helpers/navigate.ts b/shared/tests/e2e/ios-appium/helpers/navigate.ts index 38c54343f9f4..3f1710b645f9 100644 --- a/shared/tests/e2e/ios-appium/helpers/navigate.ts +++ b/shared/tests/e2e/ios-appium/helpers/navigate.ts @@ -94,24 +94,40 @@ export async function dismissKeyboard(): Promise { if (!(await browser.isKeyboardShown().catch(() => false))) return // WDA answers "Did not know how to dismiss the keyboard" for the chat composer — it has no Done - // key and no accessory to press. Blur it the way a person would, by tapping the content above it. - // This is not cosmetic: while the keyboard is up the screen's own controls stop reporting as - // hittable, so the back chevron is invisible to tapNavBack and the left-edge pop does not take, - // and escapeToTabs burns its whole budget on a conversation it cannot leave. Every flow after it - // then starts from the wrong screen. + // key and no accessory to press. This is not cosmetic: while the keyboard is up the screen's own + // controls stop reporting as hittable, so the back chevron is invisible to tapNavBack and the + // left-edge pop does not take, and escapeToTabs burns its whole budget on a conversation it + // cannot leave. Every flow after it then starts from the wrong screen. + // + // Dismiss it with a drag rather than a tap. The chat list sets keyboardDismissMode="on-drag", so + // a drag is the gesture it listens for; it also sets keyboardShouldPersistTaps="handled", which + // means a tap landing on a row is handled BY that row and does not dismiss the keyboard — while + // still doing whatever the row does (opening an attachment, following a link). A drag cannot + // activate a touchable, so it has no such side effect on any screen this runs from. const {height, width} = await browser.getWindowRect() + const x = Math.round(width / 2) await browser .action('pointer') - .move({x: Math.round(width / 2), y: Math.round(height * 0.3)}) + .move({x, y: Math.round(height * 0.45)}) .down() + .pause(60) + .move({duration: 250, x, y: Math.round(height * 0.25)}) .up() .perform() .catch(() => {}) - await browser.waitUntil(async () => !(await browser.isKeyboardShown().catch(() => false)), { - interval: 100, - timeout: 2000, - }) - .catch(() => {}) + const dismissed = await browser + .waitUntil(async () => !(await browser.isKeyboardShown().catch(() => false)), { + interval: 100, + timeout: 2000, + }) + .then(() => true) + .catch(() => false) + if (!dismissed) { + // Say so rather than leaving escapeToTabs to spend its whole budget on a screen whose controls + // are not hittable — a 50s stall with nothing in the log to explain it. + // eslint-disable-next-line no-console + console.log(`dismissKeyboard: keyboard still up after drag at ${new Date().toISOString()}`) + } } // Tap the leading (leftmost) button of a native NavigationBar — the back @@ -153,6 +169,13 @@ async function tapNavBack(requireLeftEdge = false): Promise { // element type — cheaper than three separate searches. // visible == 1: hidden nav-stack screens and keyboard toolbars can carry their // own Done/Close/Cancel — clicking one is a silent no-op that loops forever. +// The pre-loop's own predicate: an EXACT name, unlike DISMISS_PRED's substring match. This one +// clicks unattended before every test, so it must never match a button that happens to contain the +// word — a "Close team" or "Cancel invite" shipped later would otherwise become a destructive click +// in the reset. Buttons and menu items only, since a sheet's dismiss is always one of those. +const MODAL_DISMISS_PRED = + '-ios predicate string:(type == "XCUIElementTypeButton" OR type == "XCUIElementTypeMenuItem") AND (name == "Done" OR name == "Close" OR name == "Cancel" OR label == "Done" OR label == "Close" OR label == "Cancel") AND visible == 1' + const DISMISS_PRED = '-ios predicate string:(label CONTAINS "Done" OR name CONTAINS "Done" OR label CONTAINS "Close" OR name CONTAINS "Close" OR label CONTAINS "Cancel" OR name CONTAINS "Cancel") AND visible == 1' @@ -201,21 +224,29 @@ export async function escapeToTabs(): Promise { throw new Error('escapeToTabs(android): root tab bar not reached after 12 attempts') } // Dismiss anything presented over the tabs BEFORE asking whether we are at the root. A modal - // leaves the tab bar - and the whole screen behind it - in the accessibility tree, so atTabs reads + // leaves the tab bar — and the whole screen behind it — in the accessibility tree, so atTabs reads // "already home" while a New chat or account-switcher modal is still up; the reset then returns // with it still there, the next flow taps rows belonging to the modal, and every test after it // fails somewhere unrelated. It outlives the run too: the app restores its last screen, so a // leaked modal wedges the NEXT run from its first test. Bounded, and only ever clicks a control - // that is on screen - at a real root there is nothing to click and this costs one query. + // that is on screen — at a real root there is nothing to click and this costs one query. for (let i = 0; i < 3; i++) { - const controls = browser.$$(DISMISS_PRED) - if ((await controls.length) === 0) break - const ctrl = controls[0]! + const controls = await browser.$$(MODAL_DISMISS_PRED).getElements() + if (controls.length === 0) break + // The LAST match, not the first: a modal that only partly covers the screen leaves the + // background's controls in the tree, and those come first — its view controller is appended + // after. Clicking the first can click straight through the sheet. + const ctrl = controls[controls.length - 1]! await ctrl.click().catch(() => {}) - await settleAfter(ctrl) - // A control that survives its own click is not a modal dismiss - leave it to the loop below + // Waiting on atTabs here would be circular: that is the predicate this loop exists because it + // lies while a modal is up. Wait for the control itself to go. + const gone = await browser + .waitUntil(async () => !(await ctrl.isExisting().catch(() => false)), {interval: 100, timeout: 3000}) + .then(() => true) + .catch(() => false) + // A control that survives its own click is not a modal dismiss — leave it to the loop below // rather than clicking it forever. - if (await ctrl.isExisting().catch(() => false)) break + if (!gone) break } for (let i = 0; i < 10; i++) { if (await atTabs()) return diff --git a/shared/tests/e2e/shared/test-ids.ts b/shared/tests/e2e/shared/test-ids.ts index 900e59cf8c44..f5bb5e4bce0f 100644 --- a/shared/tests/e2e/shared/test-ids.ts +++ b/shared/tests/e2e/shared/test-ids.ts @@ -36,6 +36,9 @@ export const CHAT_THREAD_SEARCH_CANCEL = 'chat-thread-search-cancel' // The thread search query field. It focuses itself a beat after mounting, so a test types into it // rather than sending keys and hoping the focus landed. export const CHAT_THREAD_SEARCH_INPUT = 'chat-thread-search-input' +// "3 of 18" in the thread search bar. Read through this rather than matching " of " on screen: the +// thread behind the bar is full of message text and a body containing " of " matches first. +export const CHAT_THREAD_SEARCH_COUNT = 'chat-thread-search-count' export const CHAT_THREAD_SEARCH_PREV = 'chat-thread-search-prev' export const CHAT_THREAD_SEARCH_NEXT = 'chat-thread-search-next' // The row a thread search is currently sitting on. Really "the centre-highlighted row": pinned From bdeb2b99805f97a3277b9981edcceb09a3586174 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 12 Aug 2026 12:20:25 -0400 Subject: [PATCH 18/38] test(e2e): make the reset's waits mean what they say Review round two on the harness. Each of these could report success without it being true: The pre-loop waited for the dismissed control to disappear using isExisting, which re-runs the selector rather than checking that element - so any other Done/Close/Cancel on screen (the layer behind, a second sheet, the keyboard's own toolbar) answered "still there" and broke the loop after one iteration, which is the failure the loop was added to fix. It checks the element itself now, through isDisplayed, and treats a stale reference as gone. Its predicate excluded StaticText, which dropped the thread search bar's Cancel - a Kb.Text with an onClick. On iPad that is the one control that matters: atTabs is already true inside the Chat tab, so the loop below never runs and the pre-loop is the only thing that can close a leaked search bar. dismissKeyboard only knew how to dismiss the chat list's keyboard. The drag is right there - keyboardDismissMode="on-drag", and a tap can be swallowed by keyboardShouldPersistTaps="handled" - but no other screen sets either, so the drag was a regression everywhere else. It now asks WDA to press a named key first, then drags, then falls back to the tap at the height that used to work, and each step is tried only while the keyboard is still up. Caught by the log this added last round: "keyboard still up after drag and tap", on the team wizard, the one screen whose keyboard the drag could not reach. readHitCount could not tell "no results" from "could not read the counter", and reported both as a search that found nothing - the misdiagnosis the testID was meant to remove. Unreadable is its own answer now. It also read the count while results were still streaming, so the step budget could be a partial number and the wrap-around case would quietly stop wrapping; it waits for two reads to agree. byTextWithin escaped Android text with an ObjC-predicate escaper, which XPath cannot read, and matched descendants only where byText matches the root too. people-profile rebuilt its scoped query per poll: an element caches its parent's id, so a feed that re-renders makes every later poll throw stale, which the catch turned into a silent skip. Also updates the legend-list patch: the web scroll view now reports user interaction upward through a prop instead of importing core and clearing list state, a scroll carries whether it is the list re-aiming itself, and the momentum-grace sentinel starts at "never armed" rather than at time zero - which had been swallowing every wheel for the first 150ms of a page's life. iPhone suite: 61 passing. --- shared/patches/@legendapp+list+3.3.5.patch | 420 +++++++++++++----- .../ios-appium/flows/chat-search-hit.test.ts | 37 +- .../ios-appium/flows/people-profile.test.ts | 9 +- .../tests/e2e/ios-appium/helpers/elements.ts | 16 +- .../tests/e2e/ios-appium/helpers/navigate.ts | 86 +++- 5 files changed, 430 insertions(+), 138 deletions(-) diff --git a/shared/patches/@legendapp+list+3.3.5.patch b/shared/patches/@legendapp+list+3.3.5.patch index 98fdb7510fda..d57324d3f594 100644 --- a/shared/patches/@legendapp+list+3.3.5.patch +++ b/shared/patches/@legendapp+list+3.3.5.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js -index b3c5a30..7f1e818 100644 +index b3c5a30..c7f847c 100644 --- a/node_modules/@legendapp/list/react-native.js +++ b/node_modules/@legendapp/list/react-native.js @@ -457,611 +457,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -1568,7 +1568,21 @@ index b3c5a30..7f1e818 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -7652,6 +7808,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6001,7 +6157,12 @@ var ListComponent = typedMemo(function ListComponent2({ + SnapOrScroll, + { + ...rest, +- ...Platform.OS === "web" ? ScrollComponent === ListComponentScrollView ? { onInternalScrollEnd, useWindowScroll } : {} : { onScrollBeginDrag: onInternalScrollBeginDrag }, ++ ...Platform.OS === "web" ? ScrollComponent === ListComponentScrollView ? ( ++ // onUserInteraction is the web counterpart of onScrollBeginDrag: the view ++ // reports that the user moved the list, and LegendList decides what that ++ // means for a scroll in flight. ++ { onInternalScrollEnd, onUserInteraction: onInternalScrollBeginDrag, useWindowScroll } ++ ) : {} : { onScrollBeginDrag: onInternalScrollBeginDrag }, + contentContainerStyle: [ + horizontal ? { height: "100%" } : {}, + contentContainerStyle, +@@ -7652,6 +7813,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -1577,7 +1591,7 @@ index b3c5a30..7f1e818 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs -index 40e87cd..b9895df 100644 +index 40e87cd..fb9e73c 100644 --- a/node_modules/@legendapp/list/react-native.mjs +++ b/node_modules/@legendapp/list/react-native.mjs @@ -436,611 +436,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -3146,7 +3160,21 @@ index 40e87cd..b9895df 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -7631,6 +7787,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -5980,7 +6136,12 @@ var ListComponent = typedMemo(function ListComponent2({ + SnapOrScroll, + { + ...rest, +- ...Platform.OS === "web" ? ScrollComponent === ListComponentScrollView ? { onInternalScrollEnd, useWindowScroll } : {} : { onScrollBeginDrag: onInternalScrollBeginDrag }, ++ ...Platform.OS === "web" ? ScrollComponent === ListComponentScrollView ? ( ++ // onUserInteraction is the web counterpart of onScrollBeginDrag: the view ++ // reports that the user moved the list, and LegendList decides what that ++ // means for a scroll in flight. ++ { onInternalScrollEnd, onUserInteraction: onInternalScrollBeginDrag, useWindowScroll } ++ ) : {} : { onScrollBeginDrag: onInternalScrollBeginDrag }, + contentContainerStyle: [ + horizontal ? { height: "100%" } : {}, + contentContainerStyle, +@@ -7631,6 +7792,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -3155,7 +3183,7 @@ index 40e87cd..b9895df 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js -index 914d2da..d73d16b 100644 +index 914d2da..cd500e5 100644 --- a/node_modules/@legendapp/list/react-native.web.js +++ b/node_modules/@legendapp/list/react-native.web.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -4084,6 +4112,24 @@ index 914d2da..d73d16b 100644 } // src/core/finishScrollTo.ts +@@ -1334,7 +950,7 @@ var SCROLL_END_TARGET_EPSILON = 1; + function doScrollTo(ctx, params) { + var _a3, _b; + const state = ctx.state; +- const { animated, horizontal, offset } = params; ++ const { animated, horizontal, isCorrection, offset } = params; + state.scheduledWork.cancel("platformScrollCompletion"); + const scroller = state.refScroller.current; + const node = scroller == null ? void 0 : scroller.getScrollableNode(); +@@ -1346,7 +962,7 @@ function doScrollTo(ctx, params) { + const contentSize = isHorizontal ? getContentSize(ctx) : void 0; + const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; + const top = isHorizontal ? 0 : offset; +- scroller.scrollTo({ animated: isAnimated, x: left, y: top }); ++ scroller.scrollTo({ animated: isAnimated, isCorrection, x: left, y: top }); + if (isAnimated) { + const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; + listenForScrollEnd(ctx, { @@ -1413,16 +1029,191 @@ function listenForScrollEnd(ctx, params) { if (idleTimeout !== void 0) { clearTimeout(idleTimeout); @@ -4445,7 +4491,7 @@ index 914d2da..d73d16b 100644 + } + } + if (forceScroll || !isInitialScroll || Platform.OS === "android") { -+ doScrollTo(ctx, { animated, horizontal, offset }); ++ doScrollTo(ctx, { animated, horizontal, isCorrection: noScrollingTo, offset }); + } else { + state.scroll = offset; + } @@ -5081,7 +5127,15 @@ index 914d2da..d73d16b 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6074,6 +6448,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6055,6 +6429,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + contentOffset, + maintainVisibleContentPosition, + onScroll: onScroll2, ++ onUserInteraction, + onInternalScrollEnd, + onMomentumScrollEnd: _onMomentumScrollEnd, + showsHorizontalScrollIndicator = true, +@@ -6074,6 +6449,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -5093,7 +5147,7 @@ index 914d2da..d73d16b 100644 const getMaxScrollOffset = React3.useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6094,8 +6473,172 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,8 +6474,172 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -5195,7 +5249,7 @@ index 914d2da..d73d16b 100644 + }, + [] + ); -+ const interactionArmedAtRef = React3.useRef(0); ++ const interactionArmedAtRef = React3.useRef(Number.NEGATIVE_INFINITY); + const dragOriginRef = React3.useRef(void 0); + const ownsEvent = React3.useCallback((event) => { + const scroller = scrollRef.current; @@ -5207,11 +5261,11 @@ index 914d2da..d73d16b 100644 + }, []); + const reportUserInteraction = React3.useCallback(() => { + dragOriginRef.current = void 0; -+ releaseScrollTargetForUserInteraction(ctx.state); -+ }, [ctx]); ++ onUserInteraction == null ? void 0 : onUserInteraction(); ++ }, [onUserInteraction]); + const onWheel = React3.useCallback( + (event) => { -+ if (!ownsEvent(event)) { ++ if (!isWindowScroll && !ownsEvent(event)) { + return; + } + if (now() - interactionArmedAtRef.current < WHEEL_MOMENTUM_GRACE_MS) { @@ -5219,7 +5273,7 @@ index 914d2da..d73d16b 100644 + } + reportUserInteraction(); + }, -+ [ownsEvent, reportUserInteraction] ++ [isWindowScroll, ownsEvent, reportUserInteraction] + ); + const onPointerDown = React3.useCallback( + (event) => { @@ -5259,14 +5313,15 @@ index 914d2da..d73d16b 100644 + [reportUserInteraction] + ); const scrollToLocalOffset = React3.useCallback( - (offset, animated) => { -+ if (!ctx.state.scrollTargetSettle) { +- (offset, animated) => { ++ (offset, animated, isCorrection) => { ++ if (!isCorrection) { + interactionArmedAtRef.current = now(); + } const scrollElement = scrollRef.current; const target = getScrollTarget(); if (!target || typeof target.scrollTo !== "function") { -@@ -6116,14 +6659,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6116,14 +6660,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); options.left = left; options.top = top; @@ -5293,7 +5348,14 @@ index 914d2da..d73d16b 100644 ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6699,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6144,13 +6695,12 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + target.scrollBy({ behavior: "auto", left: x, top: y }); + }, + scrollTo: (options) => { +- const { x = 0, y = 0, animated = true } = options; +- scrollToLocalOffset(horizontal ? x : y, animated); ++ const { x = 0, y = 0, animated = true, isCorrection } = options; ++ scrollToLocalOffset(horizontal ? x : y, animated, isCorrection); }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -5303,7 +5365,7 @@ index 914d2da..d73d16b 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6163,7 +6712,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6163,7 +6713,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView if (!onScroll2 || !scrollRef.current) { return; } @@ -5320,7 +5382,7 @@ index 914d2da..d73d16b 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6218,14 +6775,29 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6218,14 +6776,29 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] ); React3.useLayoutEffect(() => { @@ -5350,7 +5412,7 @@ index 914d2da..d73d16b 100644 if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6236,7 +6808,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6236,7 +6809,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } scrollEventCoalescer.cancel(); }; @@ -5368,7 +5430,7 @@ index 914d2da..d73d16b 100644 React3.useEffect(() => { const doScroll = () => { if (contentOffset) { -@@ -6341,6 +6922,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6341,6 +6923,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView { className: scrollViewClassName, ref: scrollRef, @@ -5376,7 +5438,7 @@ index 914d2da..d73d16b 100644 ...webProps, style: scrollViewStyle }, -@@ -6379,21 +6961,6 @@ function useValueListener$(key, callback) { +@@ -6379,21 +6962,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -5398,7 +5460,7 @@ index 914d2da..d73d16b 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6411,8 +6978,6 @@ function ScrollAdjust() { +@@ -6411,8 +6979,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3__namespace.useRef(0); const lastScrollAdjustUserOffsetRef = React3__namespace.useRef(0); @@ -5407,7 +5469,7 @@ index 914d2da..d73d16b 100644 const contentNodeRef = React3__namespace.useRef(null); const callback = React3__namespace.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6423,7 +6988,7 @@ function ScrollAdjust() { +@@ -6423,7 +6989,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -5416,7 +5478,7 @@ index 914d2da..d73d16b 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6433,34 +6998,15 @@ function ScrollAdjust() { +@@ -6433,34 +6999,15 @@ function ScrollAdjust() { const scrollBy = () => scrollAdjustBy(el, axis.x * scrollDelta, axis.y * scrollDelta); contentNodeRef.current = contentNode; if (shouldScroll && contentNode) { @@ -5454,7 +5516,21 @@ index 914d2da..d73d16b 100644 } else { scrollBy(); } -@@ -7645,10 +8191,10 @@ function useThrottleDebounce(mode) { +@@ -6661,7 +7208,12 @@ var ListComponent = typedMemo(function ListComponent2({ + SnapOrScroll, + { + ...rest, +- ...ScrollComponent === ListComponentScrollView ? { onInternalScrollEnd, useWindowScroll } : {} , ++ ...ScrollComponent === ListComponentScrollView ? ( ++ // onUserInteraction is the web counterpart of onScrollBeginDrag: the view ++ // reports that the user moved the list, and LegendList decides what that ++ // means for a scroll in flight. ++ { onInternalScrollEnd, onUserInteraction: onInternalScrollBeginDrag, useWindowScroll } ++ ) : {} , + contentContainerStyle: [ + horizontal ? { height: "100%" } : {}, + contentContainerStyle, +@@ -7645,10 +8197,10 @@ function useThrottleDebounce(mode) { const execute = React3.useCallback( (callback, delay, ...args) => { { @@ -5468,7 +5544,7 @@ index 914d2da..d73d16b 100644 callback(...args); clearTimeoutRef(); } else { -@@ -7662,7 +8208,7 @@ function useThrottleDebounce(mode) { +@@ -7662,7 +8214,7 @@ function useThrottleDebounce(mode) { lastArgsRef.current = null; } }, @@ -5477,7 +5553,7 @@ index 914d2da..d73d16b 100644 ); } } -@@ -8288,6 +8834,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8288,6 +8840,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -5486,7 +5562,7 @@ index 914d2da..d73d16b 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs -index 95465f2..5e38834 100644 +index 95465f2..54826e8 100644 --- a/node_modules/@legendapp/list/react-native.web.mjs +++ b/node_modules/@legendapp/list/react-native.web.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -6415,6 +6491,24 @@ index 95465f2..5e38834 100644 } // src/core/finishScrollTo.ts +@@ -1313,7 +929,7 @@ var SCROLL_END_TARGET_EPSILON = 1; + function doScrollTo(ctx, params) { + var _a3, _b; + const state = ctx.state; +- const { animated, horizontal, offset } = params; ++ const { animated, horizontal, isCorrection, offset } = params; + state.scheduledWork.cancel("platformScrollCompletion"); + const scroller = state.refScroller.current; + const node = scroller == null ? void 0 : scroller.getScrollableNode(); +@@ -1325,7 +941,7 @@ function doScrollTo(ctx, params) { + const contentSize = isHorizontal ? getContentSize(ctx) : void 0; + const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; + const top = isHorizontal ? 0 : offset; +- scroller.scrollTo({ animated: isAnimated, x: left, y: top }); ++ scroller.scrollTo({ animated: isAnimated, isCorrection, x: left, y: top }); + if (isAnimated) { + const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; + listenForScrollEnd(ctx, { @@ -1392,16 +1008,191 @@ function listenForScrollEnd(ctx, params) { if (idleTimeout !== void 0) { clearTimeout(idleTimeout); @@ -6776,7 +6870,7 @@ index 95465f2..5e38834 100644 + } + } + if (forceScroll || !isInitialScroll || Platform.OS === "android") { -+ doScrollTo(ctx, { animated, horizontal, offset }); ++ doScrollTo(ctx, { animated, horizontal, isCorrection: noScrollingTo, offset }); + } else { + state.scroll = offset; + } @@ -7412,7 +7506,15 @@ index 95465f2..5e38834 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6053,6 +6427,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6034,6 +6408,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + contentOffset, + maintainVisibleContentPosition, + onScroll: onScroll2, ++ onUserInteraction, + onInternalScrollEnd, + onMomentumScrollEnd: _onMomentumScrollEnd, + showsHorizontalScrollIndicator = true, +@@ -6053,6 +6428,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -7424,7 +7526,7 @@ index 95465f2..5e38834 100644 const getMaxScrollOffset = useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6073,8 +6452,172 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,8 +6453,172 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -7526,7 +7628,7 @@ index 95465f2..5e38834 100644 + }, + [] + ); -+ const interactionArmedAtRef = useRef(0); ++ const interactionArmedAtRef = useRef(Number.NEGATIVE_INFINITY); + const dragOriginRef = useRef(void 0); + const ownsEvent = useCallback((event) => { + const scroller = scrollRef.current; @@ -7538,11 +7640,11 @@ index 95465f2..5e38834 100644 + }, []); + const reportUserInteraction = useCallback(() => { + dragOriginRef.current = void 0; -+ releaseScrollTargetForUserInteraction(ctx.state); -+ }, [ctx]); ++ onUserInteraction == null ? void 0 : onUserInteraction(); ++ }, [onUserInteraction]); + const onWheel = useCallback( + (event) => { -+ if (!ownsEvent(event)) { ++ if (!isWindowScroll && !ownsEvent(event)) { + return; + } + if (now() - interactionArmedAtRef.current < WHEEL_MOMENTUM_GRACE_MS) { @@ -7550,7 +7652,7 @@ index 95465f2..5e38834 100644 + } + reportUserInteraction(); + }, -+ [ownsEvent, reportUserInteraction] ++ [isWindowScroll, ownsEvent, reportUserInteraction] + ); + const onPointerDown = useCallback( + (event) => { @@ -7590,14 +7692,15 @@ index 95465f2..5e38834 100644 + [reportUserInteraction] + ); const scrollToLocalOffset = useCallback( - (offset, animated) => { -+ if (!ctx.state.scrollTargetSettle) { +- (offset, animated) => { ++ (offset, animated, isCorrection) => { ++ if (!isCorrection) { + interactionArmedAtRef.current = now(); + } const scrollElement = scrollRef.current; const target = getScrollTarget(); if (!target || typeof target.scrollTo !== "function") { -@@ -6095,14 +6638,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6095,14 +6639,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); options.left = left; options.top = top; @@ -7624,7 +7727,14 @@ index 95465f2..5e38834 100644 ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6678,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6123,13 +6674,12 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + target.scrollBy({ behavior: "auto", left: x, top: y }); + }, + scrollTo: (options) => { +- const { x = 0, y = 0, animated = true } = options; +- scrollToLocalOffset(horizontal ? x : y, animated); ++ const { x = 0, y = 0, animated = true, isCorrection } = options; ++ scrollToLocalOffset(horizontal ? x : y, animated, isCorrection); }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -7634,7 +7744,7 @@ index 95465f2..5e38834 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6142,7 +6691,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6142,7 +6692,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ if (!onScroll2 || !scrollRef.current) { return; } @@ -7651,7 +7761,7 @@ index 95465f2..5e38834 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6197,14 +6754,29 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6197,14 +6755,29 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] ); useLayoutEffect(() => { @@ -7681,7 +7791,7 @@ index 95465f2..5e38834 100644 if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6215,7 +6787,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6215,7 +6788,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } scrollEventCoalescer.cancel(); }; @@ -7699,7 +7809,7 @@ index 95465f2..5e38834 100644 useEffect(() => { const doScroll = () => { if (contentOffset) { -@@ -6320,6 +6901,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6320,6 +6902,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ { className: scrollViewClassName, ref: scrollRef, @@ -7707,7 +7817,7 @@ index 95465f2..5e38834 100644 ...webProps, style: scrollViewStyle }, -@@ -6358,21 +6940,6 @@ function useValueListener$(key, callback) { +@@ -6358,21 +6941,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -7729,7 +7839,7 @@ index 95465f2..5e38834 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6390,8 +6957,6 @@ function ScrollAdjust() { +@@ -6390,8 +6958,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3.useRef(0); const lastScrollAdjustUserOffsetRef = React3.useRef(0); @@ -7738,7 +7848,7 @@ index 95465f2..5e38834 100644 const contentNodeRef = React3.useRef(null); const callback = React3.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6402,7 +6967,7 @@ function ScrollAdjust() { +@@ -6402,7 +6968,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -7747,7 +7857,7 @@ index 95465f2..5e38834 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6412,34 +6977,15 @@ function ScrollAdjust() { +@@ -6412,34 +6978,15 @@ function ScrollAdjust() { const scrollBy = () => scrollAdjustBy(el, axis.x * scrollDelta, axis.y * scrollDelta); contentNodeRef.current = contentNode; if (shouldScroll && contentNode) { @@ -7785,7 +7895,21 @@ index 95465f2..5e38834 100644 } else { scrollBy(); } -@@ -7624,10 +8170,10 @@ function useThrottleDebounce(mode) { +@@ -6640,7 +7187,12 @@ var ListComponent = typedMemo(function ListComponent2({ + SnapOrScroll, + { + ...rest, +- ...ScrollComponent === ListComponentScrollView ? { onInternalScrollEnd, useWindowScroll } : {} , ++ ...ScrollComponent === ListComponentScrollView ? ( ++ // onUserInteraction is the web counterpart of onScrollBeginDrag: the view ++ // reports that the user moved the list, and LegendList decides what that ++ // means for a scroll in flight. ++ { onInternalScrollEnd, onUserInteraction: onInternalScrollBeginDrag, useWindowScroll } ++ ) : {} , + contentContainerStyle: [ + horizontal ? { height: "100%" } : {}, + contentContainerStyle, +@@ -7624,10 +8176,10 @@ function useThrottleDebounce(mode) { const execute = useCallback( (callback, delay, ...args) => { { @@ -7799,7 +7923,7 @@ index 95465f2..5e38834 100644 callback(...args); clearTimeoutRef(); } else { -@@ -7641,7 +8187,7 @@ function useThrottleDebounce(mode) { +@@ -7641,7 +8193,7 @@ function useThrottleDebounce(mode) { lastArgsRef.current = null; } }, @@ -7808,7 +7932,7 @@ index 95465f2..5e38834 100644 ); } } -@@ -8267,6 +8813,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8267,6 +8819,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -7817,7 +7941,7 @@ index 95465f2..5e38834 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js -index 914d2da..d73d16b 100644 +index 914d2da..cd500e5 100644 --- a/node_modules/@legendapp/list/react.js +++ b/node_modules/@legendapp/list/react.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -8746,6 +8870,24 @@ index 914d2da..d73d16b 100644 } // src/core/finishScrollTo.ts +@@ -1334,7 +950,7 @@ var SCROLL_END_TARGET_EPSILON = 1; + function doScrollTo(ctx, params) { + var _a3, _b; + const state = ctx.state; +- const { animated, horizontal, offset } = params; ++ const { animated, horizontal, isCorrection, offset } = params; + state.scheduledWork.cancel("platformScrollCompletion"); + const scroller = state.refScroller.current; + const node = scroller == null ? void 0 : scroller.getScrollableNode(); +@@ -1346,7 +962,7 @@ function doScrollTo(ctx, params) { + const contentSize = isHorizontal ? getContentSize(ctx) : void 0; + const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; + const top = isHorizontal ? 0 : offset; +- scroller.scrollTo({ animated: isAnimated, x: left, y: top }); ++ scroller.scrollTo({ animated: isAnimated, isCorrection, x: left, y: top }); + if (isAnimated) { + const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; + listenForScrollEnd(ctx, { @@ -1413,16 +1029,191 @@ function listenForScrollEnd(ctx, params) { if (idleTimeout !== void 0) { clearTimeout(idleTimeout); @@ -9107,7 +9249,7 @@ index 914d2da..d73d16b 100644 + } + } + if (forceScroll || !isInitialScroll || Platform.OS === "android") { -+ doScrollTo(ctx, { animated, horizontal, offset }); ++ doScrollTo(ctx, { animated, horizontal, isCorrection: noScrollingTo, offset }); + } else { + state.scroll = offset; + } @@ -9743,7 +9885,15 @@ index 914d2da..d73d16b 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6074,6 +6448,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6055,6 +6429,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + contentOffset, + maintainVisibleContentPosition, + onScroll: onScroll2, ++ onUserInteraction, + onInternalScrollEnd, + onMomentumScrollEnd: _onMomentumScrollEnd, + showsHorizontalScrollIndicator = true, +@@ -6074,6 +6449,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -9755,7 +9905,7 @@ index 914d2da..d73d16b 100644 const getMaxScrollOffset = React3.useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6094,8 +6473,172 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,8 +6474,172 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -9857,7 +10007,7 @@ index 914d2da..d73d16b 100644 + }, + [] + ); -+ const interactionArmedAtRef = React3.useRef(0); ++ const interactionArmedAtRef = React3.useRef(Number.NEGATIVE_INFINITY); + const dragOriginRef = React3.useRef(void 0); + const ownsEvent = React3.useCallback((event) => { + const scroller = scrollRef.current; @@ -9869,11 +10019,11 @@ index 914d2da..d73d16b 100644 + }, []); + const reportUserInteraction = React3.useCallback(() => { + dragOriginRef.current = void 0; -+ releaseScrollTargetForUserInteraction(ctx.state); -+ }, [ctx]); ++ onUserInteraction == null ? void 0 : onUserInteraction(); ++ }, [onUserInteraction]); + const onWheel = React3.useCallback( + (event) => { -+ if (!ownsEvent(event)) { ++ if (!isWindowScroll && !ownsEvent(event)) { + return; + } + if (now() - interactionArmedAtRef.current < WHEEL_MOMENTUM_GRACE_MS) { @@ -9881,7 +10031,7 @@ index 914d2da..d73d16b 100644 + } + reportUserInteraction(); + }, -+ [ownsEvent, reportUserInteraction] ++ [isWindowScroll, ownsEvent, reportUserInteraction] + ); + const onPointerDown = React3.useCallback( + (event) => { @@ -9921,14 +10071,15 @@ index 914d2da..d73d16b 100644 + [reportUserInteraction] + ); const scrollToLocalOffset = React3.useCallback( - (offset, animated) => { -+ if (!ctx.state.scrollTargetSettle) { +- (offset, animated) => { ++ (offset, animated, isCorrection) => { ++ if (!isCorrection) { + interactionArmedAtRef.current = now(); + } const scrollElement = scrollRef.current; const target = getScrollTarget(); if (!target || typeof target.scrollTo !== "function") { -@@ -6116,14 +6659,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6116,14 +6660,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); options.left = left; options.top = top; @@ -9955,7 +10106,14 @@ index 914d2da..d73d16b 100644 ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6699,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6144,13 +6695,12 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + target.scrollBy({ behavior: "auto", left: x, top: y }); + }, + scrollTo: (options) => { +- const { x = 0, y = 0, animated = true } = options; +- scrollToLocalOffset(horizontal ? x : y, animated); ++ const { x = 0, y = 0, animated = true, isCorrection } = options; ++ scrollToLocalOffset(horizontal ? x : y, animated, isCorrection); }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -9965,7 +10123,7 @@ index 914d2da..d73d16b 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6163,7 +6712,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6163,7 +6713,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView if (!onScroll2 || !scrollRef.current) { return; } @@ -9982,7 +10140,7 @@ index 914d2da..d73d16b 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6218,14 +6775,29 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6218,14 +6776,29 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] ); React3.useLayoutEffect(() => { @@ -10012,7 +10170,7 @@ index 914d2da..d73d16b 100644 if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6236,7 +6808,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6236,7 +6809,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } scrollEventCoalescer.cancel(); }; @@ -10030,7 +10188,7 @@ index 914d2da..d73d16b 100644 React3.useEffect(() => { const doScroll = () => { if (contentOffset) { -@@ -6341,6 +6922,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6341,6 +6923,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView { className: scrollViewClassName, ref: scrollRef, @@ -10038,7 +10196,7 @@ index 914d2da..d73d16b 100644 ...webProps, style: scrollViewStyle }, -@@ -6379,21 +6961,6 @@ function useValueListener$(key, callback) { +@@ -6379,21 +6962,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -10060,7 +10218,7 @@ index 914d2da..d73d16b 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6411,8 +6978,6 @@ function ScrollAdjust() { +@@ -6411,8 +6979,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3__namespace.useRef(0); const lastScrollAdjustUserOffsetRef = React3__namespace.useRef(0); @@ -10069,7 +10227,7 @@ index 914d2da..d73d16b 100644 const contentNodeRef = React3__namespace.useRef(null); const callback = React3__namespace.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6423,7 +6988,7 @@ function ScrollAdjust() { +@@ -6423,7 +6989,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -10078,7 +10236,7 @@ index 914d2da..d73d16b 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6433,34 +6998,15 @@ function ScrollAdjust() { +@@ -6433,34 +6999,15 @@ function ScrollAdjust() { const scrollBy = () => scrollAdjustBy(el, axis.x * scrollDelta, axis.y * scrollDelta); contentNodeRef.current = contentNode; if (shouldScroll && contentNode) { @@ -10116,7 +10274,21 @@ index 914d2da..d73d16b 100644 } else { scrollBy(); } -@@ -7645,10 +8191,10 @@ function useThrottleDebounce(mode) { +@@ -6661,7 +7208,12 @@ var ListComponent = typedMemo(function ListComponent2({ + SnapOrScroll, + { + ...rest, +- ...ScrollComponent === ListComponentScrollView ? { onInternalScrollEnd, useWindowScroll } : {} , ++ ...ScrollComponent === ListComponentScrollView ? ( ++ // onUserInteraction is the web counterpart of onScrollBeginDrag: the view ++ // reports that the user moved the list, and LegendList decides what that ++ // means for a scroll in flight. ++ { onInternalScrollEnd, onUserInteraction: onInternalScrollBeginDrag, useWindowScroll } ++ ) : {} , + contentContainerStyle: [ + horizontal ? { height: "100%" } : {}, + contentContainerStyle, +@@ -7645,10 +8197,10 @@ function useThrottleDebounce(mode) { const execute = React3.useCallback( (callback, delay, ...args) => { { @@ -10130,7 +10302,7 @@ index 914d2da..d73d16b 100644 callback(...args); clearTimeoutRef(); } else { -@@ -7662,7 +8208,7 @@ function useThrottleDebounce(mode) { +@@ -7662,7 +8214,7 @@ function useThrottleDebounce(mode) { lastArgsRef.current = null; } }, @@ -10139,7 +10311,7 @@ index 914d2da..d73d16b 100644 ); } } -@@ -8288,6 +8834,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8288,6 +8840,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -10148,7 +10320,7 @@ index 914d2da..d73d16b 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs -index 95465f2..5e38834 100644 +index 95465f2..54826e8 100644 --- a/node_modules/@legendapp/list/react.mjs +++ b/node_modules/@legendapp/list/react.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -11077,6 +11249,24 @@ index 95465f2..5e38834 100644 } // src/core/finishScrollTo.ts +@@ -1313,7 +929,7 @@ var SCROLL_END_TARGET_EPSILON = 1; + function doScrollTo(ctx, params) { + var _a3, _b; + const state = ctx.state; +- const { animated, horizontal, offset } = params; ++ const { animated, horizontal, isCorrection, offset } = params; + state.scheduledWork.cancel("platformScrollCompletion"); + const scroller = state.refScroller.current; + const node = scroller == null ? void 0 : scroller.getScrollableNode(); +@@ -1325,7 +941,7 @@ function doScrollTo(ctx, params) { + const contentSize = isHorizontal ? getContentSize(ctx) : void 0; + const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; + const top = isHorizontal ? 0 : offset; +- scroller.scrollTo({ animated: isAnimated, x: left, y: top }); ++ scroller.scrollTo({ animated: isAnimated, isCorrection, x: left, y: top }); + if (isAnimated) { + const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; + listenForScrollEnd(ctx, { @@ -1392,16 +1008,191 @@ function listenForScrollEnd(ctx, params) { if (idleTimeout !== void 0) { clearTimeout(idleTimeout); @@ -11438,7 +11628,7 @@ index 95465f2..5e38834 100644 + } + } + if (forceScroll || !isInitialScroll || Platform.OS === "android") { -+ doScrollTo(ctx, { animated, horizontal, offset }); ++ doScrollTo(ctx, { animated, horizontal, isCorrection: noScrollingTo, offset }); + } else { + state.scroll = offset; + } @@ -12074,7 +12264,15 @@ index 95465f2..5e38834 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6053,6 +6427,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6034,6 +6408,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + contentOffset, + maintainVisibleContentPosition, + onScroll: onScroll2, ++ onUserInteraction, + onInternalScrollEnd, + onMomentumScrollEnd: _onMomentumScrollEnd, + showsHorizontalScrollIndicator = true, +@@ -6053,6 +6428,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -12086,7 +12284,7 @@ index 95465f2..5e38834 100644 const getMaxScrollOffset = useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6073,8 +6452,172 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,8 +6453,172 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -12188,7 +12386,7 @@ index 95465f2..5e38834 100644 + }, + [] + ); -+ const interactionArmedAtRef = useRef(0); ++ const interactionArmedAtRef = useRef(Number.NEGATIVE_INFINITY); + const dragOriginRef = useRef(void 0); + const ownsEvent = useCallback((event) => { + const scroller = scrollRef.current; @@ -12200,11 +12398,11 @@ index 95465f2..5e38834 100644 + }, []); + const reportUserInteraction = useCallback(() => { + dragOriginRef.current = void 0; -+ releaseScrollTargetForUserInteraction(ctx.state); -+ }, [ctx]); ++ onUserInteraction == null ? void 0 : onUserInteraction(); ++ }, [onUserInteraction]); + const onWheel = useCallback( + (event) => { -+ if (!ownsEvent(event)) { ++ if (!isWindowScroll && !ownsEvent(event)) { + return; + } + if (now() - interactionArmedAtRef.current < WHEEL_MOMENTUM_GRACE_MS) { @@ -12212,7 +12410,7 @@ index 95465f2..5e38834 100644 + } + reportUserInteraction(); + }, -+ [ownsEvent, reportUserInteraction] ++ [isWindowScroll, ownsEvent, reportUserInteraction] + ); + const onPointerDown = useCallback( + (event) => { @@ -12252,14 +12450,15 @@ index 95465f2..5e38834 100644 + [reportUserInteraction] + ); const scrollToLocalOffset = useCallback( - (offset, animated) => { -+ if (!ctx.state.scrollTargetSettle) { +- (offset, animated) => { ++ (offset, animated, isCorrection) => { ++ if (!isCorrection) { + interactionArmedAtRef.current = now(); + } const scrollElement = scrollRef.current; const target = getScrollTarget(); if (!target || typeof target.scrollTo !== "function") { -@@ -6095,14 +6638,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6095,14 +6639,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); options.left = left; options.top = top; @@ -12286,7 +12485,14 @@ index 95465f2..5e38834 100644 ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6678,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6123,13 +6674,12 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + target.scrollBy({ behavior: "auto", left: x, top: y }); + }, + scrollTo: (options) => { +- const { x = 0, y = 0, animated = true } = options; +- scrollToLocalOffset(horizontal ? x : y, animated); ++ const { x = 0, y = 0, animated = true, isCorrection } = options; ++ scrollToLocalOffset(horizontal ? x : y, animated, isCorrection); }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -12296,7 +12502,7 @@ index 95465f2..5e38834 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6142,7 +6691,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6142,7 +6692,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ if (!onScroll2 || !scrollRef.current) { return; } @@ -12313,7 +12519,7 @@ index 95465f2..5e38834 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6197,14 +6754,29 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6197,14 +6755,29 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] ); useLayoutEffect(() => { @@ -12343,7 +12549,7 @@ index 95465f2..5e38834 100644 if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6215,7 +6787,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6215,7 +6788,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } scrollEventCoalescer.cancel(); }; @@ -12361,7 +12567,7 @@ index 95465f2..5e38834 100644 useEffect(() => { const doScroll = () => { if (contentOffset) { -@@ -6320,6 +6901,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6320,6 +6902,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ { className: scrollViewClassName, ref: scrollRef, @@ -12369,7 +12575,7 @@ index 95465f2..5e38834 100644 ...webProps, style: scrollViewStyle }, -@@ -6358,21 +6940,6 @@ function useValueListener$(key, callback) { +@@ -6358,21 +6941,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -12391,7 +12597,7 @@ index 95465f2..5e38834 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6390,8 +6957,6 @@ function ScrollAdjust() { +@@ -6390,8 +6958,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3.useRef(0); const lastScrollAdjustUserOffsetRef = React3.useRef(0); @@ -12400,7 +12606,7 @@ index 95465f2..5e38834 100644 const contentNodeRef = React3.useRef(null); const callback = React3.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6402,7 +6967,7 @@ function ScrollAdjust() { +@@ -6402,7 +6968,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -12409,7 +12615,7 @@ index 95465f2..5e38834 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6412,34 +6977,15 @@ function ScrollAdjust() { +@@ -6412,34 +6978,15 @@ function ScrollAdjust() { const scrollBy = () => scrollAdjustBy(el, axis.x * scrollDelta, axis.y * scrollDelta); contentNodeRef.current = contentNode; if (shouldScroll && contentNode) { @@ -12447,7 +12653,21 @@ index 95465f2..5e38834 100644 } else { scrollBy(); } -@@ -7624,10 +8170,10 @@ function useThrottleDebounce(mode) { +@@ -6640,7 +7187,12 @@ var ListComponent = typedMemo(function ListComponent2({ + SnapOrScroll, + { + ...rest, +- ...ScrollComponent === ListComponentScrollView ? { onInternalScrollEnd, useWindowScroll } : {} , ++ ...ScrollComponent === ListComponentScrollView ? ( ++ // onUserInteraction is the web counterpart of onScrollBeginDrag: the view ++ // reports that the user moved the list, and LegendList decides what that ++ // means for a scroll in flight. ++ { onInternalScrollEnd, onUserInteraction: onInternalScrollBeginDrag, useWindowScroll } ++ ) : {} , + contentContainerStyle: [ + horizontal ? { height: "100%" } : {}, + contentContainerStyle, +@@ -7624,10 +8176,10 @@ function useThrottleDebounce(mode) { const execute = useCallback( (callback, delay, ...args) => { { @@ -12461,7 +12681,7 @@ index 95465f2..5e38834 100644 callback(...args); clearTimeoutRef(); } else { -@@ -7641,7 +8187,7 @@ function useThrottleDebounce(mode) { +@@ -7641,7 +8193,7 @@ function useThrottleDebounce(mode) { lastArgsRef.current = null; } }, @@ -12470,7 +12690,7 @@ index 95465f2..5e38834 100644 ); } } -@@ -8267,6 +8813,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8267,6 +8819,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); diff --git a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts index 55e7f14d806b..8f0b53e61453 100644 --- a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts +++ b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts @@ -111,23 +111,35 @@ const topOfThreadPosition = async (): Promise => (await mayb // through the bar's own testID — matching " of " across the screen finds message text first, and // the thread behind the bar is full of it. // -// The testID is on the wrapper, and a wrapper reports no text of its own on iOS, so read the text -// element inside it. -const readHitCount = async (): Promise => { +// undefined means the counter could not be read, which is a different thing from a search with no +// results, and the caller says so differently. +const readHitCount = async (): Promise => { const wrapper = el(T.CHAT_THREAD_SEARCH_COUNT) const inner = browser.isAndroid ? wrapper.$('.//android.widget.TextView') : wrapper.$('-ios class chain:**/XCUIElementTypeStaticText') - const label = - (await inner.getText().catch(() => '')) || - (await wrapper.getText().catch(() => '')) || - (await wrapper.getAttribute('label').catch(() => '')) || - '' - const count = Number(/of (\d+)/.exec(label)?.[1] ?? 0) - if (count === 0) { + const label = (await inner.getText().catch(() => '')) || (await wrapper.getText().catch(() => '')) + if (/no results/i.test(label)) return 0 + const parsed = /of (\d+)/.exec(label) + if (!parsed) { console.log(`readHitCount: could not read the counter, saw "${label}" at ${new Date().toISOString()}`) + return undefined } - return count + return Number(parsed[1]) +} + +// Results stream in, and the counter renders as soon as the first ones land - so a count read too +// early is a partial count, and stepping by it would quietly stop exercising the wrap-around. +// Settled means two reads in a row agree. +const readSettledHitCount = async (): Promise => { + let previous = await readHitCount() + for (let attempt = 0; attempt < 10; attempt++) { + await browser.pause(500) + const next = await readHitCount() + if (next !== undefined && next === previous) return next + previous = next + } + return previous } const startSearch = async (query: string): Promise => { @@ -145,7 +157,8 @@ const startSearch = async (query: string): Promise => { .then(() => true) .catch(() => false) if (!landed) throw new Error(`the first hit never came on screen: ${await describeHit()}`) - const hits = await readHitCount() + const hits = await readSettledHitCount() + if (hits === undefined) throw new Error(`could not read the hit counter while searching "${query}"`) if (hits === 0) throw new Error(`"${query}" found no hits in this conversation`) return hits } diff --git a/shared/tests/e2e/ios-appium/flows/people-profile.test.ts b/shared/tests/e2e/ios-appium/flows/people-profile.test.ts index 10142fb3bf6f..dc317b4a2673 100644 --- a/shared/tests/e2e/ios-appium/flows/people-profile.test.ts +++ b/shared/tests/e2e/ios-appium/flows/people-profile.test.ts @@ -17,19 +17,22 @@ describe('people profile', () => { // Scoped to the feed, not matched across the screen: the People header's avatar carries the // username too, and tapping that opens the account switcher rather than a profile - which then // fails here on a missing profile page, and leaves a modal up for whatever runs next. - const userEl = byTextWithin(el(T.PEOPLE_FEED), smokeUser) // The feed container mounts empty and immediately, so waiting on it says nothing about whether // the feed has arrived. Wait for the row itself instead - a bounded wait rather than the retries // this flow used to carry, which re-ran the whole test to buy the same time. + // + // Rebuilt on every poll: a scoped element caches its parent's id, so a feed that re-renders + // makes the scoped lookup throw stale forever, and the swallowed error reads as "not there". + const findUser = () => byTextWithin(el(T.PEOPLE_FEED), smokeUser) const present = await browser - .waitUntil(async () => userEl.isExisting(), {interval: 250, timeout: 10000}) + .waitUntil(async () => findUser().isExisting().catch(() => false), {interval: 250, timeout: 10000}) .then(() => true) .catch(() => false) if (!present) { - // eslint-disable-next-line no-console console.log(`people profile: ${smokeUser} is not in its own feed, skipping the profile open`) return } + const userEl = findUser() await userEl.click() await waitForTestID(T.PROFILE_PAGE, 10000) await expect(el(T.PROFILE_PAGE)).toExist() diff --git a/shared/tests/e2e/ios-appium/helpers/elements.ts b/shared/tests/e2e/ios-appium/helpers/elements.ts index ebfaafcb9ae1..39f3552e82de 100644 --- a/shared/tests/e2e/ios-appium/helpers/elements.ts +++ b/shared/tests/e2e/ios-appium/helpers/elements.ts @@ -146,6 +146,13 @@ export const anyExist = async (id: string, timeout = 3000): Promise => // Backslashes and double quotes would otherwise terminate/alter the quoted // predicate literal and make the selector invalid. +// An XPath string literal for arbitrary text. XPath 1.0 cannot escape a quote inside a literal, so +// text containing one has to be assembled with concat(). +const xpathLiteral = (s: string): string => { + if (!s.includes('"')) return `"${s}"` + return `concat(${s.split('"').map(part => `"${part}"`).join(`, '"', `)})` +} + const escapePredicate = (s: string) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"') // CONTAINS, not ==, on purpose: many tappable rows (More menu items, team tabs) @@ -168,10 +175,15 @@ export const byText = (text: string): ChainablePromiseElement => { // appears in the chrome around the content: the People header's avatar carries the signed-in // username, and tapping that opens the account switcher rather than a profile. export const byTextWithin = (root: ChainablePromiseElement, text: string): ChainablePromiseElement => { - const t = escapePredicate(text) if (browser.isAndroid) { - return root.$(`.//*[contains(@text, "${t}") or contains(@content-desc, "${t}")]`) + // XPath 1.0 has no escape for a quote, so a literal containing one has to be built with + // concat(). escapePredicate is for ObjC predicates and would emit a backslash XPath cannot read. + const literal = xpathLiteral(text) + return root.$( + `descendant-or-self::*[contains(@text, ${literal}) or contains(@content-desc, ${literal})]` + ) } + const t = escapePredicate(text) return root.$(`-ios predicate string:label CONTAINS "${t}" OR name CONTAINS "${t}"`) } diff --git a/shared/tests/e2e/ios-appium/helpers/navigate.ts b/shared/tests/e2e/ios-appium/helpers/navigate.ts index 3f1710b645f9..255d5db30c16 100644 --- a/shared/tests/e2e/ios-appium/helpers/navigate.ts +++ b/shared/tests/e2e/ios-appium/helpers/navigate.ts @@ -93,6 +93,12 @@ export async function dismissKeyboard(): Promise { await browser.execute('mobile: hideKeyboard').catch(() => {}) if (!(await browser.isKeyboardShown().catch(() => false))) return + // Naming the keys to press gets WDA past "Did not know how to dismiss the keyboard" on the + // screens whose keyboard carries one of them (search fields show Search, the team wizard's + // shows Done). + await browser.execute('mobile: hideKeyboard', {keys: ['Done', 'Search', 'Go', 'Return']}).catch(() => {}) + if (!(await browser.isKeyboardShown().catch(() => false))) return + // WDA answers "Did not know how to dismiss the keyboard" for the chat composer — it has no Done // key and no accessory to press. This is not cosmetic: while the keyboard is up the screen's own // controls stop reporting as hittable, so the back chevron is invisible to tapNavBack and the @@ -106,28 +112,61 @@ export async function dismissKeyboard(): Promise { // activate a touchable, so it has no such side effect on any screen this runs from. const {height, width} = await browser.getWindowRect() const x = Math.round(width / 2) + const keyboardGone = async () => !(await browser.isKeyboardShown().catch(() => false)) + // Start well above the keyboard: on an iPad in landscape its top edge sits around 55% of the + // screen, and an accessory or autocorrect bar raises it further. await browser .action('pointer') - .move({x, y: Math.round(height * 0.45)}) + .move({x, y: Math.round(height * 0.35)}) .down() .pause(60) - .move({duration: 250, x, y: Math.round(height * 0.25)}) + .move({duration: 250, x, y: Math.round(height * 0.15)}) .up() .perform() .catch(() => {}) - const dismissed = await browser - .waitUntil(async () => !(await browser.isKeyboardShown().catch(() => false)), { - interval: 100, - timeout: 2000, - }) - .then(() => true) + if ( + await browser + .waitUntil(keyboardGone, {interval: 100, timeout: 2000}) + .then(() => true) + .catch(() => false) + ) { + return + } + + // The drag is what the CHAT list listens for - it is the one screen that sets + // keyboardDismissMode="on-drag", and the one where a tap is unsafe because + // keyboardShouldPersistTaps="handled" lets a row swallow it and act on it. Everywhere else + // (feedback, crypto) there is no dismiss-on-drag and the default persist-taps is "never", so a + // tap is both safe and the only thing that works. Try it when this is not the chat list. + const onChatList = await el(T.CHAT_MESSAGE_LIST) + .isExisting() .catch(() => false) - if (!dismissed) { - // Say so rather than leaving escapeToTabs to spend its whole budget on a screen whose controls - // are not hittable — a 50s stall with nothing in the log to explain it. - // eslint-disable-next-line no-console - console.log(`dismissKeyboard: keyboard still up after drag at ${new Date().toISOString()}`) + if (!onChatList) { + // 30%, the same place the tap used to land before this became a drag: it is above the keyboard + // on every screen this runs from and below their headers and inputs. + await browser + .action('pointer') + .move({x, y: Math.round(height * 0.3)}) + .down() + .pause(60) + .up() + .perform() + .catch(() => {}) + if ( + await browser + .waitUntil(keyboardGone, {interval: 100, timeout: 2000}) + .then(() => true) + .catch(() => false) + ) { + return + } } + + // Say so rather than leaving escapeToTabs to spend its whole budget on a screen whose controls + // are not hittable - a 50s stall with nothing in the log to explain it. + console.log( + `dismissKeyboard: keyboard still up after drag${onChatList ? '' : ' and tap'} at ${new Date().toISOString()}` + ) } // Tap the leading (leftmost) button of a native NavigationBar — the back @@ -170,11 +209,13 @@ async function tapNavBack(requireLeftEdge = false): Promise { // visible == 1: hidden nav-stack screens and keyboard toolbars can carry their // own Done/Close/Cancel — clicking one is a silent no-op that loops forever. // The pre-loop's own predicate: an EXACT name, unlike DISMISS_PRED's substring match. This one -// clicks unattended before every test, so it must never match a button that happens to contain the +// clicks unattended before every test, so it must never match a control that merely contains the // word — a "Close team" or "Cancel invite" shipped later would otherwise become a destructive click -// in the reset. Buttons and menu items only, since a sheet's dismiss is always one of those. +// in the reset. StaticText is in the type list because the thread search bar's Cancel is a Kb.Text +// with an onClick, and on iPad that bar is the only thing the reset has to close: atTabs is already +// true inside the Chat tab, so the loop below never runs. const MODAL_DISMISS_PRED = - '-ios predicate string:(type == "XCUIElementTypeButton" OR type == "XCUIElementTypeMenuItem") AND (name == "Done" OR name == "Close" OR name == "Cancel" OR label == "Done" OR label == "Close" OR label == "Cancel") AND visible == 1' + '-ios predicate string:(type == "XCUIElementTypeButton" OR type == "XCUIElementTypeMenuItem" OR type == "XCUIElementTypeStaticText") AND (name == "Done" OR name == "Close" OR name == "Cancel" OR label == "Done" OR label == "Close" OR label == "Cancel") AND visible == 1' const DISMISS_PRED = '-ios predicate string:(label CONTAINS "Done" OR name CONTAINS "Done" OR label CONTAINS "Close" OR name CONTAINS "Close" OR label CONTAINS "Cancel" OR name CONTAINS "Cancel") AND visible == 1' @@ -239,9 +280,12 @@ export async function escapeToTabs(): Promise { const ctrl = controls[controls.length - 1]! await ctrl.click().catch(() => {}) // Waiting on atTabs here would be circular: that is the predicate this loop exists because it - // lies while a modal is up. Wait for the control itself to go. + // lies while a modal is up. Wait for THIS control to go - isDisplayed goes through the element + // id, where isExisting re-runs the selector and would answer "still there" for any other + // Done/Close/Cancel on screen (the layer behind, a second sheet, the keyboard's own toolbar). + // A stale element throws, which is the clearest "it is gone" there is. const gone = await browser - .waitUntil(async () => !(await ctrl.isExisting().catch(() => false)), {interval: 100, timeout: 3000}) + .waitUntil(async () => !(await ctrl.isDisplayed().catch(() => false)), {interval: 100, timeout: 3000}) .then(() => true) .catch(() => false) // A control that survives its own click is not a modal dismiss — leave it to the loop below @@ -258,7 +302,7 @@ export async function escapeToTabs(): Promise { // so its Done/Close/Cancel must win. if ((await browser.$$(DISMISS_PRED).length) > 0) { const ctrl = browser.$$(DISMISS_PRED)[0]! - // eslint-disable-next-line no-console + if (debug) console.log(` escapeToTabs[${i}]: dismissing "${await ctrl.getAttribute('label').catch(() => '?')}"`) await ctrl.click().catch(() => {}) await settleAfter(ctrl) @@ -266,10 +310,10 @@ export async function escapeToTabs(): Promise { // whose click no-ops (still present, still not at tabs) must fall through // to the back/pop path or we'd click it forever. if ((await atTabs()) || !(await ctrl.isExisting().catch(() => false))) continue - // eslint-disable-next-line no-console + if (debug) console.log(` escapeToTabs[${i}]: dismiss no-oped, falling through to back/pop`) } else if (debug) { - // eslint-disable-next-line no-console + console.log(` escapeToTabs[${i}]: no dismiss control, trying back/pop`) } if ((await els(T.COMMON_BACK_BUTTON).length) > 0) { From 9ee334eec78b701589f28d7bacb0226a9ed910b8 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 12 Aug 2026 14:36:03 -0400 Subject: [PATCH 19/38] fix(chat): give the native thread the resets the desktop one already had MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The patch update carries four library fixes. The borrowed-room bookkeeping now records the extent a borrow bought rather than the padding it asked for — the two differ whenever the content box swallows part of a request — so the scroll event, the committed-extent check and ScrollAdjust all subtract the same number. A correction re-aims the pending scroll by ownership instead of by comparing indices, which a prepend shifts out from under it. A plain jump to the last item settles again, rather than being refused for an end alignment scrollToIndex filled in on the caller's behalf when nothing is anchoring the end. And a second finger landing mid-drag no longer re-anchors the slop origin. On the app side, native passed dataKey={conversationIDKey}, so it never took the reset desktop takes on clearVersion — and every centered load (search hit, reply-quote jump, pinned message) clears the thread before refetching it. For the same reason lastCenteredOrdinal is now reset per dataset: re-centering on the ordinal already stored still reloads the thread, so the list has to be sent to it again. useKeyboardChatComposerInset was inert as wired — composerRef was never attached and onComposerLayout was discarded, leaving contentInsetEndAdjustment pinned at 0, which is what the prop defaults to anyway. --- shared/chat/conversation/list-area/index.tsx | 26 +- shared/patches/@legendapp+list+3.3.5.patch | 408 +++++++++++-------- 2 files changed, 256 insertions(+), 178 deletions(-) diff --git a/shared/chat/conversation/list-area/index.tsx b/shared/chat/conversation/list-area/index.tsx index b944e15e3c03..c4b88172978f 100644 --- a/shared/chat/conversation/list-area/index.tsx +++ b/shared/chat/conversation/list-area/index.tsx @@ -30,11 +30,9 @@ import {copyToClipboard} from '@/util/storeless-actions' import noop from 'lodash/noop' import {LegendList} from '@legendapp/list/react' import type {LegendListRef} from '@/common-adapters' -import type {View} from 'react-native' import {mobileTypingContainerHeight} from '../input-area/normal/typing' import { KeyboardAwareLegendList, - useKeyboardChatComposerInset, useKeyboardScrollToEnd, } from '@legendapp/list/keyboard' import {useReanimatedKeyboardAnimation} from 'react-native-keyboard-controller' @@ -228,7 +226,9 @@ const DesktopThreadWrapper = function DesktopThreadWrapper() { const {clearVersion, containsLatestMessage, messageOrdinals, loaded} = data // Centered loads (search hit, reply-quote jump, pinned message) clear the thread before - // refetching, so the list sees a non-empty -> empty -> non-empty transition. + // refetching, so the list sees a non-empty -> empty -> non-empty transition it cannot recover + // from on its own. dataKey tells it the data is a new dataset, which is what makes it reset + // rather than wait for a container layout that never comes. const datasetKey = `${conversationIDKey}:${clearVersion}` const listRef = React.useRef(null) @@ -605,7 +605,11 @@ const NativeConversationList = function NativeConversationList() { const {centeredOrdinal} = useConversationCenter() const noCenteredOrdinal = T.Chat.numberToOrdinal(-1) const centeredOrdinalOrNone = centeredOrdinal ?? noCenteredOrdinal - const {loaded, containsLatestMessage, messageOrdinals} = listData + const {clearVersion, loaded, containsLatestMessage, messageOrdinals} = listData + // Same reason as desktop: a centered load empties the thread before refilling it, and the list + // needs to be told that is a new dataset rather than left waiting on layout for rows it already + // threw away. + const datasetKey = `${conversationIDKey}:${clearVersion}` const hasCentered = centeredOrdinal !== undefined const listRef = React.useRef(null) @@ -650,12 +654,6 @@ const NativeConversationList = function NativeConversationList() { messageOrdinals, }) - // The bottom clearance for the input bar is reserved statically via contentContainerStyle - // (listContentStyle) below, so this composer inset is seeded to 0 — otherwise the two stack - // and leave a large empty gap below the newest message on cold start. composerRef is null - // (the composer lives in a sibling subtree, not this list) so measure() is never called. - const composerRef = React.useRef(null) - const {contentInsetEndAdjustment} = useKeyboardChatComposerInset(listRef, composerRef, 0) const {freeze, scrollMessageToEnd} = useKeyboardScrollToEnd({listRef}) const {scrollToCentered, scrollToBottom} = useNativeScrolling({ @@ -674,6 +672,11 @@ const NativeConversationList = function NativeConversationList() { // the list out from under someone reading around the hit. The list itself keeps the target in // place while rows measure, and maintainVisibleContentPosition holds it across prepends. const lastCenteredOrdinal = React.useRef(undefined) + // Reset per dataset, not per conversation: re-centering on the ordinal already stored still + // clears and reloads the thread, so the list has to be sent to it again. + React.useLayoutEffect(() => { + lastCenteredOrdinal.current = undefined + }, [datasetKey]) React.useEffect(() => { if (centeredOrdinalOrNone <= 0) { lastCenteredOrdinal.current = undefined @@ -732,7 +735,7 @@ const NativeConversationList = function NativeConversationList() { diff --git a/shared/patches/@legendapp+list+3.3.5.patch b/shared/patches/@legendapp+list+3.3.5.patch index d57324d3f594..385ca59b9d47 100644 --- a/shared/patches/@legendapp+list+3.3.5.patch +++ b/shared/patches/@legendapp+list+3.3.5.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js -index b3c5a30..c7f847c 100644 +index b3c5a30..ff02901 100644 --- a/node_modules/@legendapp/list/react-native.js +++ b/node_modules/@legendapp/list/react-native.js @@ -457,611 +457,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -1190,7 +1190,15 @@ index b3c5a30..c7f847c 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2256,6 +2075,15 @@ function scrollTo(ctx, params) { +@@ -2246,6 +2065,7 @@ function scrollTo(ctx, params) { + if (!noScrollingTo) { + if (isInitialScroll) { + initialScrollCompletion.resetFlags(state); ++ clearScrollTargetSettle(state); + } + const averageSizeSnapshot = getAverageSizeSnapshot(state); + state.scrollingTo = { +@@ -2256,6 +2076,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); @@ -1206,7 +1214,7 @@ index b3c5a30..c7f847c 100644 } } state.scrollPending = targetOffset; -@@ -2263,7 +2091,7 @@ function scrollTo(ctx, params) { +@@ -2263,7 +2092,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -1215,7 +1223,7 @@ index b3c5a30..c7f847c 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2276,6 +2104,323 @@ function scrollTo(ctx, params) { +@@ -2276,6 +2105,325 @@ function scrollTo(ctx, params) { } } @@ -1236,9 +1244,10 @@ index b3c5a30..c7f847c 100644 +function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; + const { index, viewOffset, viewPosition } = params; -+ const { data } = state.props; ++ const { alignItemsAtEnd, data, maintainScrollAtEnd } = state.props; ++ const hasEndAnchoring = !!maintainScrollAtEnd || !!alignItemsAtEnd; + const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; -+ if (index < 0 || index >= data.length || isEndAlignedLastItem) { ++ if (index < 0 || index >= data.length || isEndAlignedLastItem && hasEndAnchoring) { + clearScrollTargetSettle(state); + return; + } @@ -1250,6 +1259,7 @@ index b3c5a30..c7f847c 100644 + expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, ++ ownsScrollingTo: state.scrollingTo !== void 0, + quietPasses: 0, + viewOffset, + viewPosition @@ -1292,7 +1302,7 @@ index b3c5a30..c7f847c 100644 + viewPosition: settle.viewPosition + }); + const scrollingTo = state.scrollingTo; -+ if (scrollingTo && scrollingTo.index === index) { ++ if (scrollingTo && settle.ownsScrollingTo) { + const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); + scrollingTo.targetOffset = correctedOffset; + scrollingTo.offset = position; @@ -1539,7 +1549,7 @@ index b3c5a30..c7f847c 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4320,6 +4465,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4320,6 +4468,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -1547,7 +1557,7 @@ index b3c5a30..c7f847c 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4337,8 +4483,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4337,8 +4486,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -1568,7 +1578,7 @@ index b3c5a30..c7f847c 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -6001,7 +6157,12 @@ var ListComponent = typedMemo(function ListComponent2({ +@@ -6001,7 +6160,12 @@ var ListComponent = typedMemo(function ListComponent2({ SnapOrScroll, { ...rest, @@ -1582,7 +1592,7 @@ index b3c5a30..c7f847c 100644 contentContainerStyle: [ horizontal ? { height: "100%" } : {}, contentContainerStyle, -@@ -7652,6 +7813,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7652,6 +7816,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -1591,7 +1601,7 @@ index b3c5a30..c7f847c 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs -index 40e87cd..fb9e73c 100644 +index 40e87cd..dc63618 100644 --- a/node_modules/@legendapp/list/react-native.mjs +++ b/node_modules/@legendapp/list/react-native.mjs @@ -436,611 +436,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -2782,7 +2792,15 @@ index 40e87cd..fb9e73c 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2235,6 +2054,15 @@ function scrollTo(ctx, params) { +@@ -2225,6 +2044,7 @@ function scrollTo(ctx, params) { + if (!noScrollingTo) { + if (isInitialScroll) { + initialScrollCompletion.resetFlags(state); ++ clearScrollTargetSettle(state); + } + const averageSizeSnapshot = getAverageSizeSnapshot(state); + state.scrollingTo = { +@@ -2235,6 +2055,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); @@ -2798,7 +2816,7 @@ index 40e87cd..fb9e73c 100644 } } state.scrollPending = targetOffset; -@@ -2242,7 +2070,7 @@ function scrollTo(ctx, params) { +@@ -2242,7 +2071,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -2807,7 +2825,7 @@ index 40e87cd..fb9e73c 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2255,6 +2083,323 @@ function scrollTo(ctx, params) { +@@ -2255,6 +2084,325 @@ function scrollTo(ctx, params) { } } @@ -2828,9 +2846,10 @@ index 40e87cd..fb9e73c 100644 +function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; + const { index, viewOffset, viewPosition } = params; -+ const { data } = state.props; ++ const { alignItemsAtEnd, data, maintainScrollAtEnd } = state.props; ++ const hasEndAnchoring = !!maintainScrollAtEnd || !!alignItemsAtEnd; + const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; -+ if (index < 0 || index >= data.length || isEndAlignedLastItem) { ++ if (index < 0 || index >= data.length || isEndAlignedLastItem && hasEndAnchoring) { + clearScrollTargetSettle(state); + return; + } @@ -2842,6 +2861,7 @@ index 40e87cd..fb9e73c 100644 + expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, ++ ownsScrollingTo: state.scrollingTo !== void 0, + quietPasses: 0, + viewOffset, + viewPosition @@ -2884,7 +2904,7 @@ index 40e87cd..fb9e73c 100644 + viewPosition: settle.viewPosition + }); + const scrollingTo = state.scrollingTo; -+ if (scrollingTo && scrollingTo.index === index) { ++ if (scrollingTo && settle.ownsScrollingTo) { + const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); + scrollingTo.targetOffset = correctedOffset; + scrollingTo.offset = position; @@ -3131,7 +3151,7 @@ index 40e87cd..fb9e73c 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4299,6 +4444,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4299,6 +4447,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -3139,7 +3159,7 @@ index 40e87cd..fb9e73c 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4316,8 +4462,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4316,8 +4465,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -3160,7 +3180,7 @@ index 40e87cd..fb9e73c 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5980,7 +6136,12 @@ var ListComponent = typedMemo(function ListComponent2({ +@@ -5980,7 +6139,12 @@ var ListComponent = typedMemo(function ListComponent2({ SnapOrScroll, { ...rest, @@ -3174,7 +3194,7 @@ index 40e87cd..fb9e73c 100644 contentContainerStyle: [ horizontal ? { height: "100%" } : {}, contentContainerStyle, -@@ -7631,6 +7792,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7631,6 +7795,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -3183,7 +3203,7 @@ index 40e87cd..fb9e73c 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js -index 914d2da..cd500e5 100644 +index 914d2da..f97cffb 100644 --- a/node_modules/@legendapp/list/react-native.web.js +++ b/node_modules/@legendapp/list/react-native.web.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -4429,7 +4449,7 @@ index 914d2da..cd500e5 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2068,57 +1881,389 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -2068,57 +1881,392 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { ctx.state.scrollTargetPinnedRange = void 0; } } @@ -4458,6 +4478,7 @@ index 914d2da..cd500e5 100644 + if (!noScrollingTo) { + if (isInitialScroll) { + initialScrollCompletion.resetFlags(state); ++ clearScrollTargetSettle(state); + } + const averageSizeSnapshot = getAverageSizeSnapshot(state); + state.scrollingTo = { @@ -4514,9 +4535,10 @@ index 914d2da..cd500e5 100644 +function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; + const { index, viewOffset, viewPosition } = params; -+ const { data } = state.props; ++ const { alignItemsAtEnd, data, maintainScrollAtEnd } = state.props; ++ const hasEndAnchoring = !!maintainScrollAtEnd || !!alignItemsAtEnd; + const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; -+ if (index < 0 || index >= data.length || isEndAlignedLastItem) { ++ if (index < 0 || index >= data.length || isEndAlignedLastItem && hasEndAnchoring) { + clearScrollTargetSettle(state); + return; + } @@ -4528,6 +4550,7 @@ index 914d2da..cd500e5 100644 + expiresAt: now2 + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, ++ ownsScrollingTo: state.scrollingTo !== void 0, + quietPasses: 0, + viewOffset, + viewPosition @@ -4570,7 +4593,7 @@ index 914d2da..cd500e5 100644 + viewPosition: settle.viewPosition + }); + const scrollingTo = state.scrollingTo; -+ if (scrollingTo && scrollingTo.index === index) { ++ if (scrollingTo && settle.ownsScrollingTo) { + const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); + scrollingTo.targetOffset = correctedOffset; + scrollingTo.offset = position; @@ -4863,7 +4886,7 @@ index 914d2da..cd500e5 100644 } // src/core/scrollToIndex.ts -@@ -4350,6 +4495,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4350,6 +4498,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -4871,7 +4894,7 @@ index 914d2da..cd500e5 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4367,8 +4513,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4367,8 +4516,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -4892,7 +4915,7 @@ index 914d2da..cd500e5 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5927,6 +6083,21 @@ function getDocumentScrollerNode() { +@@ -5927,6 +6086,21 @@ function getDocumentScrollerNode() { } return document.scrollingElement || document.documentElement || document.body; } @@ -4914,7 +4937,7 @@ index 914d2da..cd500e5 100644 function getWindowScrollPosition() { var _a3, _b, _c, _d; if (typeof window === "undefined") { -@@ -6003,9 +6174,212 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -6003,9 +6177,219 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll }; } @@ -4935,9 +4958,14 @@ index 914d2da..cd500e5 100644 +function isOwnedByUs(node, prop, entry) { + return entry.lastApplied === "" || node.style[prop] === entry.lastApplied; +} ++function measureNodeExtent(node, prop) { ++ return prop === "paddingBottom" ? node.scrollHeight : node.scrollWidth; ++} +function applyPadding(node, prop, entry) { + const total = totalRequested(entry); ++ const before = measureNodeExtent(node, prop); + node.style[prop] = total === 0 ? entry.baseline : `${entry.baselineSize + total}px`; ++ entry.boughtTotal = Math.max(0, entry.boughtTotal + (measureNodeExtent(node, prop) - before)); + entry.lastApplied = node.style[prop]; +} +function drainPendingReleases(entry) { @@ -4975,11 +5003,13 @@ index 914d2da..cd500e5 100644 + if (entry && !isOwnedByUs(node, prop, entry)) { + entry.baseline = node.style[prop]; + entry.baselineSize = readResolvedPadding(node, prop); ++ entry.boughtTotal = 0; + } + if (!entry) { + entry = { + baseline: node.style[prop], + baselineSize: readResolvedPadding(node, prop), ++ boughtTotal: 0, + lastApplied: "", + pendingReleases: /* @__PURE__ */ new Set(), + requests: /* @__PURE__ */ new Map(), @@ -5025,7 +5055,7 @@ index 914d2da..cd500e5 100644 + if (!entry || !isOwnedByUs(node, prop, entry)) { + return 0; + } -+ return totalRequested(entry); ++ return entry.boughtTotal; +} +function releaseAllTemporaryEndPadding(node) { + const entries = entriesByNode.get(node); @@ -5127,7 +5157,7 @@ index 914d2da..cd500e5 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6055,6 +6429,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6055,6 +6439,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView contentOffset, maintainVisibleContentPosition, onScroll: onScroll2, @@ -5135,7 +5165,7 @@ index 914d2da..cd500e5 100644 onInternalScrollEnd, onMomentumScrollEnd: _onMomentumScrollEnd, showsHorizontalScrollIndicator = true, -@@ -6074,6 +6449,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6074,6 +6459,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -5147,13 +5177,11 @@ index 914d2da..cd500e5 100644 const getMaxScrollOffset = React3.useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6094,8 +6474,172 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,8 +6484,170 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); + const paddedNodeRef = React3.useRef(null); -+ const borrowedExtentsRef = React3.useRef(/* @__PURE__ */ new Map()); -+ const borrowIdRef = React3.useRef(0); + const borrowWatchRef = React3.useRef(0); + const animatedPaddingReleaseRef = React3.useRef(void 0); + const withReachableExtent = React3.useCallback( @@ -5182,14 +5210,11 @@ index 914d2da..cd500e5 100644 + run(offset); + return; + } -+ const borrowId = ++borrowIdRef.current; + const release = () => { + for (const releaseOne of releases) { + releaseOne(); + } -+ borrowedExtentsRef.current.delete(borrowId); + }; -+ borrowedExtentsRef.current.set(borrowId, Math.max(0, getMaxScrollOffset() - committedMaxOffset)); + paddedNodeRef.current = contentNode; + run(offset); + if (!animated) { @@ -5277,6 +5302,9 @@ index 914d2da..cd500e5 100644 + ); + const onPointerDown = React3.useCallback( + (event) => { ++ if (dragOriginRef.current) { ++ return; ++ } + dragOriginRef.current = ownsEvent(event) ? pointerPosition(event, void 0) : void 0; + }, + [ownsEvent] @@ -5321,7 +5349,7 @@ index 914d2da..cd500e5 100644 const scrollElement = scrollRef.current; const target = getScrollTarget(); if (!target || typeof target.scrollTo !== "function") { -@@ -6116,14 +6660,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6116,14 +6668,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); options.left = left; options.top = top; @@ -5348,7 +5376,7 @@ index 914d2da..cd500e5 100644 ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6144,13 +6695,12 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6144,13 +6703,12 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView target.scrollBy({ behavior: "auto", left: x, top: y }); }, scrollTo: (options) => { @@ -5365,16 +5393,13 @@ index 914d2da..cd500e5 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6163,7 +6713,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6163,7 +6721,12 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView if (!onScroll2 || !scrollRef.current) { return; } - const contentSize = getContentSize2(contentRef.current); + const rawContentSize = getContentSize2(contentRef.current); -+ let temporaryPadding = 0; -+ for (const bought of borrowedExtentsRef.current.values()) { -+ temporaryPadding = Math.max(temporaryPadding, bought); -+ } ++ const temporaryPadding = getTemporaryEndPadding(contentRef.current, paddingEndProp); + const contentSize = temporaryPadding ? { + height: horizontal ? rawContentSize.height : rawContentSize.height - temporaryPadding, + width: horizontal ? rawContentSize.width - temporaryPadding : rawContentSize.width @@ -5382,7 +5407,16 @@ index 914d2da..cd500e5 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6218,14 +6776,29 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6183,7 +6746,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + } + }; + onScroll2(scrollEvent); +- }, [getCurrentScrollOffset, horizontal, isWindowScroll, onScroll2]); ++ }, [getCurrentScrollOffset, horizontal, isWindowScroll, onScroll2, paddingEndProp]); + const scrollEventCoalescer = useRafCoalescer(emitScroll); + const scrollEndFallbackRef = React3.useRef(void 0); + const emitScrollEnd = React3.useCallback(() => { +@@ -6218,14 +6781,29 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] ); React3.useLayoutEffect(() => { @@ -5412,7 +5446,7 @@ index 914d2da..cd500e5 100644 if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6236,7 +6809,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6236,7 +6814,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } scrollEventCoalescer.cancel(); }; @@ -5430,7 +5464,7 @@ index 914d2da..cd500e5 100644 React3.useEffect(() => { const doScroll = () => { if (contentOffset) { -@@ -6341,6 +6923,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6341,6 +6928,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView { className: scrollViewClassName, ref: scrollRef, @@ -5438,7 +5472,7 @@ index 914d2da..cd500e5 100644 ...webProps, style: scrollViewStyle }, -@@ -6379,21 +6962,6 @@ function useValueListener$(key, callback) { +@@ -6379,21 +6967,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -5460,7 +5494,7 @@ index 914d2da..cd500e5 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6411,8 +6979,6 @@ function ScrollAdjust() { +@@ -6411,8 +6984,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3__namespace.useRef(0); const lastScrollAdjustUserOffsetRef = React3__namespace.useRef(0); @@ -5469,7 +5503,7 @@ index 914d2da..cd500e5 100644 const contentNodeRef = React3__namespace.useRef(null); const callback = React3__namespace.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6423,7 +6989,7 @@ function ScrollAdjust() { +@@ -6423,7 +6994,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -5478,7 +5512,7 @@ index 914d2da..cd500e5 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6433,34 +6999,15 @@ function ScrollAdjust() { +@@ -6433,34 +7004,15 @@ function ScrollAdjust() { const scrollBy = () => scrollAdjustBy(el, axis.x * scrollDelta, axis.y * scrollDelta); contentNodeRef.current = contentNode; if (shouldScroll && contentNode) { @@ -5516,7 +5550,7 @@ index 914d2da..cd500e5 100644 } else { scrollBy(); } -@@ -6661,7 +7208,12 @@ var ListComponent = typedMemo(function ListComponent2({ +@@ -6661,7 +7213,12 @@ var ListComponent = typedMemo(function ListComponent2({ SnapOrScroll, { ...rest, @@ -5530,7 +5564,7 @@ index 914d2da..cd500e5 100644 contentContainerStyle: [ horizontal ? { height: "100%" } : {}, contentContainerStyle, -@@ -7645,10 +8197,10 @@ function useThrottleDebounce(mode) { +@@ -7645,10 +8202,10 @@ function useThrottleDebounce(mode) { const execute = React3.useCallback( (callback, delay, ...args) => { { @@ -5544,7 +5578,7 @@ index 914d2da..cd500e5 100644 callback(...args); clearTimeoutRef(); } else { -@@ -7662,7 +8214,7 @@ function useThrottleDebounce(mode) { +@@ -7662,7 +8219,7 @@ function useThrottleDebounce(mode) { lastArgsRef.current = null; } }, @@ -5553,7 +5587,7 @@ index 914d2da..cd500e5 100644 ); } } -@@ -8288,6 +8840,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8288,6 +8845,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -5562,7 +5596,7 @@ index 914d2da..cd500e5 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs -index 95465f2..54826e8 100644 +index 95465f2..6158381 100644 --- a/node_modules/@legendapp/list/react-native.web.mjs +++ b/node_modules/@legendapp/list/react-native.web.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -6808,7 +6842,7 @@ index 95465f2..54826e8 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2047,57 +1860,389 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -2047,57 +1860,392 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { ctx.state.scrollTargetPinnedRange = void 0; } } @@ -6837,6 +6871,7 @@ index 95465f2..54826e8 100644 + if (!noScrollingTo) { + if (isInitialScroll) { + initialScrollCompletion.resetFlags(state); ++ clearScrollTargetSettle(state); + } + const averageSizeSnapshot = getAverageSizeSnapshot(state); + state.scrollingTo = { @@ -6893,9 +6928,10 @@ index 95465f2..54826e8 100644 +function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; + const { index, viewOffset, viewPosition } = params; -+ const { data } = state.props; ++ const { alignItemsAtEnd, data, maintainScrollAtEnd } = state.props; ++ const hasEndAnchoring = !!maintainScrollAtEnd || !!alignItemsAtEnd; + const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; -+ if (index < 0 || index >= data.length || isEndAlignedLastItem) { ++ if (index < 0 || index >= data.length || isEndAlignedLastItem && hasEndAnchoring) { + clearScrollTargetSettle(state); + return; + } @@ -6907,6 +6943,7 @@ index 95465f2..54826e8 100644 + expiresAt: now2 + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, ++ ownsScrollingTo: state.scrollingTo !== void 0, + quietPasses: 0, + viewOffset, + viewPosition @@ -6949,7 +6986,7 @@ index 95465f2..54826e8 100644 + viewPosition: settle.viewPosition + }); + const scrollingTo = state.scrollingTo; -+ if (scrollingTo && scrollingTo.index === index) { ++ if (scrollingTo && settle.ownsScrollingTo) { + const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); + scrollingTo.targetOffset = correctedOffset; + scrollingTo.offset = position; @@ -7242,7 +7279,7 @@ index 95465f2..54826e8 100644 } // src/core/scrollToIndex.ts -@@ -4329,6 +4474,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4329,6 +4477,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -7250,7 +7287,7 @@ index 95465f2..54826e8 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4346,8 +4492,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4346,8 +4495,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -7271,7 +7308,7 @@ index 95465f2..54826e8 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5906,6 +6062,21 @@ function getDocumentScrollerNode() { +@@ -5906,6 +6065,21 @@ function getDocumentScrollerNode() { } return document.scrollingElement || document.documentElement || document.body; } @@ -7293,7 +7330,7 @@ index 95465f2..54826e8 100644 function getWindowScrollPosition() { var _a3, _b, _c, _d; if (typeof window === "undefined") { -@@ -5982,9 +6153,212 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -5982,9 +6156,219 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll }; } @@ -7314,9 +7351,14 @@ index 95465f2..54826e8 100644 +function isOwnedByUs(node, prop, entry) { + return entry.lastApplied === "" || node.style[prop] === entry.lastApplied; +} ++function measureNodeExtent(node, prop) { ++ return prop === "paddingBottom" ? node.scrollHeight : node.scrollWidth; ++} +function applyPadding(node, prop, entry) { + const total = totalRequested(entry); ++ const before = measureNodeExtent(node, prop); + node.style[prop] = total === 0 ? entry.baseline : `${entry.baselineSize + total}px`; ++ entry.boughtTotal = Math.max(0, entry.boughtTotal + (measureNodeExtent(node, prop) - before)); + entry.lastApplied = node.style[prop]; +} +function drainPendingReleases(entry) { @@ -7354,11 +7396,13 @@ index 95465f2..54826e8 100644 + if (entry && !isOwnedByUs(node, prop, entry)) { + entry.baseline = node.style[prop]; + entry.baselineSize = readResolvedPadding(node, prop); ++ entry.boughtTotal = 0; + } + if (!entry) { + entry = { + baseline: node.style[prop], + baselineSize: readResolvedPadding(node, prop), ++ boughtTotal: 0, + lastApplied: "", + pendingReleases: /* @__PURE__ */ new Set(), + requests: /* @__PURE__ */ new Map(), @@ -7404,7 +7448,7 @@ index 95465f2..54826e8 100644 + if (!entry || !isOwnedByUs(node, prop, entry)) { + return 0; + } -+ return totalRequested(entry); ++ return entry.boughtTotal; +} +function releaseAllTemporaryEndPadding(node) { + const entries = entriesByNode.get(node); @@ -7506,7 +7550,7 @@ index 95465f2..54826e8 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6034,6 +6408,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6034,6 +6418,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ contentOffset, maintainVisibleContentPosition, onScroll: onScroll2, @@ -7514,7 +7558,7 @@ index 95465f2..54826e8 100644 onInternalScrollEnd, onMomentumScrollEnd: _onMomentumScrollEnd, showsHorizontalScrollIndicator = true, -@@ -6053,6 +6428,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6053,6 +6438,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -7526,13 +7570,11 @@ index 95465f2..54826e8 100644 const getMaxScrollOffset = useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6073,8 +6453,172 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,8 +6463,170 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); + const paddedNodeRef = useRef(null); -+ const borrowedExtentsRef = useRef(/* @__PURE__ */ new Map()); -+ const borrowIdRef = useRef(0); + const borrowWatchRef = useRef(0); + const animatedPaddingReleaseRef = useRef(void 0); + const withReachableExtent = useCallback( @@ -7561,14 +7603,11 @@ index 95465f2..54826e8 100644 + run(offset); + return; + } -+ const borrowId = ++borrowIdRef.current; + const release = () => { + for (const releaseOne of releases) { + releaseOne(); + } -+ borrowedExtentsRef.current.delete(borrowId); + }; -+ borrowedExtentsRef.current.set(borrowId, Math.max(0, getMaxScrollOffset() - committedMaxOffset)); + paddedNodeRef.current = contentNode; + run(offset); + if (!animated) { @@ -7656,6 +7695,9 @@ index 95465f2..54826e8 100644 + ); + const onPointerDown = useCallback( + (event) => { ++ if (dragOriginRef.current) { ++ return; ++ } + dragOriginRef.current = ownsEvent(event) ? pointerPosition(event, void 0) : void 0; + }, + [ownsEvent] @@ -7700,7 +7742,7 @@ index 95465f2..54826e8 100644 const scrollElement = scrollRef.current; const target = getScrollTarget(); if (!target || typeof target.scrollTo !== "function") { -@@ -6095,14 +6639,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6095,14 +6647,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); options.left = left; options.top = top; @@ -7727,7 +7769,7 @@ index 95465f2..54826e8 100644 ); useImperativeHandle(ref, () => { const api = { -@@ -6123,13 +6674,12 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6123,13 +6682,12 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ target.scrollBy({ behavior: "auto", left: x, top: y }); }, scrollTo: (options) => { @@ -7744,16 +7786,13 @@ index 95465f2..54826e8 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6142,7 +6692,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6142,7 +6700,12 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ if (!onScroll2 || !scrollRef.current) { return; } - const contentSize = getContentSize2(contentRef.current); + const rawContentSize = getContentSize2(contentRef.current); -+ let temporaryPadding = 0; -+ for (const bought of borrowedExtentsRef.current.values()) { -+ temporaryPadding = Math.max(temporaryPadding, bought); -+ } ++ const temporaryPadding = getTemporaryEndPadding(contentRef.current, paddingEndProp); + const contentSize = temporaryPadding ? { + height: horizontal ? rawContentSize.height : rawContentSize.height - temporaryPadding, + width: horizontal ? rawContentSize.width - temporaryPadding : rawContentSize.width @@ -7761,7 +7800,16 @@ index 95465f2..54826e8 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6197,14 +6755,29 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6162,7 +6725,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + } + }; + onScroll2(scrollEvent); +- }, [getCurrentScrollOffset, horizontal, isWindowScroll, onScroll2]); ++ }, [getCurrentScrollOffset, horizontal, isWindowScroll, onScroll2, paddingEndProp]); + const scrollEventCoalescer = useRafCoalescer(emitScroll); + const scrollEndFallbackRef = useRef(void 0); + const emitScrollEnd = useCallback(() => { +@@ -6197,14 +6760,29 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] ); useLayoutEffect(() => { @@ -7791,7 +7839,7 @@ index 95465f2..54826e8 100644 if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6215,7 +6788,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6215,7 +6793,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } scrollEventCoalescer.cancel(); }; @@ -7809,7 +7857,7 @@ index 95465f2..54826e8 100644 useEffect(() => { const doScroll = () => { if (contentOffset) { -@@ -6320,6 +6902,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6320,6 +6907,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ { className: scrollViewClassName, ref: scrollRef, @@ -7817,7 +7865,7 @@ index 95465f2..54826e8 100644 ...webProps, style: scrollViewStyle }, -@@ -6358,21 +6941,6 @@ function useValueListener$(key, callback) { +@@ -6358,21 +6946,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -7839,7 +7887,7 @@ index 95465f2..54826e8 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6390,8 +6958,6 @@ function ScrollAdjust() { +@@ -6390,8 +6963,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3.useRef(0); const lastScrollAdjustUserOffsetRef = React3.useRef(0); @@ -7848,7 +7896,7 @@ index 95465f2..54826e8 100644 const contentNodeRef = React3.useRef(null); const callback = React3.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6402,7 +6968,7 @@ function ScrollAdjust() { +@@ -6402,7 +6973,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -7857,7 +7905,7 @@ index 95465f2..54826e8 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6412,34 +6978,15 @@ function ScrollAdjust() { +@@ -6412,34 +6983,15 @@ function ScrollAdjust() { const scrollBy = () => scrollAdjustBy(el, axis.x * scrollDelta, axis.y * scrollDelta); contentNodeRef.current = contentNode; if (shouldScroll && contentNode) { @@ -7895,7 +7943,7 @@ index 95465f2..54826e8 100644 } else { scrollBy(); } -@@ -6640,7 +7187,12 @@ var ListComponent = typedMemo(function ListComponent2({ +@@ -6640,7 +7192,12 @@ var ListComponent = typedMemo(function ListComponent2({ SnapOrScroll, { ...rest, @@ -7909,7 +7957,7 @@ index 95465f2..54826e8 100644 contentContainerStyle: [ horizontal ? { height: "100%" } : {}, contentContainerStyle, -@@ -7624,10 +8176,10 @@ function useThrottleDebounce(mode) { +@@ -7624,10 +8181,10 @@ function useThrottleDebounce(mode) { const execute = useCallback( (callback, delay, ...args) => { { @@ -7923,7 +7971,7 @@ index 95465f2..54826e8 100644 callback(...args); clearTimeoutRef(); } else { -@@ -7641,7 +8193,7 @@ function useThrottleDebounce(mode) { +@@ -7641,7 +8198,7 @@ function useThrottleDebounce(mode) { lastArgsRef.current = null; } }, @@ -7932,7 +7980,7 @@ index 95465f2..54826e8 100644 ); } } -@@ -8267,6 +8819,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8267,6 +8824,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -7941,7 +7989,7 @@ index 95465f2..54826e8 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js -index 914d2da..cd500e5 100644 +index 914d2da..f97cffb 100644 --- a/node_modules/@legendapp/list/react.js +++ b/node_modules/@legendapp/list/react.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -9187,7 +9235,7 @@ index 914d2da..cd500e5 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2068,57 +1881,389 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -2068,57 +1881,392 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { ctx.state.scrollTargetPinnedRange = void 0; } } @@ -9216,6 +9264,7 @@ index 914d2da..cd500e5 100644 + if (!noScrollingTo) { + if (isInitialScroll) { + initialScrollCompletion.resetFlags(state); ++ clearScrollTargetSettle(state); + } + const averageSizeSnapshot = getAverageSizeSnapshot(state); + state.scrollingTo = { @@ -9272,9 +9321,10 @@ index 914d2da..cd500e5 100644 +function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; + const { index, viewOffset, viewPosition } = params; -+ const { data } = state.props; ++ const { alignItemsAtEnd, data, maintainScrollAtEnd } = state.props; ++ const hasEndAnchoring = !!maintainScrollAtEnd || !!alignItemsAtEnd; + const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; -+ if (index < 0 || index >= data.length || isEndAlignedLastItem) { ++ if (index < 0 || index >= data.length || isEndAlignedLastItem && hasEndAnchoring) { + clearScrollTargetSettle(state); + return; + } @@ -9286,6 +9336,7 @@ index 914d2da..cd500e5 100644 + expiresAt: now2 + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, ++ ownsScrollingTo: state.scrollingTo !== void 0, + quietPasses: 0, + viewOffset, + viewPosition @@ -9328,7 +9379,7 @@ index 914d2da..cd500e5 100644 + viewPosition: settle.viewPosition + }); + const scrollingTo = state.scrollingTo; -+ if (scrollingTo && scrollingTo.index === index) { ++ if (scrollingTo && settle.ownsScrollingTo) { + const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); + scrollingTo.targetOffset = correctedOffset; + scrollingTo.offset = position; @@ -9621,7 +9672,7 @@ index 914d2da..cd500e5 100644 } // src/core/scrollToIndex.ts -@@ -4350,6 +4495,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4350,6 +4498,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -9629,7 +9680,7 @@ index 914d2da..cd500e5 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4367,8 +4513,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4367,8 +4516,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -9650,7 +9701,7 @@ index 914d2da..cd500e5 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5927,6 +6083,21 @@ function getDocumentScrollerNode() { +@@ -5927,6 +6086,21 @@ function getDocumentScrollerNode() { } return document.scrollingElement || document.documentElement || document.body; } @@ -9672,7 +9723,7 @@ index 914d2da..cd500e5 100644 function getWindowScrollPosition() { var _a3, _b, _c, _d; if (typeof window === "undefined") { -@@ -6003,9 +6174,212 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -6003,9 +6177,219 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll }; } @@ -9693,9 +9744,14 @@ index 914d2da..cd500e5 100644 +function isOwnedByUs(node, prop, entry) { + return entry.lastApplied === "" || node.style[prop] === entry.lastApplied; +} ++function measureNodeExtent(node, prop) { ++ return prop === "paddingBottom" ? node.scrollHeight : node.scrollWidth; ++} +function applyPadding(node, prop, entry) { + const total = totalRequested(entry); ++ const before = measureNodeExtent(node, prop); + node.style[prop] = total === 0 ? entry.baseline : `${entry.baselineSize + total}px`; ++ entry.boughtTotal = Math.max(0, entry.boughtTotal + (measureNodeExtent(node, prop) - before)); + entry.lastApplied = node.style[prop]; +} +function drainPendingReleases(entry) { @@ -9733,11 +9789,13 @@ index 914d2da..cd500e5 100644 + if (entry && !isOwnedByUs(node, prop, entry)) { + entry.baseline = node.style[prop]; + entry.baselineSize = readResolvedPadding(node, prop); ++ entry.boughtTotal = 0; + } + if (!entry) { + entry = { + baseline: node.style[prop], + baselineSize: readResolvedPadding(node, prop), ++ boughtTotal: 0, + lastApplied: "", + pendingReleases: /* @__PURE__ */ new Set(), + requests: /* @__PURE__ */ new Map(), @@ -9783,7 +9841,7 @@ index 914d2da..cd500e5 100644 + if (!entry || !isOwnedByUs(node, prop, entry)) { + return 0; + } -+ return totalRequested(entry); ++ return entry.boughtTotal; +} +function releaseAllTemporaryEndPadding(node) { + const entries = entriesByNode.get(node); @@ -9885,7 +9943,7 @@ index 914d2da..cd500e5 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6055,6 +6429,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6055,6 +6439,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView contentOffset, maintainVisibleContentPosition, onScroll: onScroll2, @@ -9893,7 +9951,7 @@ index 914d2da..cd500e5 100644 onInternalScrollEnd, onMomentumScrollEnd: _onMomentumScrollEnd, showsHorizontalScrollIndicator = true, -@@ -6074,6 +6449,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6074,6 +6459,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -9905,13 +9963,11 @@ index 914d2da..cd500e5 100644 const getMaxScrollOffset = React3.useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6094,8 +6474,172 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,8 +6484,170 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); + const paddedNodeRef = React3.useRef(null); -+ const borrowedExtentsRef = React3.useRef(/* @__PURE__ */ new Map()); -+ const borrowIdRef = React3.useRef(0); + const borrowWatchRef = React3.useRef(0); + const animatedPaddingReleaseRef = React3.useRef(void 0); + const withReachableExtent = React3.useCallback( @@ -9940,14 +9996,11 @@ index 914d2da..cd500e5 100644 + run(offset); + return; + } -+ const borrowId = ++borrowIdRef.current; + const release = () => { + for (const releaseOne of releases) { + releaseOne(); + } -+ borrowedExtentsRef.current.delete(borrowId); + }; -+ borrowedExtentsRef.current.set(borrowId, Math.max(0, getMaxScrollOffset() - committedMaxOffset)); + paddedNodeRef.current = contentNode; + run(offset); + if (!animated) { @@ -10035,6 +10088,9 @@ index 914d2da..cd500e5 100644 + ); + const onPointerDown = React3.useCallback( + (event) => { ++ if (dragOriginRef.current) { ++ return; ++ } + dragOriginRef.current = ownsEvent(event) ? pointerPosition(event, void 0) : void 0; + }, + [ownsEvent] @@ -10079,7 +10135,7 @@ index 914d2da..cd500e5 100644 const scrollElement = scrollRef.current; const target = getScrollTarget(); if (!target || typeof target.scrollTo !== "function") { -@@ -6116,14 +6660,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6116,14 +6668,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); options.left = left; options.top = top; @@ -10106,7 +10162,7 @@ index 914d2da..cd500e5 100644 ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6144,13 +6695,12 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6144,13 +6703,12 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView target.scrollBy({ behavior: "auto", left: x, top: y }); }, scrollTo: (options) => { @@ -10123,16 +10179,13 @@ index 914d2da..cd500e5 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6163,7 +6713,15 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6163,7 +6721,12 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView if (!onScroll2 || !scrollRef.current) { return; } - const contentSize = getContentSize2(contentRef.current); + const rawContentSize = getContentSize2(contentRef.current); -+ let temporaryPadding = 0; -+ for (const bought of borrowedExtentsRef.current.values()) { -+ temporaryPadding = Math.max(temporaryPadding, bought); -+ } ++ const temporaryPadding = getTemporaryEndPadding(contentRef.current, paddingEndProp); + const contentSize = temporaryPadding ? { + height: horizontal ? rawContentSize.height : rawContentSize.height - temporaryPadding, + width: horizontal ? rawContentSize.width - temporaryPadding : rawContentSize.width @@ -10140,7 +10193,16 @@ index 914d2da..cd500e5 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6218,14 +6776,29 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6183,7 +6746,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + } + }; + onScroll2(scrollEvent); +- }, [getCurrentScrollOffset, horizontal, isWindowScroll, onScroll2]); ++ }, [getCurrentScrollOffset, horizontal, isWindowScroll, onScroll2, paddingEndProp]); + const scrollEventCoalescer = useRafCoalescer(emitScroll); + const scrollEndFallbackRef = React3.useRef(void 0); + const emitScrollEnd = React3.useCallback(() => { +@@ -6218,14 +6781,29 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] ); React3.useLayoutEffect(() => { @@ -10170,7 +10232,7 @@ index 914d2da..cd500e5 100644 if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6236,7 +6809,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6236,7 +6814,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } scrollEventCoalescer.cancel(); }; @@ -10188,7 +10250,7 @@ index 914d2da..cd500e5 100644 React3.useEffect(() => { const doScroll = () => { if (contentOffset) { -@@ -6341,6 +6923,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6341,6 +6928,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView { className: scrollViewClassName, ref: scrollRef, @@ -10196,7 +10258,7 @@ index 914d2da..cd500e5 100644 ...webProps, style: scrollViewStyle }, -@@ -6379,21 +6962,6 @@ function useValueListener$(key, callback) { +@@ -6379,21 +6967,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -10218,7 +10280,7 @@ index 914d2da..cd500e5 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6411,8 +6979,6 @@ function ScrollAdjust() { +@@ -6411,8 +6984,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3__namespace.useRef(0); const lastScrollAdjustUserOffsetRef = React3__namespace.useRef(0); @@ -10227,7 +10289,7 @@ index 914d2da..cd500e5 100644 const contentNodeRef = React3__namespace.useRef(null); const callback = React3__namespace.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6423,7 +6989,7 @@ function ScrollAdjust() { +@@ -6423,7 +6994,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -10236,7 +10298,7 @@ index 914d2da..cd500e5 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6433,34 +6999,15 @@ function ScrollAdjust() { +@@ -6433,34 +7004,15 @@ function ScrollAdjust() { const scrollBy = () => scrollAdjustBy(el, axis.x * scrollDelta, axis.y * scrollDelta); contentNodeRef.current = contentNode; if (shouldScroll && contentNode) { @@ -10274,7 +10336,7 @@ index 914d2da..cd500e5 100644 } else { scrollBy(); } -@@ -6661,7 +7208,12 @@ var ListComponent = typedMemo(function ListComponent2({ +@@ -6661,7 +7213,12 @@ var ListComponent = typedMemo(function ListComponent2({ SnapOrScroll, { ...rest, @@ -10288,7 +10350,7 @@ index 914d2da..cd500e5 100644 contentContainerStyle: [ horizontal ? { height: "100%" } : {}, contentContainerStyle, -@@ -7645,10 +8197,10 @@ function useThrottleDebounce(mode) { +@@ -7645,10 +8202,10 @@ function useThrottleDebounce(mode) { const execute = React3.useCallback( (callback, delay, ...args) => { { @@ -10302,7 +10364,7 @@ index 914d2da..cd500e5 100644 callback(...args); clearTimeoutRef(); } else { -@@ -7662,7 +8214,7 @@ function useThrottleDebounce(mode) { +@@ -7662,7 +8219,7 @@ function useThrottleDebounce(mode) { lastArgsRef.current = null; } }, @@ -10311,7 +10373,7 @@ index 914d2da..cd500e5 100644 ); } } -@@ -8288,6 +8840,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8288,6 +8845,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -10320,7 +10382,7 @@ index 914d2da..cd500e5 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs -index 95465f2..54826e8 100644 +index 95465f2..6158381 100644 --- a/node_modules/@legendapp/list/react.mjs +++ b/node_modules/@legendapp/list/react.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -11566,7 +11628,7 @@ index 95465f2..54826e8 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2047,57 +1860,389 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -2047,57 +1860,392 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { ctx.state.scrollTargetPinnedRange = void 0; } } @@ -11595,6 +11657,7 @@ index 95465f2..54826e8 100644 + if (!noScrollingTo) { + if (isInitialScroll) { + initialScrollCompletion.resetFlags(state); ++ clearScrollTargetSettle(state); + } + const averageSizeSnapshot = getAverageSizeSnapshot(state); + state.scrollingTo = { @@ -11651,9 +11714,10 @@ index 95465f2..54826e8 100644 +function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; + const { index, viewOffset, viewPosition } = params; -+ const { data } = state.props; ++ const { alignItemsAtEnd, data, maintainScrollAtEnd } = state.props; ++ const hasEndAnchoring = !!maintainScrollAtEnd || !!alignItemsAtEnd; + const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; -+ if (index < 0 || index >= data.length || isEndAlignedLastItem) { ++ if (index < 0 || index >= data.length || isEndAlignedLastItem && hasEndAnchoring) { + clearScrollTargetSettle(state); + return; + } @@ -11665,6 +11729,7 @@ index 95465f2..54826e8 100644 + expiresAt: now2 + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, ++ ownsScrollingTo: state.scrollingTo !== void 0, + quietPasses: 0, + viewOffset, + viewPosition @@ -11707,7 +11772,7 @@ index 95465f2..54826e8 100644 + viewPosition: settle.viewPosition + }); + const scrollingTo = state.scrollingTo; -+ if (scrollingTo && scrollingTo.index === index) { ++ if (scrollingTo && settle.ownsScrollingTo) { + const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); + scrollingTo.targetOffset = correctedOffset; + scrollingTo.offset = position; @@ -12000,7 +12065,7 @@ index 95465f2..54826e8 100644 } // src/core/scrollToIndex.ts -@@ -4329,6 +4474,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4329,6 +4477,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -12008,7 +12073,7 @@ index 95465f2..54826e8 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4346,8 +4492,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4346,8 +4495,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -12029,7 +12094,7 @@ index 95465f2..54826e8 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5906,6 +6062,21 @@ function getDocumentScrollerNode() { +@@ -5906,6 +6065,21 @@ function getDocumentScrollerNode() { } return document.scrollingElement || document.documentElement || document.body; } @@ -12051,7 +12116,7 @@ index 95465f2..54826e8 100644 function getWindowScrollPosition() { var _a3, _b, _c, _d; if (typeof window === "undefined") { -@@ -5982,9 +6153,212 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -5982,9 +6156,219 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll }; } @@ -12072,9 +12137,14 @@ index 95465f2..54826e8 100644 +function isOwnedByUs(node, prop, entry) { + return entry.lastApplied === "" || node.style[prop] === entry.lastApplied; +} ++function measureNodeExtent(node, prop) { ++ return prop === "paddingBottom" ? node.scrollHeight : node.scrollWidth; ++} +function applyPadding(node, prop, entry) { + const total = totalRequested(entry); ++ const before = measureNodeExtent(node, prop); + node.style[prop] = total === 0 ? entry.baseline : `${entry.baselineSize + total}px`; ++ entry.boughtTotal = Math.max(0, entry.boughtTotal + (measureNodeExtent(node, prop) - before)); + entry.lastApplied = node.style[prop]; +} +function drainPendingReleases(entry) { @@ -12112,11 +12182,13 @@ index 95465f2..54826e8 100644 + if (entry && !isOwnedByUs(node, prop, entry)) { + entry.baseline = node.style[prop]; + entry.baselineSize = readResolvedPadding(node, prop); ++ entry.boughtTotal = 0; + } + if (!entry) { + entry = { + baseline: node.style[prop], + baselineSize: readResolvedPadding(node, prop), ++ boughtTotal: 0, + lastApplied: "", + pendingReleases: /* @__PURE__ */ new Set(), + requests: /* @__PURE__ */ new Map(), @@ -12162,7 +12234,7 @@ index 95465f2..54826e8 100644 + if (!entry || !isOwnedByUs(node, prop, entry)) { + return 0; + } -+ return totalRequested(entry); ++ return entry.boughtTotal; +} +function releaseAllTemporaryEndPadding(node) { + const entries = entriesByNode.get(node); @@ -12264,7 +12336,7 @@ index 95465f2..54826e8 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6034,6 +6408,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6034,6 +6418,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ contentOffset, maintainVisibleContentPosition, onScroll: onScroll2, @@ -12272,7 +12344,7 @@ index 95465f2..54826e8 100644 onInternalScrollEnd, onMomentumScrollEnd: _onMomentumScrollEnd, showsHorizontalScrollIndicator = true, -@@ -6053,6 +6428,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6053,6 +6438,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -12284,13 +12356,11 @@ index 95465f2..54826e8 100644 const getMaxScrollOffset = useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6073,8 +6453,172 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,8 +6463,170 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); + const paddedNodeRef = useRef(null); -+ const borrowedExtentsRef = useRef(/* @__PURE__ */ new Map()); -+ const borrowIdRef = useRef(0); + const borrowWatchRef = useRef(0); + const animatedPaddingReleaseRef = useRef(void 0); + const withReachableExtent = useCallback( @@ -12319,14 +12389,11 @@ index 95465f2..54826e8 100644 + run(offset); + return; + } -+ const borrowId = ++borrowIdRef.current; + const release = () => { + for (const releaseOne of releases) { + releaseOne(); + } -+ borrowedExtentsRef.current.delete(borrowId); + }; -+ borrowedExtentsRef.current.set(borrowId, Math.max(0, getMaxScrollOffset() - committedMaxOffset)); + paddedNodeRef.current = contentNode; + run(offset); + if (!animated) { @@ -12414,6 +12481,9 @@ index 95465f2..54826e8 100644 + ); + const onPointerDown = useCallback( + (event) => { ++ if (dragOriginRef.current) { ++ return; ++ } + dragOriginRef.current = ownsEvent(event) ? pointerPosition(event, void 0) : void 0; + }, + [ownsEvent] @@ -12458,7 +12528,7 @@ index 95465f2..54826e8 100644 const scrollElement = scrollRef.current; const target = getScrollTarget(); if (!target || typeof target.scrollTo !== "function") { -@@ -6095,14 +6639,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6095,14 +6647,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); options.left = left; options.top = top; @@ -12485,7 +12555,7 @@ index 95465f2..54826e8 100644 ); useImperativeHandle(ref, () => { const api = { -@@ -6123,13 +6674,12 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6123,13 +6682,12 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ target.scrollBy({ behavior: "auto", left: x, top: y }); }, scrollTo: (options) => { @@ -12502,16 +12572,13 @@ index 95465f2..54826e8 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6142,7 +6692,15 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6142,7 +6700,12 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ if (!onScroll2 || !scrollRef.current) { return; } - const contentSize = getContentSize2(contentRef.current); + const rawContentSize = getContentSize2(contentRef.current); -+ let temporaryPadding = 0; -+ for (const bought of borrowedExtentsRef.current.values()) { -+ temporaryPadding = Math.max(temporaryPadding, bought); -+ } ++ const temporaryPadding = getTemporaryEndPadding(contentRef.current, paddingEndProp); + const contentSize = temporaryPadding ? { + height: horizontal ? rawContentSize.height : rawContentSize.height - temporaryPadding, + width: horizontal ? rawContentSize.width - temporaryPadding : rawContentSize.width @@ -12519,7 +12586,16 @@ index 95465f2..54826e8 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6197,14 +6755,29 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6162,7 +6725,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + } + }; + onScroll2(scrollEvent); +- }, [getCurrentScrollOffset, horizontal, isWindowScroll, onScroll2]); ++ }, [getCurrentScrollOffset, horizontal, isWindowScroll, onScroll2, paddingEndProp]); + const scrollEventCoalescer = useRafCoalescer(emitScroll); + const scrollEndFallbackRef = useRef(void 0); + const emitScrollEnd = useCallback(() => { +@@ -6197,14 +6760,29 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] ); useLayoutEffect(() => { @@ -12549,7 +12625,7 @@ index 95465f2..54826e8 100644 if ("onscrollend" in target) { target.removeEventListener("scrollend", emitScrollEnd); } -@@ -6215,7 +6788,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6215,7 +6793,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } scrollEventCoalescer.cancel(); }; @@ -12567,7 +12643,7 @@ index 95465f2..54826e8 100644 useEffect(() => { const doScroll = () => { if (contentOffset) { -@@ -6320,6 +6902,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6320,6 +6907,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ { className: scrollViewClassName, ref: scrollRef, @@ -12575,7 +12651,7 @@ index 95465f2..54826e8 100644 ...webProps, style: scrollViewStyle }, -@@ -6358,21 +6941,6 @@ function useValueListener$(key, callback) { +@@ -6358,21 +6946,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -12597,7 +12673,7 @@ index 95465f2..54826e8 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6390,8 +6958,6 @@ function ScrollAdjust() { +@@ -6390,8 +6963,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3.useRef(0); const lastScrollAdjustUserOffsetRef = React3.useRef(0); @@ -12606,7 +12682,7 @@ index 95465f2..54826e8 100644 const contentNodeRef = React3.useRef(null); const callback = React3.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6402,7 +6968,7 @@ function ScrollAdjust() { +@@ -6402,7 +6973,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -12615,7 +12691,7 @@ index 95465f2..54826e8 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6412,34 +6978,15 @@ function ScrollAdjust() { +@@ -6412,34 +6983,15 @@ function ScrollAdjust() { const scrollBy = () => scrollAdjustBy(el, axis.x * scrollDelta, axis.y * scrollDelta); contentNodeRef.current = contentNode; if (shouldScroll && contentNode) { @@ -12653,7 +12729,7 @@ index 95465f2..54826e8 100644 } else { scrollBy(); } -@@ -6640,7 +7187,12 @@ var ListComponent = typedMemo(function ListComponent2({ +@@ -6640,7 +7192,12 @@ var ListComponent = typedMemo(function ListComponent2({ SnapOrScroll, { ...rest, @@ -12667,7 +12743,7 @@ index 95465f2..54826e8 100644 contentContainerStyle: [ horizontal ? { height: "100%" } : {}, contentContainerStyle, -@@ -7624,10 +8176,10 @@ function useThrottleDebounce(mode) { +@@ -7624,10 +8181,10 @@ function useThrottleDebounce(mode) { const execute = useCallback( (callback, delay, ...args) => { { @@ -12681,7 +12757,7 @@ index 95465f2..54826e8 100644 callback(...args); clearTimeoutRef(); } else { -@@ -7641,7 +8193,7 @@ function useThrottleDebounce(mode) { +@@ -7641,7 +8198,7 @@ function useThrottleDebounce(mode) { lastArgsRef.current = null; } }, @@ -12690,7 +12766,7 @@ index 95465f2..54826e8 100644 ); } } -@@ -8267,6 +8819,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8267,6 +8824,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); From 72187587abd2b0b443644177c2cdfd2c728ef227 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 12 Aug 2026 14:36:14 -0400 Subject: [PATCH 20/38] test(e2e): cover the desktop search cases, and stop two checks from lying MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The desktop thread-search cases were manual: search a thread, step every hit, and confirm the list is left where the reader drags it. They are a flow now. The header's search icon carried a testID only on the mobile branch, so the desktop one was unreachable; it has the same testID now. The click needs force — the conversation header is inside the window's drag region, which makes playwright's actionability check wait forever on a control that is perfectly clickable. Two existing checks passed without checking anything. The snap-back assertion read the hit's resting position right after a drag whose whole job is to push the row out of view, so when the row was unmounted the travel check was skipped and the test fell back to visibility alone — which is the half that already had a check. It now treats the row coming back into the render window as the same failure. And escapeToTabs matched XCUIElementTypeStaticText by exact name, which also matches a chat message whose whole body is "Cancel" — with the suite parked in a thread, before every test. The one StaticText that needed dismissing was the thread search bar, which is now closed by its own testID instead. --- shared/chat/inbox-and-conversation-header.tsx | 8 +- .../electron/flows/chat-search-hit.test.ts | 76 +++++++++++++++++++ .../ios-appium/flows/chat-search-hit.test.ts | 12 ++- .../tests/e2e/ios-appium/helpers/navigate.ts | 21 +++-- 4 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 shared/tests/e2e/electron/flows/chat-search-hit.test.ts diff --git a/shared/chat/inbox-and-conversation-header.tsx b/shared/chat/inbox-and-conversation-header.tsx index c37177055a14..4805c487fb35 100644 --- a/shared/chat/inbox-and-conversation-header.tsx +++ b/shared/chat/inbox-and-conversation-header.tsx @@ -16,6 +16,7 @@ import {navToPath} from '@/constants/fs' import {showConversationInfoPanel, toggleConversationThreadSearch} from '@/chat/conversation/thread-context' import {muteConversation} from '@/chat/conversation/status-actions' import AccountSwitchHeaderAvatar from '@/router-v2/account-switch-header-avatar' +import * as TestIDs from '@/tests/e2e/shared/test-ids' const emptyMeta = Chat.makeConversationMeta() const emptyParticipantInfo = Chat.uiParticipantsToParticipantInfo([]) @@ -245,7 +246,12 @@ const Header = () => { direction="vertical" tooltip={`Search in this chat (${C.shortcutSymbol}F)`} > - + { + test.setTimeout(120_000) + expect(await openFirstConversation(page)).toBe(true) + + // force: the conversation header sits in the window's WebkitAppRegion drag region, which makes + // playwright's actionability check wait forever on a control that is perfectly clickable. + await page.getByTestId(T.CHAT_HEADER_SEARCH_BUTTON).first().click({force: true}) + await page.waitForTimeout(1_000) + // A word common enough to hit repeatedly in any conversation with history. + await page.keyboard.type('the') + await page.waitForTimeout(4_000) + + // Enter steps to the next hit, wrapping around at the end — which is the case that used to land + // off screen, since wrapping jumps the furthest. + let checked = 0 + for (let i = 0; i < 12; i++) { + await page.keyboard.press('Enter') + await page.waitForTimeout(900) + const hit = await page + .getByTestId(T.CHAT_SEARCH_HIT) + .first() + .boundingBox({timeout: 3_000}) + .catch(() => null) + const list = await page.getByTestId(T.CHAT_MESSAGE_LIST).first().boundingBox() + if (!hit || !list) continue + checked++ + const onScreen = hit.y + hit.height > list.y && hit.y < list.y + list.height + expect( + onScreen, + `step ${i}: hit at y=${Math.round(hit.y)} h=${Math.round(hit.height)} is outside the list (y=${Math.round(list.y)} h=${Math.round(list.height)})` + ).toBe(true) + } + // Without this the loop above passes by never measuring anything. + expect(checked, 'no hit was ever measurable, so nothing was checked').toBeGreaterThan(0) + + const listBox = await page.getByTestId(T.CHAT_MESSAGE_LIST).first().boundingBox() + expect(listBox).not.toBeNull() + await page.mouse.move(listBox!.x + listBox!.width / 2, listBox!.y + listBox!.height / 2) + for (let i = 0; i < 10; i++) { + await page.mouse.wheel(0, -600) + await page.waitForTimeout(150) + } + await page.waitForTimeout(1_500) + + const readHit = async () => + page + .getByTestId(T.CHAT_SEARCH_HIT) + .first() + .boundingBox({timeout: 3_000}) + .catch(() => null) + + // Watch rather than look once: a re-centre lands whenever the rows scrolled past finish + // measuring, which is after the gesture rather than during it. + const settled = await readHit() + await page.waitForTimeout(3_000) + const after = await readHit() + + if (settled && after) { + const moved = Math.abs(after.y - settled.y) + expect(moved, `the thread scrolled itself ${Math.round(moved)}px back toward the hit`).toBeLessThanOrEqual(8) + } else if (!settled && after) { + // Scrolled far enough to unmount the row, and then it came back — which only a scroll does. + throw new Error('the hit came back into the render window after being scrolled away from') + } +}) diff --git a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts index 8f0b53e61453..e63472bcaa68 100644 --- a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts +++ b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts @@ -318,6 +318,9 @@ describe('chat thread search', function () { // Settle where the reader left it, and watch rather than look once: a re-centre lands whenever // the page finishes measuring, and it does not necessarily stay. await dragUntilHitLeaves() + // Undefined means the drag pushed the row clean out of the render window, which is the usual + // outcome — the drags keep going until it is off screen, and off screen far enough is unmounted. + // The samples below handle both cases rather than skipping the check in one of them. const restingPosition = (await maybeBoundsOf(el(T.CHAT_SEARCH_HIT)))?.y let snappedBack: string | undefined @@ -332,7 +335,14 @@ describe('chat thread search', function () { // travelled back toward it does not. maintainVisibleContentPosition holds the visible content // in place across a page-in, so a row that marches back moved because something scrolled. const position = (await maybeBoundsOf(el(T.CHAT_SEARCH_HIT)))?.y - if (restingPosition !== undefined && position !== undefined) { + if (restingPosition === undefined) { + // It was outside the render window when the reader stopped dragging. Coming back into it is + // itself the movement this is watching for: a prepend does not carry a row toward the + // viewport, so something scrolled to bring it back within drawDistance of it. + if (position !== undefined) { + snappedBack = `the hit came back into the render window at ${position} after being dragged out of it` + } + } else if (position !== undefined) { const travelled = Math.abs(position - restingPosition) if (travelled > SNAP_BACK_TOLERANCE) { snappedBack = `the hit moved ${Math.round(travelled)} back toward the viewport (${restingPosition} -> ${position})` diff --git a/shared/tests/e2e/ios-appium/helpers/navigate.ts b/shared/tests/e2e/ios-appium/helpers/navigate.ts index 255d5db30c16..f1a267c7e368 100644 --- a/shared/tests/e2e/ios-appium/helpers/navigate.ts +++ b/shared/tests/e2e/ios-appium/helpers/navigate.ts @@ -211,11 +211,12 @@ async function tapNavBack(requireLeftEdge = false): Promise { // The pre-loop's own predicate: an EXACT name, unlike DISMISS_PRED's substring match. This one // clicks unattended before every test, so it must never match a control that merely contains the // word — a "Close team" or "Cancel invite" shipped later would otherwise become a destructive click -// in the reset. StaticText is in the type list because the thread search bar's Cancel is a Kb.Text -// with an onClick, and on iPad that bar is the only thing the reset has to close: atTabs is already -// true inside the Chat tab, so the loop below never runs. +// in the reset. Buttons and menu items only: a StaticText matching by name would also match a chat +// message whose whole body is "Cancel", and the reset runs with the suite parked in a thread. The +// one StaticText that did need dismissing — the thread search bar's Cancel, a Kb.Text with an +// onClick — is closed by its own testID above instead. const MODAL_DISMISS_PRED = - '-ios predicate string:(type == "XCUIElementTypeButton" OR type == "XCUIElementTypeMenuItem" OR type == "XCUIElementTypeStaticText") AND (name == "Done" OR name == "Close" OR name == "Cancel" OR label == "Done" OR label == "Close" OR label == "Cancel") AND visible == 1' + '-ios predicate string:(type == "XCUIElementTypeButton" OR type == "XCUIElementTypeMenuItem") AND (name == "Done" OR name == "Close" OR name == "Cancel" OR label == "Done" OR label == "Close" OR label == "Cancel") AND visible == 1' const DISMISS_PRED = '-ios predicate string:(label CONTAINS "Done" OR name CONTAINS "Done" OR label CONTAINS "Close" OR name CONTAINS "Close" OR label CONTAINS "Cancel" OR name CONTAINS "Cancel") AND visible == 1' @@ -271,6 +272,15 @@ export async function escapeToTabs(): Promise { // fails somewhere unrelated. It outlives the run too: the app restores its last screen, so a // leaked modal wedges the NEXT run from its first test. Bounded, and only ever clicks a control // that is on screen — at a real root there is nothing to click and this costs one query. + // The thread search bar first, by its own testID. It is a Kb.Text with an onClick rather than a + // button, so nothing in MODAL_DISMISS_PRED reaches it, and on iPad it is the only thing the reset + // has to close — atTabs is already true inside the Chat tab, so the loop below never runs. + const searchCancel = els(T.CHAT_THREAD_SEARCH_CANCEL) + if ((await searchCancel.length) > 0) { + const ctrl = searchCancel[0]! + await ctrl.click().catch(() => {}) + await settleAfter(ctrl) + } for (let i = 0; i < 3; i++) { const controls = await browser.$$(MODAL_DISMISS_PRED).getElements() if (controls.length === 0) break @@ -302,7 +312,6 @@ export async function escapeToTabs(): Promise { // so its Done/Close/Cancel must win. if ((await browser.$$(DISMISS_PRED).length) > 0) { const ctrl = browser.$$(DISMISS_PRED)[0]! - if (debug) console.log(` escapeToTabs[${i}]: dismissing "${await ctrl.getAttribute('label').catch(() => '?')}"`) await ctrl.click().catch(() => {}) await settleAfter(ctrl) @@ -310,10 +319,8 @@ export async function escapeToTabs(): Promise { // whose click no-ops (still present, still not at tabs) must fall through // to the back/pop path or we'd click it forever. if ((await atTabs()) || !(await ctrl.isExisting().catch(() => false))) continue - if (debug) console.log(` escapeToTabs[${i}]: dismiss no-oped, falling through to back/pop`) } else if (debug) { - console.log(` escapeToTabs[${i}]: no dismiss control, trying back/pop`) } if ((await els(T.COMMON_BACK_BUTTON).length) > 0) { From 22862aef9880c991533f7daf1a8560caffd9024e Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 12 Aug 2026 15:41:12 -0400 Subject: [PATCH 21/38] test(e2e): stop the search flow from timing out as the thread grows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stepping every hit costs more the more hits there are, and the conversation the suite runs against keeps gaining them — 70 by now. The walk ran past mocha's per-test budget, and the timeout surfaced as "the hit landed and then drifted off screen": a product failure that had not happened. The wrap-around is forced directly now (next, from the hit the search lands on, jumps straight to the far end of the thread, which is the longest jump the list is ever asked to make) and the walk after it is bounded, saying out loud how much of the ring it skipped. The same-screen query was a word out of the conversation's history, and it stopped matching — which the flow reported as the hit row never rendering rather than as a query with no results. It searches for a word out of the messages this suite itself sends now, which is both guaranteed present and recent. --- .../ios-appium/flows/chat-search-hit.test.ts | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts index e63472bcaa68..36579d159ea5 100644 --- a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts +++ b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts @@ -9,7 +9,10 @@ import * as T from '../../shared/test-ids' const QUERY = 'one' // A word whose hit sits among the messages already on screen: jumping a few rows is the case where // the list has nothing to load and the scroll lands against a content size that has not caught up. -const SAME_SCREEN_QUERY = 'working' +// Taken from the messages this suite itself sends ("e2e-test-"), so it is both present +// and recent. A word picked out of the conversation's history instead is a word that can stop +// matching — one did, and the flow then reported it as the hit row never rendering. +const SAME_SCREEN_QUERY = 'test' // Flings back through the thread until a page of older messages arrives. const MAX_FLINGS = 20 // Drags needed to move a hit clear of the viewport. One drag moves about a third of a screen, and @@ -163,9 +166,15 @@ const startSearch = async (query: string): Promise => { return hits } -const stepThroughHits = async (steps: number) => { +// Walking the whole ring is what this used to do, and its cost grows with the conversation: once +// the thread has enough hits the walk runs past the suite's per-test budget, and the timeout is +// reported as "the hit drifted off screen" rather than as this having run out of time. The wrap is +// forced directly now, and the walk after it is bounded. +const MAX_HIT_STEPS = 12 + +const stepThroughHits = async (steps: number, control: string = T.CHAT_THREAD_SEARCH_PREV) => { for (let step = 0; step < steps; step++) { - await el(T.CHAT_THREAD_SEARCH_PREV).click() + await el(control).click() // Give the jump, and the measurements that follow it, time to land. const landed = await browser @@ -256,10 +265,17 @@ describe('chat thread search', function () { requireSmokeUser() if (!(await openFirstConversation())) throw new Error('no conversations in the inbox') - // Two past the end, so the search wraps and lands on hits it has already visited from a - // different scroll position - the case that used to leave the hit off screen. const hits = await startSearch(QUERY) - await stepThroughHits(hits + 2) + // Next from the hit the search lands on wraps straight to the far end of the thread, which is + // the longest jump the list is ever asked to make - and the case that used to leave the hit off + // screen. Forced first rather than reached by walking the whole ring to it. + await stepThroughHits(1, T.CHAT_THREAD_SEARCH_NEXT) + const walk = Math.min(hits + 2, MAX_HIT_STEPS) + if (walk < hits + 2) { + // Said out loud: a bound nobody can see reads as "every hit was checked" when it was not. + console.log(`stepping ${walk} of ${hits + 2} hits — the rest costs more than the test budget`) + } + await stepThroughHits(walk) await closeThreadSearch() }) From 340a758edb28c7cca8ccbc13f4e85c234b4e0a4c Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 12 Aug 2026 15:48:59 -0400 Subject: [PATCH 22/38] test(e2e): point the search flows at a named conversation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both flows opened the first inbox row, and the inbox is ordered by recency — so which conversation they searched depended on what had most recently received a message, including the messages these suites send themselves. A run could search one conversation and the next run another, which changes the hit count and which words match at all. Today that surfaced as the wrap-around walk running past its budget and as a query returning nothing, neither of which had anything to do with the list. They open the smoke user's own chat by name now. It always exists, its name is the username the run was given, and it has the history these cases need. --- .../electron/flows/chat-search-hit.test.ts | 15 ++++++++-- .../ios-appium/flows/chat-search-hit.test.ts | 28 +++++++++++++------ 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/shared/tests/e2e/electron/flows/chat-search-hit.test.ts b/shared/tests/e2e/electron/flows/chat-search-hit.test.ts index c4f68194affd..46a6fe43d1e8 100644 --- a/shared/tests/e2e/electron/flows/chat-search-hit.test.ts +++ b/shared/tests/e2e/electron/flows/chat-search-hit.test.ts @@ -1,5 +1,5 @@ import {test, expect} from '@/tests/e2e/electron/helpers/fixtures' -import {openFirstConversation} from '@/tests/e2e/electron/helpers/navigate' +import {navigateToChat} from '@/tests/e2e/electron/helpers/navigate' import * as T from '@/tests/e2e/shared/test-ids' // Searching a thread has to land on the hit, and then leave the thread alone. Both were manual @@ -11,7 +11,18 @@ import * as T from '@/tests/e2e/shared/test-ids' // first half again. test('lands on every search hit, then stays where the reader scrolls it', async ({page}) => { test.setTimeout(120_000) - expect(await openFirstConversation(page)).toBe(true) + // Named, not "whichever row is first". The inbox is ordered by recency and the suites send + // messages of their own, so the first row is a different conversation from one run to the next — + // and with it the hit count and which words match at all. + const smokeUser = process.env['KB_SMOKE_USER'] + expect(smokeUser, 'KB_SMOKE_USER is not set').toBeTruthy() + await navigateToChat(page) + const row = page + .getByTestId(T.CHAT_INBOX_ROW) + .filter({hasText: smokeUser!}) + .first() + await row.click({timeout: 10_000}) + await page.waitForSelector(`[data-testid="${T.CHAT_MESSAGE_LIST}"]`, {timeout: 5_000}) // force: the conversation header sits in the window's WebkitAppRegion drag region, which makes // playwright's actionability check wait forever on a control that is perfectly clickable. diff --git a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts index 36579d159ea5..4bd01e683f19 100644 --- a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts +++ b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts @@ -1,7 +1,7 @@ import type {ChainablePromiseElement} from 'webdriverio' import {expect} from '@wdio/globals' import {requireSmokeUser} from '../helpers/app' -import {anyExist, el, els, tab, waitForTestID, enterText} from '../helpers/elements' +import {anyExist, byTextWithin, el, tab, waitForTestID, enterText} from '../helpers/elements' import {dismissKeyboard, escapeToTabs} from '../helpers/navigate' import * as T from '../../shared/test-ids' @@ -10,8 +10,7 @@ const QUERY = 'one' // A word whose hit sits among the messages already on screen: jumping a few rows is the case where // the list has nothing to load and the scroll lands against a content size that has not caught up. // Taken from the messages this suite itself sends ("e2e-test-"), so it is both present -// and recent. A word picked out of the conversation's history instead is a word that can stop -// matching — one did, and the flow then reported it as the hit row never rendering. +// and recent in whatever conversation the run is pointed at. const SAME_SCREEN_QUERY = 'test' // Flings back through the thread until a page of older messages arrives. const MAX_FLINGS = 20 @@ -242,14 +241,27 @@ const dragUntilHitLeaves = async () => { // Each test starts from the tab root: the suite returns there between tests, so a flow cannot // assume the conversation another one left open. The conversation needs enough history to page in // and enough matches for both queries, which is the smoke account's own chat with itself. -const openFirstConversation = async (): Promise => { +// Named, not "whichever row is first". The inbox is ordered by recency and this suite sends +// messages of its own, so the first row is a different conversation from one run to the next — and +// then so are the hit count and which words match at all. That drift reads as this flow failing. +// +// The smoke user's own chat is the stable choice: it always exists, its name is the username the +// run was given, and it has the history these cases need. +const openSearchConversation = async (): Promise => { + const smokeUser = requireSmokeUser() await escapeToTabs() await tab('Teams').click() await tab('Chat').click() await waitForTestID(T.CHAT_INBOX_LIST, 5000) if (!(await anyExist(T.CHAT_INBOX_ROW))) return false - await els(T.CHAT_INBOX_ROW)[0]!.click() + // Scoped to the inbox: the signed-in username is also on the header avatar above it, and tapping + // that opens the account switcher. + const row = byTextWithin(el(T.CHAT_INBOX_LIST), smokeUser) + if (!(await row.isExisting())) { + throw new Error(`no conversation named "${smokeUser}" in the inbox`) + } + await row.click() await waitForTestID(T.CHAT_MESSAGE_LIST, 5000) await dismissKeyboard() return true @@ -263,7 +275,7 @@ describe('chat thread search', function () { it('keeps every hit it lands on visible, including wrapping around', async () => { requireSmokeUser() - if (!(await openFirstConversation())) throw new Error('no conversations in the inbox') + if (!(await openSearchConversation())) throw new Error('no conversations in the inbox') const hits = await startSearch(QUERY) // Next from the hit the search lands on wraps straight to the far end of the thread, which is @@ -281,7 +293,7 @@ describe('chat thread search', function () { it('lands on a hit that is already on screen', async () => { requireSmokeUser() - if (!(await openFirstConversation())) throw new Error('no conversations in the inbox') + if (!(await openSearchConversation())) throw new Error('no conversations in the inbox') // No native mutation has been found that makes this case fail on its own - it is here because // it is the case people report on desktop, where it does fail. Treat a green here as coverage @@ -293,7 +305,7 @@ describe('chat thread search', function () { it('leaves the thread where the user drags it after a hit', async () => { requireSmokeUser() - if (!(await openFirstConversation())) throw new Error('no conversations in the inbox') + if (!(await openSearchConversation())) throw new Error('no conversations in the inbox') await startSearch(SAME_SCREEN_QUERY) // A moment after landing is when the list is still measuring, and where anything holding the From 545c8b526b845025600a2cebe6449a0e33dbb7b5 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 12 Aug 2026 17:55:17 -0400 Subject: [PATCH 23/38] fix(chat): drop the library change we could not show we needed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The patch carried a third fix reporting web input events — wheel, pointer, touch, key — as the user taking hold of the list, so that a scroll target still settling would let go. Measured against a build without it, a drag issued immediately after a jump was not corrected back in any of six rounds, on either platform. The native half of that release was never in it: onScrollBeginDrag already clears the settle. The two that remain were each shown to be load-bearing by removing them from a real build. Without the web extent borrow a search hit lands at y=1771 in a viewport of y=80..867; without the settle it lands at y=1524. Both deterministic. Roughly 700 lines of library source and 1300 of tests gone, and the patch is about 1300 lines smaller. --- shared/patches/@legendapp+list+3.3.5.patch | 2298 +++++--------------- 1 file changed, 506 insertions(+), 1792 deletions(-) diff --git a/shared/patches/@legendapp+list+3.3.5.patch b/shared/patches/@legendapp+list+3.3.5.patch index 385ca59b9d47..18131d79b669 100644 --- a/shared/patches/@legendapp+list+3.3.5.patch +++ b/shared/patches/@legendapp+list+3.3.5.patch @@ -1,5 +1,9 @@ +diff --git a/node_modules/@legendapp/list/.DS_Store b/node_modules/@legendapp/list/.DS_Store +deleted file mode 100644 +index b86e710..0000000 +Binary files a/node_modules/@legendapp/list/.DS_Store and /dev/null differ diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js -index b3c5a30..ff02901 100644 +index b3c5a30..6d323a3 100644 --- a/node_modules/@legendapp/list/react-native.js +++ b/node_modules/@legendapp/list/react-native.js @@ -457,611 +457,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -1223,7 +1227,7 @@ index b3c5a30..ff02901 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2276,6 +2105,325 @@ function scrollTo(ctx, params) { +@@ -2276,6 +2105,322 @@ function scrollTo(ctx, params) { } } @@ -1233,9 +1237,6 @@ index b3c5a30..ff02901 100644 +var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; -+function releaseScrollTargetForUserInteraction(state) { -+ clearScrollTargetSettle(state); -+} +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; + state.scheduledWork.cancel("scrollTargetSettle"); @@ -1549,7 +1550,7 @@ index b3c5a30..ff02901 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4320,6 +4468,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4320,6 +4465,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -1557,7 +1558,7 @@ index b3c5a30..ff02901 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4337,8 +4486,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4337,8 +4483,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -1578,30 +1579,16 @@ index b3c5a30..ff02901 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -6001,7 +6160,12 @@ var ListComponent = typedMemo(function ListComponent2({ - SnapOrScroll, - { - ...rest, -- ...Platform.OS === "web" ? ScrollComponent === ListComponentScrollView ? { onInternalScrollEnd, useWindowScroll } : {} : { onScrollBeginDrag: onInternalScrollBeginDrag }, -+ ...Platform.OS === "web" ? ScrollComponent === ListComponentScrollView ? ( -+ // onUserInteraction is the web counterpart of onScrollBeginDrag: the view -+ // reports that the user moved the list, and LegendList decides what that -+ // means for a scroll in flight. -+ { onInternalScrollEnd, onUserInteraction: onInternalScrollBeginDrag, useWindowScroll } -+ ) : {} : { onScrollBeginDrag: onInternalScrollBeginDrag }, - contentContainerStyle: [ - horizontal ? { height: "100%" } : {}, - contentContainerStyle, -@@ -7652,6 +7816,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7652,6 +7808,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); -+ releaseScrollTargetForUserInteraction(state); ++ clearScrollTargetSettle(state); (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs -index 40e87cd..dc63618 100644 +index 40e87cd..877e5d4 100644 --- a/node_modules/@legendapp/list/react-native.mjs +++ b/node_modules/@legendapp/list/react-native.mjs @@ -436,611 +436,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -2825,7 +2812,7 @@ index 40e87cd..dc63618 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2255,6 +2084,325 @@ function scrollTo(ctx, params) { +@@ -2255,6 +2084,322 @@ function scrollTo(ctx, params) { } } @@ -2835,9 +2822,6 @@ index 40e87cd..dc63618 100644 +var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; -+function releaseScrollTargetForUserInteraction(state) { -+ clearScrollTargetSettle(state); -+} +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; + state.scheduledWork.cancel("scrollTargetSettle"); @@ -3151,7 +3135,7 @@ index 40e87cd..dc63618 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4299,6 +4447,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4299,6 +4444,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -3159,7 +3143,7 @@ index 40e87cd..dc63618 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4316,8 +4465,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4316,8 +4462,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -3180,30 +3164,16 @@ index 40e87cd..dc63618 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5980,7 +6139,12 @@ var ListComponent = typedMemo(function ListComponent2({ - SnapOrScroll, - { - ...rest, -- ...Platform.OS === "web" ? ScrollComponent === ListComponentScrollView ? { onInternalScrollEnd, useWindowScroll } : {} : { onScrollBeginDrag: onInternalScrollBeginDrag }, -+ ...Platform.OS === "web" ? ScrollComponent === ListComponentScrollView ? ( -+ // onUserInteraction is the web counterpart of onScrollBeginDrag: the view -+ // reports that the user moved the list, and LegendList decides what that -+ // means for a scroll in flight. -+ { onInternalScrollEnd, onUserInteraction: onInternalScrollBeginDrag, useWindowScroll } -+ ) : {} : { onScrollBeginDrag: onInternalScrollBeginDrag }, - contentContainerStyle: [ - horizontal ? { height: "100%" } : {}, - contentContainerStyle, -@@ -7631,6 +7795,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7631,6 +7787,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); -+ releaseScrollTargetForUserInteraction(state); ++ clearScrollTargetSettle(state); (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js -index 914d2da..f97cffb 100644 +index 914d2da..c235070 100644 --- a/node_modules/@legendapp/list/react-native.web.js +++ b/node_modules/@legendapp/list/react-native.web.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -3812,16 +3782,18 @@ index 914d2da..f97cffb 100644 // src/core/getStartOffsetAdjustment.ts function getStartOffsetAdjustment(ctx) { const { state } = ctx; -@@ -1234,58 +635,273 @@ function getItemSizeAtIndex(ctx, index) { - if (index === void 0 || index < 0) { - return void 0; - } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+ const targetId = getId(ctx.state, index); -+ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+} -+ +@@ -1238,54 +639,269 @@ function getItemSizeAtIndex(ctx, index) { + return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); + } + +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +// src/core/calculateOffsetWithOffsetPosition.ts +function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { + var _a3; @@ -4025,7 +3997,12 @@ index 914d2da..f97cffb 100644 + const state = ctx == null ? void 0 : ctx.state; + if (!state) { + return; -+ } + } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } + const { + isStartReached, + props: { data, onStartReachedThreshold }, @@ -4040,7 +4017,22 @@ index 914d2da..f97cffb 100644 + if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { + state.isStartReached = false; + state.startReachedSnapshot = void 0; -+ } + } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } + set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); + set$(ctx, "isNearStart", scroll <= threshold); + const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; @@ -4067,50 +4059,19 @@ index 914d2da..f97cffb 100644 + state.startReachedSnapshot = snapshot; + } + ); -+ } + } +- return offset; } --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; -- } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } -- } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } -- } -- return offset; +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); - } - --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { ++} ++ +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { + var _a3, _b; @@ -4132,24 +4093,6 @@ index 914d2da..f97cffb 100644 } // src/core/finishScrollTo.ts -@@ -1334,7 +950,7 @@ var SCROLL_END_TARGET_EPSILON = 1; - function doScrollTo(ctx, params) { - var _a3, _b; - const state = ctx.state; -- const { animated, horizontal, offset } = params; -+ const { animated, horizontal, isCorrection, offset } = params; - state.scheduledWork.cancel("platformScrollCompletion"); - const scroller = state.refScroller.current; - const node = scroller == null ? void 0 : scroller.getScrollableNode(); -@@ -1346,7 +962,7 @@ function doScrollTo(ctx, params) { - const contentSize = isHorizontal ? getContentSize(ctx) : void 0; - const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; - const top = isHorizontal ? 0 : offset; -- scroller.scrollTo({ animated: isAnimated, x: left, y: top }); -+ scroller.scrollTo({ animated: isAnimated, isCorrection, x: left, y: top }); - if (isAnimated) { - const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; - listenForScrollEnd(ctx, { @@ -1413,16 +1029,191 @@ function listenForScrollEnd(ctx, params) { if (idleTimeout !== void 0) { clearTimeout(idleTimeout); @@ -4359,68 +4302,6 @@ index 914d2da..f97cffb 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1525,7 +1317,7 @@ var MVCP_POSITION_EPSILON = 0.1; - var MVCP_ANCHOR_LOCK_TTL_MS = 300; - var MVCP_ANCHOR_LOCK_QUIET_PASSES_TO_RELEASE = 2; - var NATIVE_END_CLAMP_EPSILON = 1; --function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { -+function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now2) { - if (!enableMVCPAnchorLock) { - state.mvcpAnchorLock = void 0; - return void 0; -@@ -1534,7 +1326,7 @@ function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { - if (!lock) { - return void 0; - } -- const isExpired = now > lock.expiresAt; -+ const isExpired = now2 > lock.expiresAt; - const isMissing = state.indexByKey.get(lock.id) === void 0; - if (isExpired || isMissing || !mvcpData) { - state.mvcpAnchorLock = void 0; -@@ -1544,7 +1336,7 @@ function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { - } - function updateAnchorLock(state, params) { - { -- const { anchorId, anchorPosition, dataChanged, now, positionDiff } = params; -+ const { anchorId, anchorPosition, dataChanged, now: now2, positionDiff } = params; - const enableMVCPAnchorLock = !!dataChanged || !!state.mvcpAnchorLock; - const mvcpData = state.props.maintainVisibleContentPosition.data; - if (!enableMVCPAnchorLock || !mvcpData || state.scrollingTo || !anchorId || anchorPosition === void 0) { -@@ -1557,7 +1349,7 @@ function updateAnchorLock(state, params) { - return; - } - state.mvcpAnchorLock = { -- expiresAt: now + MVCP_ANCHOR_LOCK_TTL_MS, -+ expiresAt: now2 + MVCP_ANCHOR_LOCK_TTL_MS, - id: anchorId, - position: anchorPosition, - quietPasses -@@ -1662,14 +1454,14 @@ function prepareMVCP(ctx, dataChanged) { - const { - maintainVisibleContentPosition: { data: mvcpData, size: mvcpScroll, shouldRestorePosition } - } = props; -- const now = Date.now(); -+ const now2 = Date.now(); - const enableMVCPAnchorLock = (!!dataChanged || !!state.mvcpAnchorLock); - const scrollingTo = state.scrollingTo; - if (dataChanged && state.pendingScrollToEnd && scrollingTo === void 0) { - state.mvcpAnchorLock = void 0; - return void 0; - } -- const anchorLock = resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) ; -+ const anchorLock = resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now2) ; - let prevPosition; - let targetId; - const idsInViewWithPositions = []; -@@ -1786,7 +1578,7 @@ function prepareMVCP(ctx, dataChanged) { - anchorId: anchorIdForLock, - anchorPosition: anchorPositionForLock, - dataChanged, -- now, -+ now: now2, - positionDiff - }); - if (shouldQueueNativeMVCPAdjust()) { @@ -1809,6 +1601,27 @@ function prepareMVCP(ctx, dataChanged) { } } @@ -4449,12 +4330,24 @@ index 914d2da..f97cffb 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2068,57 +1881,392 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { - ctx.state.scrollTargetPinnedRange = void 0; - } - } --function scrollTo(ctx, params) { -- var _a3, _b; +@@ -2055,70 +1868,402 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { + if (nextTop === void 0 || nextTop > viewportEnd) { + break; + } +- end++; ++ end++; ++ } ++ return { end, start }; ++} ++function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { ++ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); ++ if (range) { ++ ctx.state.scrollTargetPinnedRange = range; ++ ctx.state.scrollForNextCalculateItemsInView = void 0; ++ } else { ++ ctx.state.scrollTargetPinnedRange = void 0; ++ } ++} +function scrollTo(ctx, params) { + var _a3, _b, _c; + const state = ctx.state; @@ -4512,7 +4405,7 @@ index 914d2da..f97cffb 100644 + } + } + if (forceScroll || !isInitialScroll || Platform.OS === "android") { -+ doScrollTo(ctx, { animated, horizontal, isCorrection: noScrollingTo, offset }); ++ doScrollTo(ctx, { animated, horizontal, offset }); + } else { + state.scroll = offset; + } @@ -4524,9 +4417,6 @@ index 914d2da..f97cffb 100644 +var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; -+function releaseScrollTargetForUserInteraction(state) { -+ clearScrollTargetSettle(state); -+} +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; + state.scheduledWork.cancel("scrollTargetSettle"); @@ -4543,11 +4433,11 @@ index 914d2da..f97cffb 100644 + return; + } + clearScrollTargetSettle(state); -+ const now2 = Date.now(); ++ const now = Date.now(); + state.scrollTargetSettle = { + corrections: 0, -+ deadline: now2 + SETTLE_MAX_MS, -+ expiresAt: now2 + SETTLE_TTL_MS, ++ deadline: now + SETTLE_MAX_MS, ++ expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, + ownsScrollingTo: state.scrollingTo !== void 0, @@ -4630,8 +4520,8 @@ index 914d2da..f97cffb 100644 + clearScrollTargetSettle(state); + return false; + } -+ const now2 = Date.now(); -+ if (now2 > settle.expiresAt || now2 > settle.deadline) { ++ const now = Date.now(); ++ if (now > settle.expiresAt || now > settle.deadline) { + clearScrollTargetSettle(state); + return false; + } @@ -4649,7 +4539,7 @@ index 914d2da..f97cffb 100644 + return false; + } + settle.quietPasses = 0; -+ settle.expiresAt = now2 + SETTLE_TTL_MS; ++ settle.expiresAt = now + SETTLE_TTL_MS; + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} @@ -4748,8 +4638,17 @@ index 914d2da..f97cffb 100644 + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); + } + } -+ } -+} + } +- return { end, start }; + } +-function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); +- if (range) { +- ctx.state.scrollTargetPinnedRange = range; +- ctx.state.scrollForNextCalculateItemsInView = void 0; +- } else { +- ctx.state.scrollTargetPinnedRange = void 0; +- } + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -4757,7 +4656,9 @@ index 914d2da..f97cffb 100644 + state.scroll = offset; + state.scrollPending = offset; + state.scrollPrev = offset; -+} + } +-function scrollTo(ctx, params) { +- var _a3, _b; +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -4768,24 +4669,6 @@ index 914d2da..f97cffb 100644 + setInitialScrollSession(state); +} +function supersedeInitialScroll(ctx) { -+ var _a3, _b, _c; -+ const state = ctx.state; -+ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; -+ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { -+ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { -+ cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -+ } -+ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { -+ cancelScrollCompletionChecks(state); -+ state.scrollingTo = void 0; -+ state.scrollTargetPinnedRange = void 0; -+ } -+ initialScrollCompletion.resetFlags(state); -+ setInitialScrollSession(state, { bootstrap: null }); -+ finishInitialScroll(ctx); -+ } -+} -+function finishInitialScroll(ctx, options) { + var _a3, _b, _c; const state = ctx.state; - const { noScrollingTo, forceScroll, ...scrollTarget } = params; @@ -4808,7 +4691,11 @@ index 914d2da..f97cffb 100644 - if (!noScrollingTo) { - if (isInitialScroll) { - initialScrollCompletion.resetFlags(state); -- } ++ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; ++ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { ++ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { ++ cancelAnimationFrame(bootstrapInitialScroll.frameHandle); + } - const averageSizeSnapshot = getAverageSizeSnapshot(state); - state.scrollingTo = { - ...scrollTarget, @@ -4818,13 +4705,14 @@ index 914d2da..f97cffb 100644 - }; - if (!isInitialScroll) { - pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); -+ if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { -+ syncInitialScrollOffset(state, options.resolvedOffset); -+ } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { -+ const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); -+ if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { -+ syncInitialScrollOffset(state, observedOffset); ++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { ++ cancelScrollCompletionChecks(state); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; } ++ initialScrollCompletion.resetFlags(state); ++ setInitialScrollSession(state, { bootstrap: null }); ++ finishInitialScroll(ctx); } - state.scrollPending = targetOffset; - syncInitialScrollNativeWatchdog(state, { isInitialScroll, requestedOffset: offset, targetOffset }); @@ -4832,6 +4720,18 @@ index 914d2da..f97cffb 100644 - if (animated) { - if (state.scrollTargetPinnedRange) { - (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++} ++function finishInitialScroll(ctx, options) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { ++ syncInitialScrollOffset(state, options.resolvedOffset); ++ } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { ++ const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); ++ if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { ++ syncInitialScrollOffset(state, observedOffset); ++ } ++ } + const complete = () => { + var _a4, _b2, _c2, _d, _e; + const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; @@ -4886,7 +4786,7 @@ index 914d2da..f97cffb 100644 } // src/core/scrollToIndex.ts -@@ -4350,6 +4498,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4350,6 +4495,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -4894,7 +4794,7 @@ index 914d2da..f97cffb 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4367,8 +4516,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4367,8 +4513,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -4915,7 +4815,7 @@ index 914d2da..f97cffb 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5927,6 +6086,21 @@ function getDocumentScrollerNode() { +@@ -5927,6 +6083,21 @@ function getDocumentScrollerNode() { } return document.scrollingElement || document.documentElement || document.body; } @@ -4937,7 +4837,7 @@ index 914d2da..f97cffb 100644 function getWindowScrollPosition() { var _a3, _b, _c, _d; if (typeof window === "undefined") { -@@ -6003,9 +6177,219 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -6003,9 +6174,155 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll }; } @@ -5085,87 +4985,15 @@ index 914d2da..f97cffb 100644 + // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; -+var SCROLLER_MARKER_ATTRIBUTE = "data-legend-list-scroller"; var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; +var SMOOTH_SCROLL_MAX_MS = 2e3; +var BORROW_WATCH_FRAME_INTERVAL = 3; +var BORROW_MEASURE_ATTEMPTS = 2; -+var USER_DRAG_SLOP = 8; -+var WHEEL_MOMENTUM_GRACE_MS = 150; -+var SCROLL_KEYS = /* @__PURE__ */ new Set([ -+ " ", -+ "ArrowDown", -+ "ArrowLeft", -+ "ArrowRight", -+ "ArrowUp", -+ "End", -+ "Home", -+ "PageDown", -+ "PageUp" -+]); -+var NON_SCROLLING_KEY_TARGETS = "input,textarea,select,button,[contenteditable],[role=textbox],[role=button]"; -+function isTextEntryTarget(target) { -+ var _a3; -+ const element = target; -+ if (!element) { -+ return false; -+ } -+ if (element.isContentEditable) { -+ return true; -+ } -+ return !!((_a3 = element.closest) == null ? void 0 : _a3.call(element, NON_SCROLLING_KEY_TARGETS)); -+} -+function pointerPosition(event, id) { -+ const touches = event.touches; -+ if (touches == null ? void 0 : touches.length) { -+ const touch = id === void 0 ? touches[0] : [...touches].find((t) => t.identifier === id); -+ return touch ? { id: touch.identifier, x: touch.clientX, y: touch.clientY } : void 0; -+ } -+ const pointer = event; -+ if (typeof pointer.clientX !== "number") { -+ return void 0; -+ } -+ if (id !== void 0 && pointer.pointerId !== void 0 && pointer.pointerId !== id) { -+ return void 0; -+ } -+ return { id: pointer.pointerId, x: pointer.clientX, y: pointer.clientY }; -+} -+var POINTER_EVENT_HANDLERS = (onDown, onMove, onUp) => [ -+ ["pointerdown", onDown], -+ ["pointermove", onMove], -+ ["pointerup", onUp], -+ ["pointercancel", onUp], -+ ["touchstart", onDown], -+ ["touchmove", onMove], -+ ["touchend", onUp], -+ ["touchcancel", onUp] -+]; -+function now() { -+ return typeof performance !== "undefined" ? performance.now() : 0; -+} -+function isHover(event) { -+ const pointer = event; -+ return pointer.pointerType === "mouse" && pointer.buttons === 0; -+} -+function isScrollKey(event) { -+ if (event.altKey || event.ctrlKey || event.metaKey) { -+ return false; -+ } -+ return SCROLL_KEYS.has(event.key) && !isTextEntryTarget(event.target); -+} var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6055,6 +6439,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - contentOffset, - maintainVisibleContentPosition, - onScroll: onScroll2, -+ onUserInteraction, - onInternalScrollEnd, - onMomentumScrollEnd: _onMomentumScrollEnd, - showsHorizontalScrollIndicator = true, -@@ -6074,6 +6459,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6074,6 +6391,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -5177,7 +5005,7 @@ index 914d2da..f97cffb 100644 const getMaxScrollOffset = React3.useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6094,8 +6484,170 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,6 +6416,99 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -5273,83 +5101,11 @@ index 914d2da..f97cffb 100644 + } + }, + [] -+ ); -+ const interactionArmedAtRef = React3.useRef(Number.NEGATIVE_INFINITY); -+ const dragOriginRef = React3.useRef(void 0); -+ const ownsEvent = React3.useCallback((event) => { -+ const scroller = scrollRef.current; -+ const target = event.target; -+ if (!scroller || !(target == null ? void 0 : target.closest)) { -+ return true; -+ } -+ return target.closest(`[${SCROLLER_MARKER_ATTRIBUTE}]`) === scroller; -+ }, []); -+ const reportUserInteraction = React3.useCallback(() => { -+ dragOriginRef.current = void 0; -+ onUserInteraction == null ? void 0 : onUserInteraction(); -+ }, [onUserInteraction]); -+ const onWheel = React3.useCallback( -+ (event) => { -+ if (!isWindowScroll && !ownsEvent(event)) { -+ return; -+ } -+ if (now() - interactionArmedAtRef.current < WHEEL_MOMENTUM_GRACE_MS) { -+ return; -+ } -+ reportUserInteraction(); -+ }, -+ [isWindowScroll, ownsEvent, reportUserInteraction] -+ ); -+ const onPointerDown = React3.useCallback( -+ (event) => { -+ if (dragOriginRef.current) { -+ return; -+ } -+ dragOriginRef.current = ownsEvent(event) ? pointerPosition(event, void 0) : void 0; -+ }, -+ [ownsEvent] -+ ); -+ const onPointerUp = React3.useCallback(() => { -+ dragOriginRef.current = void 0; -+ }, []); -+ const onPointerMove = React3.useCallback( -+ (event) => { -+ const origin = dragOriginRef.current; -+ if (!origin) { -+ return; -+ } -+ if (isHover(event)) { -+ dragOriginRef.current = void 0; -+ return; -+ } -+ const point = pointerPosition(event, origin.id); -+ if (!point) { -+ return; -+ } -+ if (Math.abs(point.x - origin.x) > USER_DRAG_SLOP || Math.abs(point.y - origin.y) > USER_DRAG_SLOP) { -+ reportUserInteraction(); -+ } -+ }, -+ [reportUserInteraction] -+ ); -+ const onKeyDown = React3.useCallback( -+ (event) => { -+ if (isScrollKey(event)) { -+ reportUserInteraction(); -+ } -+ }, -+ [reportUserInteraction] + ); const scrollToLocalOffset = React3.useCallback( -- (offset, animated) => { -+ (offset, animated, isCorrection) => { -+ if (!isCorrection) { -+ interactionArmedAtRef.current = now(); -+ } + (offset, animated) => { const scrollElement = scrollRef.current; - const target = getScrollTarget(); - if (!target || typeof target.scrollTo !== "function") { -@@ -6116,14 +6668,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6116,14 +6531,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); options.left = left; options.top = top; @@ -5376,14 +5132,7 @@ index 914d2da..f97cffb 100644 ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6144,13 +6703,12 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - target.scrollBy({ behavior: "auto", left: x, top: y }); - }, - scrollTo: (options) => { -- const { x = 0, y = 0, animated = true } = options; -- scrollToLocalOffset(horizontal ? x : y, animated); -+ const { x = 0, y = 0, animated = true, isCorrection } = options; -+ scrollToLocalOffset(horizontal ? x : y, animated, isCorrection); +@@ -6149,8 +6571,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -5393,7 +5142,7 @@ index 914d2da..f97cffb 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6163,7 +6721,12 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6163,7 +6584,12 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView if (!onScroll2 || !scrollRef.current) { return; } @@ -5407,7 +5156,7 @@ index 914d2da..f97cffb 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6183,7 +6746,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6183,7 +6609,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } }; onScroll2(scrollEvent); @@ -5416,63 +5165,7 @@ index 914d2da..f97cffb 100644 const scrollEventCoalescer = useRafCoalescer(emitScroll); const scrollEndFallbackRef = React3.useRef(void 0); const emitScrollEnd = React3.useCallback(() => { -@@ -6218,14 +6781,29 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] - ); - React3.useLayoutEffect(() => { -+ var _a4; - const target = getScrollTarget(); - if (!target) return; - target.addEventListener("scroll", handleScroll, { passive: true }); -+ const listenerOptions = { capture: true, passive: true }; -+ const removeOptions = { capture: true }; -+ const interactionTarget = (_a4 = scrollRef.current) != null ? _a4 : void 0; -+ const keyTarget = isWindowScroll ? target : interactionTarget; -+ target.addEventListener("wheel", onWheel, listenerOptions); -+ keyTarget == null ? void 0 : keyTarget.addEventListener("keydown", onKeyDown, listenerOptions); -+ for (const [type, handler] of POINTER_EVENT_HANDLERS(onPointerDown, onPointerMove, onPointerUp)) { -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener(type, handler, listenerOptions); -+ } - if ("onscrollend" in target) { - target.addEventListener("scrollend", emitScrollEnd); - } - return () => { - target.removeEventListener("scroll", handleScroll); -+ target.removeEventListener("wheel", onWheel, removeOptions); -+ keyTarget == null ? void 0 : keyTarget.removeEventListener("keydown", onKeyDown, removeOptions); -+ for (const [type, handler] of POINTER_EVENT_HANDLERS(onPointerDown, onPointerMove, onPointerUp)) { -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener(type, handler, removeOptions); -+ } - if ("onscrollend" in target) { - target.removeEventListener("scrollend", emitScrollEnd); - } -@@ -6236,7 +6814,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - } - scrollEventCoalescer.cancel(); - }; -- }, [emitScrollEnd, getScrollTarget, handleScroll, scrollEventCoalescer]); -+ }, [ -+ emitScrollEnd, -+ getScrollTarget, -+ handleScroll, -+ onKeyDown, -+ onPointerDown, -+ onPointerMove, -+ onWheel, -+ scrollEventCoalescer -+ ]); - React3.useEffect(() => { - const doScroll = () => { - if (contentOffset) { -@@ -6341,6 +6928,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - { - className: scrollViewClassName, - ref: scrollRef, -+ ...{ [SCROLLER_MARKER_ATTRIBUTE]: "" }, - ...webProps, - style: scrollViewStyle - }, -@@ -6379,21 +6967,6 @@ function useValueListener$(key, callback) { +@@ -6379,21 +6805,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -5494,7 +5187,7 @@ index 914d2da..f97cffb 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6411,8 +6984,6 @@ function ScrollAdjust() { +@@ -6411,8 +6822,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3__namespace.useRef(0); const lastScrollAdjustUserOffsetRef = React3__namespace.useRef(0); @@ -5503,7 +5196,7 @@ index 914d2da..f97cffb 100644 const contentNodeRef = React3__namespace.useRef(null); const callback = React3__namespace.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6423,7 +6994,7 @@ function ScrollAdjust() { +@@ -6423,7 +6832,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -5512,7 +5205,7 @@ index 914d2da..f97cffb 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6433,34 +7004,15 @@ function ScrollAdjust() { +@@ -6433,34 +6842,15 @@ function ScrollAdjust() { const scrollBy = () => scrollAdjustBy(el, axis.x * scrollDelta, axis.y * scrollDelta); contentNodeRef.current = contentNode; if (shouldScroll && contentNode) { @@ -5550,53 +5243,16 @@ index 914d2da..f97cffb 100644 } else { scrollBy(); } -@@ -6661,7 +7213,12 @@ var ListComponent = typedMemo(function ListComponent2({ - SnapOrScroll, - { - ...rest, -- ...ScrollComponent === ListComponentScrollView ? { onInternalScrollEnd, useWindowScroll } : {} , -+ ...ScrollComponent === ListComponentScrollView ? ( -+ // onUserInteraction is the web counterpart of onScrollBeginDrag: the view -+ // reports that the user moved the list, and LegendList decides what that -+ // means for a scroll in flight. -+ { onInternalScrollEnd, onUserInteraction: onInternalScrollBeginDrag, useWindowScroll } -+ ) : {} , - contentContainerStyle: [ - horizontal ? { height: "100%" } : {}, - contentContainerStyle, -@@ -7645,10 +8202,10 @@ function useThrottleDebounce(mode) { - const execute = React3.useCallback( - (callback, delay, ...args) => { - { -- const now = Date.now(); -+ const now2 = Date.now(); - lastArgsRef.current = args; -- if (now - lastCallTimeRef.current >= delay) { -- lastCallTimeRef.current = now; -+ if (now2 - lastCallTimeRef.current >= delay) { -+ lastCallTimeRef.current = now2; - callback(...args); - clearTimeoutRef(); - } else { -@@ -7662,7 +8219,7 @@ function useThrottleDebounce(mode) { - lastArgsRef.current = null; - } - }, -- delay - (now - lastCallTimeRef.current) -+ delay - (now2 - lastCallTimeRef.current) - ); - } - } -@@ -8288,6 +8845,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8288,6 +8678,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); -+ releaseScrollTargetForUserInteraction(state); ++ clearScrollTargetSettle(state); (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs -index 95465f2..6158381 100644 +index 95465f2..e8a3fb5 100644 --- a/node_modules/@legendapp/list/react-native.web.mjs +++ b/node_modules/@legendapp/list/react-native.web.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -6205,16 +5861,18 @@ index 95465f2..6158381 100644 // src/core/getStartOffsetAdjustment.ts function getStartOffsetAdjustment(ctx) { const { state } = ctx; -@@ -1213,58 +614,273 @@ function getItemSizeAtIndex(ctx, index) { - if (index === void 0 || index < 0) { - return void 0; - } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+ const targetId = getId(ctx.state, index); -+ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+} -+ +@@ -1217,54 +618,269 @@ function getItemSizeAtIndex(ctx, index) { + return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); + } + +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +// src/core/calculateOffsetWithOffsetPosition.ts +function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { + var _a3; @@ -6418,7 +6076,12 @@ index 95465f2..6158381 100644 + const state = ctx == null ? void 0 : ctx.state; + if (!state) { + return; -+ } + } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } + const { + isStartReached, + props: { data, onStartReachedThreshold }, @@ -6433,10 +6096,25 @@ index 95465f2..6158381 100644 + if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { + state.isStartReached = false; + state.startReachedSnapshot = void 0; -+ } -+ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); -+ set$(ctx, "isNearStart", scroll <= threshold); -+ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; + } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } ++ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); ++ set$(ctx, "isNearStart", scroll <= threshold); ++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; + if (!shouldSkipThresholdChecks) { + state.isStartReached = checkThreshold( + scroll, @@ -6460,50 +6138,19 @@ index 95465f2..6158381 100644 + state.startReachedSnapshot = snapshot; + } + ); -+ } + } +- return offset; } --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; -- } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } -- } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } -- } -- return offset; +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); - } - --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { ++} ++ +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { + var _a3, _b; @@ -6525,24 +6172,6 @@ index 95465f2..6158381 100644 } // src/core/finishScrollTo.ts -@@ -1313,7 +929,7 @@ var SCROLL_END_TARGET_EPSILON = 1; - function doScrollTo(ctx, params) { - var _a3, _b; - const state = ctx.state; -- const { animated, horizontal, offset } = params; -+ const { animated, horizontal, isCorrection, offset } = params; - state.scheduledWork.cancel("platformScrollCompletion"); - const scroller = state.refScroller.current; - const node = scroller == null ? void 0 : scroller.getScrollableNode(); -@@ -1325,7 +941,7 @@ function doScrollTo(ctx, params) { - const contentSize = isHorizontal ? getContentSize(ctx) : void 0; - const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; - const top = isHorizontal ? 0 : offset; -- scroller.scrollTo({ animated: isAnimated, x: left, y: top }); -+ scroller.scrollTo({ animated: isAnimated, isCorrection, x: left, y: top }); - if (isAnimated) { - const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; - listenForScrollEnd(ctx, { @@ -1392,16 +1008,191 @@ function listenForScrollEnd(ctx, params) { if (idleTimeout !== void 0) { clearTimeout(idleTimeout); @@ -6752,68 +6381,6 @@ index 95465f2..6158381 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1504,7 +1296,7 @@ var MVCP_POSITION_EPSILON = 0.1; - var MVCP_ANCHOR_LOCK_TTL_MS = 300; - var MVCP_ANCHOR_LOCK_QUIET_PASSES_TO_RELEASE = 2; - var NATIVE_END_CLAMP_EPSILON = 1; --function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { -+function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now2) { - if (!enableMVCPAnchorLock) { - state.mvcpAnchorLock = void 0; - return void 0; -@@ -1513,7 +1305,7 @@ function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { - if (!lock) { - return void 0; - } -- const isExpired = now > lock.expiresAt; -+ const isExpired = now2 > lock.expiresAt; - const isMissing = state.indexByKey.get(lock.id) === void 0; - if (isExpired || isMissing || !mvcpData) { - state.mvcpAnchorLock = void 0; -@@ -1523,7 +1315,7 @@ function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { - } - function updateAnchorLock(state, params) { - { -- const { anchorId, anchorPosition, dataChanged, now, positionDiff } = params; -+ const { anchorId, anchorPosition, dataChanged, now: now2, positionDiff } = params; - const enableMVCPAnchorLock = !!dataChanged || !!state.mvcpAnchorLock; - const mvcpData = state.props.maintainVisibleContentPosition.data; - if (!enableMVCPAnchorLock || !mvcpData || state.scrollingTo || !anchorId || anchorPosition === void 0) { -@@ -1536,7 +1328,7 @@ function updateAnchorLock(state, params) { - return; - } - state.mvcpAnchorLock = { -- expiresAt: now + MVCP_ANCHOR_LOCK_TTL_MS, -+ expiresAt: now2 + MVCP_ANCHOR_LOCK_TTL_MS, - id: anchorId, - position: anchorPosition, - quietPasses -@@ -1641,14 +1433,14 @@ function prepareMVCP(ctx, dataChanged) { - const { - maintainVisibleContentPosition: { data: mvcpData, size: mvcpScroll, shouldRestorePosition } - } = props; -- const now = Date.now(); -+ const now2 = Date.now(); - const enableMVCPAnchorLock = (!!dataChanged || !!state.mvcpAnchorLock); - const scrollingTo = state.scrollingTo; - if (dataChanged && state.pendingScrollToEnd && scrollingTo === void 0) { - state.mvcpAnchorLock = void 0; - return void 0; - } -- const anchorLock = resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) ; -+ const anchorLock = resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now2) ; - let prevPosition; - let targetId; - const idsInViewWithPositions = []; -@@ -1765,7 +1557,7 @@ function prepareMVCP(ctx, dataChanged) { - anchorId: anchorIdForLock, - anchorPosition: anchorPositionForLock, - dataChanged, -- now, -+ now: now2, - positionDiff - }); - if (shouldQueueNativeMVCPAdjust()) { @@ -1788,6 +1580,27 @@ function prepareMVCP(ctx, dataChanged) { } } @@ -6842,12 +6409,24 @@ index 95465f2..6158381 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2047,57 +1860,392 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { - ctx.state.scrollTargetPinnedRange = void 0; - } - } --function scrollTo(ctx, params) { -- var _a3, _b; +@@ -2034,70 +1847,402 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { + if (nextTop === void 0 || nextTop > viewportEnd) { + break; + } +- end++; ++ end++; ++ } ++ return { end, start }; ++} ++function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { ++ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); ++ if (range) { ++ ctx.state.scrollTargetPinnedRange = range; ++ ctx.state.scrollForNextCalculateItemsInView = void 0; ++ } else { ++ ctx.state.scrollTargetPinnedRange = void 0; ++ } ++} +function scrollTo(ctx, params) { + var _a3, _b, _c; + const state = ctx.state; @@ -6905,7 +6484,7 @@ index 95465f2..6158381 100644 + } + } + if (forceScroll || !isInitialScroll || Platform.OS === "android") { -+ doScrollTo(ctx, { animated, horizontal, isCorrection: noScrollingTo, offset }); ++ doScrollTo(ctx, { animated, horizontal, offset }); + } else { + state.scroll = offset; + } @@ -6917,9 +6496,6 @@ index 95465f2..6158381 100644 +var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; -+function releaseScrollTargetForUserInteraction(state) { -+ clearScrollTargetSettle(state); -+} +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; + state.scheduledWork.cancel("scrollTargetSettle"); @@ -6936,11 +6512,11 @@ index 95465f2..6158381 100644 + return; + } + clearScrollTargetSettle(state); -+ const now2 = Date.now(); ++ const now = Date.now(); + state.scrollTargetSettle = { + corrections: 0, -+ deadline: now2 + SETTLE_MAX_MS, -+ expiresAt: now2 + SETTLE_TTL_MS, ++ deadline: now + SETTLE_MAX_MS, ++ expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, + ownsScrollingTo: state.scrollingTo !== void 0, @@ -7023,8 +6599,8 @@ index 95465f2..6158381 100644 + clearScrollTargetSettle(state); + return false; + } -+ const now2 = Date.now(); -+ if (now2 > settle.expiresAt || now2 > settle.deadline) { ++ const now = Date.now(); ++ if (now > settle.expiresAt || now > settle.deadline) { + clearScrollTargetSettle(state); + return false; + } @@ -7042,7 +6618,7 @@ index 95465f2..6158381 100644 + return false; + } + settle.quietPasses = 0; -+ settle.expiresAt = now2 + SETTLE_TTL_MS; ++ settle.expiresAt = now + SETTLE_TTL_MS; + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} @@ -7141,8 +6717,17 @@ index 95465f2..6158381 100644 + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); + } + } -+ } -+} + } +- return { end, start }; + } +-function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); +- if (range) { +- ctx.state.scrollTargetPinnedRange = range; +- ctx.state.scrollForNextCalculateItemsInView = void 0; +- } else { +- ctx.state.scrollTargetPinnedRange = void 0; +- } + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -7150,7 +6735,9 @@ index 95465f2..6158381 100644 + state.scroll = offset; + state.scrollPending = offset; + state.scrollPrev = offset; -+} + } +-function scrollTo(ctx, params) { +- var _a3, _b; +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -7161,24 +6748,6 @@ index 95465f2..6158381 100644 + setInitialScrollSession(state); +} +function supersedeInitialScroll(ctx) { -+ var _a3, _b, _c; -+ const state = ctx.state; -+ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; -+ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { -+ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { -+ cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -+ } -+ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { -+ cancelScrollCompletionChecks(state); -+ state.scrollingTo = void 0; -+ state.scrollTargetPinnedRange = void 0; -+ } -+ initialScrollCompletion.resetFlags(state); -+ setInitialScrollSession(state, { bootstrap: null }); -+ finishInitialScroll(ctx); -+ } -+} -+function finishInitialScroll(ctx, options) { + var _a3, _b, _c; const state = ctx.state; - const { noScrollingTo, forceScroll, ...scrollTarget } = params; @@ -7201,7 +6770,11 @@ index 95465f2..6158381 100644 - if (!noScrollingTo) { - if (isInitialScroll) { - initialScrollCompletion.resetFlags(state); -- } ++ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; ++ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { ++ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { ++ cancelAnimationFrame(bootstrapInitialScroll.frameHandle); + } - const averageSizeSnapshot = getAverageSizeSnapshot(state); - state.scrollingTo = { - ...scrollTarget, @@ -7211,13 +6784,14 @@ index 95465f2..6158381 100644 - }; - if (!isInitialScroll) { - pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); -+ if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { -+ syncInitialScrollOffset(state, options.resolvedOffset); -+ } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { -+ const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); -+ if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { -+ syncInitialScrollOffset(state, observedOffset); ++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { ++ cancelScrollCompletionChecks(state); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; } ++ initialScrollCompletion.resetFlags(state); ++ setInitialScrollSession(state, { bootstrap: null }); ++ finishInitialScroll(ctx); } - state.scrollPending = targetOffset; - syncInitialScrollNativeWatchdog(state, { isInitialScroll, requestedOffset: offset, targetOffset }); @@ -7225,6 +6799,18 @@ index 95465f2..6158381 100644 - if (animated) { - if (state.scrollTargetPinnedRange) { - (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++} ++function finishInitialScroll(ctx, options) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { ++ syncInitialScrollOffset(state, options.resolvedOffset); ++ } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { ++ const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); ++ if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { ++ syncInitialScrollOffset(state, observedOffset); ++ } ++ } + const complete = () => { + var _a4, _b2, _c2, _d, _e; + const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; @@ -7279,7 +6865,7 @@ index 95465f2..6158381 100644 } // src/core/scrollToIndex.ts -@@ -4329,6 +4477,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4329,6 +4474,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -7287,7 +6873,7 @@ index 95465f2..6158381 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4346,8 +4495,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4346,8 +4492,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -7308,7 +6894,7 @@ index 95465f2..6158381 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5906,6 +6065,21 @@ function getDocumentScrollerNode() { +@@ -5906,6 +6062,21 @@ function getDocumentScrollerNode() { } return document.scrollingElement || document.documentElement || document.body; } @@ -7330,7 +6916,7 @@ index 95465f2..6158381 100644 function getWindowScrollPosition() { var _a3, _b, _c, _d; if (typeof window === "undefined") { -@@ -5982,9 +6156,219 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -5982,9 +6153,155 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll }; } @@ -7478,87 +7064,15 @@ index 95465f2..6158381 100644 + // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; -+var SCROLLER_MARKER_ATTRIBUTE = "data-legend-list-scroller"; var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; +var SMOOTH_SCROLL_MAX_MS = 2e3; +var BORROW_WATCH_FRAME_INTERVAL = 3; +var BORROW_MEASURE_ATTEMPTS = 2; -+var USER_DRAG_SLOP = 8; -+var WHEEL_MOMENTUM_GRACE_MS = 150; -+var SCROLL_KEYS = /* @__PURE__ */ new Set([ -+ " ", -+ "ArrowDown", -+ "ArrowLeft", -+ "ArrowRight", -+ "ArrowUp", -+ "End", -+ "Home", -+ "PageDown", -+ "PageUp" -+]); -+var NON_SCROLLING_KEY_TARGETS = "input,textarea,select,button,[contenteditable],[role=textbox],[role=button]"; -+function isTextEntryTarget(target) { -+ var _a3; -+ const element = target; -+ if (!element) { -+ return false; -+ } -+ if (element.isContentEditable) { -+ return true; -+ } -+ return !!((_a3 = element.closest) == null ? void 0 : _a3.call(element, NON_SCROLLING_KEY_TARGETS)); -+} -+function pointerPosition(event, id) { -+ const touches = event.touches; -+ if (touches == null ? void 0 : touches.length) { -+ const touch = id === void 0 ? touches[0] : [...touches].find((t) => t.identifier === id); -+ return touch ? { id: touch.identifier, x: touch.clientX, y: touch.clientY } : void 0; -+ } -+ const pointer = event; -+ if (typeof pointer.clientX !== "number") { -+ return void 0; -+ } -+ if (id !== void 0 && pointer.pointerId !== void 0 && pointer.pointerId !== id) { -+ return void 0; -+ } -+ return { id: pointer.pointerId, x: pointer.clientX, y: pointer.clientY }; -+} -+var POINTER_EVENT_HANDLERS = (onDown, onMove, onUp) => [ -+ ["pointerdown", onDown], -+ ["pointermove", onMove], -+ ["pointerup", onUp], -+ ["pointercancel", onUp], -+ ["touchstart", onDown], -+ ["touchmove", onMove], -+ ["touchend", onUp], -+ ["touchcancel", onUp] -+]; -+function now() { -+ return typeof performance !== "undefined" ? performance.now() : 0; -+} -+function isHover(event) { -+ const pointer = event; -+ return pointer.pointerType === "mouse" && pointer.buttons === 0; -+} -+function isScrollKey(event) { -+ if (event.altKey || event.ctrlKey || event.metaKey) { -+ return false; -+ } -+ return SCROLL_KEYS.has(event.key) && !isTextEntryTarget(event.target); -+} var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6034,6 +6418,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - contentOffset, - maintainVisibleContentPosition, - onScroll: onScroll2, -+ onUserInteraction, - onInternalScrollEnd, - onMomentumScrollEnd: _onMomentumScrollEnd, - showsHorizontalScrollIndicator = true, -@@ -6053,6 +6438,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6053,6 +6370,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -7570,7 +7084,7 @@ index 95465f2..6158381 100644 const getMaxScrollOffset = useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6073,8 +6463,170 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,6 +6395,99 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -7666,83 +7180,11 @@ index 95465f2..6158381 100644 + } + }, + [] -+ ); -+ const interactionArmedAtRef = useRef(Number.NEGATIVE_INFINITY); -+ const dragOriginRef = useRef(void 0); -+ const ownsEvent = useCallback((event) => { -+ const scroller = scrollRef.current; -+ const target = event.target; -+ if (!scroller || !(target == null ? void 0 : target.closest)) { -+ return true; -+ } -+ return target.closest(`[${SCROLLER_MARKER_ATTRIBUTE}]`) === scroller; -+ }, []); -+ const reportUserInteraction = useCallback(() => { -+ dragOriginRef.current = void 0; -+ onUserInteraction == null ? void 0 : onUserInteraction(); -+ }, [onUserInteraction]); -+ const onWheel = useCallback( -+ (event) => { -+ if (!isWindowScroll && !ownsEvent(event)) { -+ return; -+ } -+ if (now() - interactionArmedAtRef.current < WHEEL_MOMENTUM_GRACE_MS) { -+ return; -+ } -+ reportUserInteraction(); -+ }, -+ [isWindowScroll, ownsEvent, reportUserInteraction] -+ ); -+ const onPointerDown = useCallback( -+ (event) => { -+ if (dragOriginRef.current) { -+ return; -+ } -+ dragOriginRef.current = ownsEvent(event) ? pointerPosition(event, void 0) : void 0; -+ }, -+ [ownsEvent] -+ ); -+ const onPointerUp = useCallback(() => { -+ dragOriginRef.current = void 0; -+ }, []); -+ const onPointerMove = useCallback( -+ (event) => { -+ const origin = dragOriginRef.current; -+ if (!origin) { -+ return; -+ } -+ if (isHover(event)) { -+ dragOriginRef.current = void 0; -+ return; -+ } -+ const point = pointerPosition(event, origin.id); -+ if (!point) { -+ return; -+ } -+ if (Math.abs(point.x - origin.x) > USER_DRAG_SLOP || Math.abs(point.y - origin.y) > USER_DRAG_SLOP) { -+ reportUserInteraction(); -+ } -+ }, -+ [reportUserInteraction] -+ ); -+ const onKeyDown = useCallback( -+ (event) => { -+ if (isScrollKey(event)) { -+ reportUserInteraction(); -+ } -+ }, -+ [reportUserInteraction] + ); const scrollToLocalOffset = useCallback( -- (offset, animated) => { -+ (offset, animated, isCorrection) => { -+ if (!isCorrection) { -+ interactionArmedAtRef.current = now(); -+ } + (offset, animated) => { const scrollElement = scrollRef.current; - const target = getScrollTarget(); - if (!target || typeof target.scrollTo !== "function") { -@@ -6095,14 +6647,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6095,14 +6510,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); options.left = left; options.top = top; @@ -7769,14 +7211,7 @@ index 95465f2..6158381 100644 ); useImperativeHandle(ref, () => { const api = { -@@ -6123,13 +6682,12 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - target.scrollBy({ behavior: "auto", left: x, top: y }); - }, - scrollTo: (options) => { -- const { x = 0, y = 0, animated = true } = options; -- scrollToLocalOffset(horizontal ? x : y, animated); -+ const { x = 0, y = 0, animated = true, isCorrection } = options; -+ scrollToLocalOffset(horizontal ? x : y, animated, isCorrection); +@@ -6128,8 +6550,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -7786,7 +7221,7 @@ index 95465f2..6158381 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6142,7 +6700,12 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6142,7 +6563,12 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ if (!onScroll2 || !scrollRef.current) { return; } @@ -7800,7 +7235,7 @@ index 95465f2..6158381 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6162,7 +6725,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6162,7 +6588,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } }; onScroll2(scrollEvent); @@ -7809,63 +7244,7 @@ index 95465f2..6158381 100644 const scrollEventCoalescer = useRafCoalescer(emitScroll); const scrollEndFallbackRef = useRef(void 0); const emitScrollEnd = useCallback(() => { -@@ -6197,14 +6760,29 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] - ); - useLayoutEffect(() => { -+ var _a4; - const target = getScrollTarget(); - if (!target) return; - target.addEventListener("scroll", handleScroll, { passive: true }); -+ const listenerOptions = { capture: true, passive: true }; -+ const removeOptions = { capture: true }; -+ const interactionTarget = (_a4 = scrollRef.current) != null ? _a4 : void 0; -+ const keyTarget = isWindowScroll ? target : interactionTarget; -+ target.addEventListener("wheel", onWheel, listenerOptions); -+ keyTarget == null ? void 0 : keyTarget.addEventListener("keydown", onKeyDown, listenerOptions); -+ for (const [type, handler] of POINTER_EVENT_HANDLERS(onPointerDown, onPointerMove, onPointerUp)) { -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener(type, handler, listenerOptions); -+ } - if ("onscrollend" in target) { - target.addEventListener("scrollend", emitScrollEnd); - } - return () => { - target.removeEventListener("scroll", handleScroll); -+ target.removeEventListener("wheel", onWheel, removeOptions); -+ keyTarget == null ? void 0 : keyTarget.removeEventListener("keydown", onKeyDown, removeOptions); -+ for (const [type, handler] of POINTER_EVENT_HANDLERS(onPointerDown, onPointerMove, onPointerUp)) { -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener(type, handler, removeOptions); -+ } - if ("onscrollend" in target) { - target.removeEventListener("scrollend", emitScrollEnd); - } -@@ -6215,7 +6793,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - } - scrollEventCoalescer.cancel(); - }; -- }, [emitScrollEnd, getScrollTarget, handleScroll, scrollEventCoalescer]); -+ }, [ -+ emitScrollEnd, -+ getScrollTarget, -+ handleScroll, -+ onKeyDown, -+ onPointerDown, -+ onPointerMove, -+ onWheel, -+ scrollEventCoalescer -+ ]); - useEffect(() => { - const doScroll = () => { - if (contentOffset) { -@@ -6320,6 +6907,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - { - className: scrollViewClassName, - ref: scrollRef, -+ ...{ [SCROLLER_MARKER_ATTRIBUTE]: "" }, - ...webProps, - style: scrollViewStyle - }, -@@ -6358,21 +6946,6 @@ function useValueListener$(key, callback) { +@@ -6358,21 +6784,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -7887,7 +7266,7 @@ index 95465f2..6158381 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6390,8 +6963,6 @@ function ScrollAdjust() { +@@ -6390,8 +6801,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3.useRef(0); const lastScrollAdjustUserOffsetRef = React3.useRef(0); @@ -7896,7 +7275,7 @@ index 95465f2..6158381 100644 const contentNodeRef = React3.useRef(null); const callback = React3.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6402,7 +6973,7 @@ function ScrollAdjust() { +@@ -6402,7 +6811,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -7905,7 +7284,7 @@ index 95465f2..6158381 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6412,34 +6983,15 @@ function ScrollAdjust() { +@@ -6412,34 +6821,15 @@ function ScrollAdjust() { const scrollBy = () => scrollAdjustBy(el, axis.x * scrollDelta, axis.y * scrollDelta); contentNodeRef.current = contentNode; if (shouldScroll && contentNode) { @@ -7943,53 +7322,16 @@ index 95465f2..6158381 100644 } else { scrollBy(); } -@@ -6640,7 +7192,12 @@ var ListComponent = typedMemo(function ListComponent2({ - SnapOrScroll, - { - ...rest, -- ...ScrollComponent === ListComponentScrollView ? { onInternalScrollEnd, useWindowScroll } : {} , -+ ...ScrollComponent === ListComponentScrollView ? ( -+ // onUserInteraction is the web counterpart of onScrollBeginDrag: the view -+ // reports that the user moved the list, and LegendList decides what that -+ // means for a scroll in flight. -+ { onInternalScrollEnd, onUserInteraction: onInternalScrollBeginDrag, useWindowScroll } -+ ) : {} , - contentContainerStyle: [ - horizontal ? { height: "100%" } : {}, - contentContainerStyle, -@@ -7624,10 +8181,10 @@ function useThrottleDebounce(mode) { - const execute = useCallback( - (callback, delay, ...args) => { - { -- const now = Date.now(); -+ const now2 = Date.now(); - lastArgsRef.current = args; -- if (now - lastCallTimeRef.current >= delay) { -- lastCallTimeRef.current = now; -+ if (now2 - lastCallTimeRef.current >= delay) { -+ lastCallTimeRef.current = now2; - callback(...args); - clearTimeoutRef(); - } else { -@@ -7641,7 +8198,7 @@ function useThrottleDebounce(mode) { - lastArgsRef.current = null; - } - }, -- delay - (now - lastCallTimeRef.current) -+ delay - (now2 - lastCallTimeRef.current) - ); - } - } -@@ -8267,6 +8824,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8267,6 +8657,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); -+ releaseScrollTargetForUserInteraction(state); ++ clearScrollTargetSettle(state); (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js -index 914d2da..f97cffb 100644 +index 914d2da..c235070 100644 --- a/node_modules/@legendapp/list/react.js +++ b/node_modules/@legendapp/list/react.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -8598,16 +7940,18 @@ index 914d2da..f97cffb 100644 // src/core/getStartOffsetAdjustment.ts function getStartOffsetAdjustment(ctx) { const { state } = ctx; -@@ -1234,58 +635,273 @@ function getItemSizeAtIndex(ctx, index) { - if (index === void 0 || index < 0) { - return void 0; - } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+ const targetId = getId(ctx.state, index); -+ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+} -+ +@@ -1238,54 +639,269 @@ function getItemSizeAtIndex(ctx, index) { + return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); + } + +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +// src/core/calculateOffsetWithOffsetPosition.ts +function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { + var _a3; @@ -8811,7 +8155,12 @@ index 914d2da..f97cffb 100644 + const state = ctx == null ? void 0 : ctx.state; + if (!state) { + return; -+ } + } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } + const { + isStartReached, + props: { data, onStartReachedThreshold }, @@ -8826,7 +8175,22 @@ index 914d2da..f97cffb 100644 + if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { + state.isStartReached = false; + state.startReachedSnapshot = void 0; -+ } + } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } + set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); + set$(ctx, "isNearStart", scroll <= threshold); + const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; @@ -8853,50 +8217,19 @@ index 914d2da..f97cffb 100644 + state.startReachedSnapshot = snapshot; + } + ); -+ } + } +- return offset; } --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; -- } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } -- } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } -- } -- return offset; +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); - } - --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { ++} ++ +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { + var _a3, _b; @@ -8918,24 +8251,6 @@ index 914d2da..f97cffb 100644 } // src/core/finishScrollTo.ts -@@ -1334,7 +950,7 @@ var SCROLL_END_TARGET_EPSILON = 1; - function doScrollTo(ctx, params) { - var _a3, _b; - const state = ctx.state; -- const { animated, horizontal, offset } = params; -+ const { animated, horizontal, isCorrection, offset } = params; - state.scheduledWork.cancel("platformScrollCompletion"); - const scroller = state.refScroller.current; - const node = scroller == null ? void 0 : scroller.getScrollableNode(); -@@ -1346,7 +962,7 @@ function doScrollTo(ctx, params) { - const contentSize = isHorizontal ? getContentSize(ctx) : void 0; - const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; - const top = isHorizontal ? 0 : offset; -- scroller.scrollTo({ animated: isAnimated, x: left, y: top }); -+ scroller.scrollTo({ animated: isAnimated, isCorrection, x: left, y: top }); - if (isAnimated) { - const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; - listenForScrollEnd(ctx, { @@ -1413,16 +1029,191 @@ function listenForScrollEnd(ctx, params) { if (idleTimeout !== void 0) { clearTimeout(idleTimeout); @@ -9145,68 +8460,6 @@ index 914d2da..f97cffb 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1525,7 +1317,7 @@ var MVCP_POSITION_EPSILON = 0.1; - var MVCP_ANCHOR_LOCK_TTL_MS = 300; - var MVCP_ANCHOR_LOCK_QUIET_PASSES_TO_RELEASE = 2; - var NATIVE_END_CLAMP_EPSILON = 1; --function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { -+function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now2) { - if (!enableMVCPAnchorLock) { - state.mvcpAnchorLock = void 0; - return void 0; -@@ -1534,7 +1326,7 @@ function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { - if (!lock) { - return void 0; - } -- const isExpired = now > lock.expiresAt; -+ const isExpired = now2 > lock.expiresAt; - const isMissing = state.indexByKey.get(lock.id) === void 0; - if (isExpired || isMissing || !mvcpData) { - state.mvcpAnchorLock = void 0; -@@ -1544,7 +1336,7 @@ function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { - } - function updateAnchorLock(state, params) { - { -- const { anchorId, anchorPosition, dataChanged, now, positionDiff } = params; -+ const { anchorId, anchorPosition, dataChanged, now: now2, positionDiff } = params; - const enableMVCPAnchorLock = !!dataChanged || !!state.mvcpAnchorLock; - const mvcpData = state.props.maintainVisibleContentPosition.data; - if (!enableMVCPAnchorLock || !mvcpData || state.scrollingTo || !anchorId || anchorPosition === void 0) { -@@ -1557,7 +1349,7 @@ function updateAnchorLock(state, params) { - return; - } - state.mvcpAnchorLock = { -- expiresAt: now + MVCP_ANCHOR_LOCK_TTL_MS, -+ expiresAt: now2 + MVCP_ANCHOR_LOCK_TTL_MS, - id: anchorId, - position: anchorPosition, - quietPasses -@@ -1662,14 +1454,14 @@ function prepareMVCP(ctx, dataChanged) { - const { - maintainVisibleContentPosition: { data: mvcpData, size: mvcpScroll, shouldRestorePosition } - } = props; -- const now = Date.now(); -+ const now2 = Date.now(); - const enableMVCPAnchorLock = (!!dataChanged || !!state.mvcpAnchorLock); - const scrollingTo = state.scrollingTo; - if (dataChanged && state.pendingScrollToEnd && scrollingTo === void 0) { - state.mvcpAnchorLock = void 0; - return void 0; - } -- const anchorLock = resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) ; -+ const anchorLock = resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now2) ; - let prevPosition; - let targetId; - const idsInViewWithPositions = []; -@@ -1786,7 +1578,7 @@ function prepareMVCP(ctx, dataChanged) { - anchorId: anchorIdForLock, - anchorPosition: anchorPositionForLock, - dataChanged, -- now, -+ now: now2, - positionDiff - }); - if (shouldQueueNativeMVCPAdjust()) { @@ -1809,6 +1601,27 @@ function prepareMVCP(ctx, dataChanged) { } } @@ -9235,12 +8488,24 @@ index 914d2da..f97cffb 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2068,57 +1881,392 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { - ctx.state.scrollTargetPinnedRange = void 0; - } - } --function scrollTo(ctx, params) { -- var _a3, _b; +@@ -2055,70 +1868,402 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { + if (nextTop === void 0 || nextTop > viewportEnd) { + break; + } +- end++; ++ end++; ++ } ++ return { end, start }; ++} ++function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { ++ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); ++ if (range) { ++ ctx.state.scrollTargetPinnedRange = range; ++ ctx.state.scrollForNextCalculateItemsInView = void 0; ++ } else { ++ ctx.state.scrollTargetPinnedRange = void 0; ++ } ++} +function scrollTo(ctx, params) { + var _a3, _b, _c; + const state = ctx.state; @@ -9298,7 +8563,7 @@ index 914d2da..f97cffb 100644 + } + } + if (forceScroll || !isInitialScroll || Platform.OS === "android") { -+ doScrollTo(ctx, { animated, horizontal, isCorrection: noScrollingTo, offset }); ++ doScrollTo(ctx, { animated, horizontal, offset }); + } else { + state.scroll = offset; + } @@ -9310,9 +8575,6 @@ index 914d2da..f97cffb 100644 +var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; -+function releaseScrollTargetForUserInteraction(state) { -+ clearScrollTargetSettle(state); -+} +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; + state.scheduledWork.cancel("scrollTargetSettle"); @@ -9329,11 +8591,11 @@ index 914d2da..f97cffb 100644 + return; + } + clearScrollTargetSettle(state); -+ const now2 = Date.now(); ++ const now = Date.now(); + state.scrollTargetSettle = { + corrections: 0, -+ deadline: now2 + SETTLE_MAX_MS, -+ expiresAt: now2 + SETTLE_TTL_MS, ++ deadline: now + SETTLE_MAX_MS, ++ expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, + ownsScrollingTo: state.scrollingTo !== void 0, @@ -9416,8 +8678,8 @@ index 914d2da..f97cffb 100644 + clearScrollTargetSettle(state); + return false; + } -+ const now2 = Date.now(); -+ if (now2 > settle.expiresAt || now2 > settle.deadline) { ++ const now = Date.now(); ++ if (now > settle.expiresAt || now > settle.deadline) { + clearScrollTargetSettle(state); + return false; + } @@ -9435,7 +8697,7 @@ index 914d2da..f97cffb 100644 + return false; + } + settle.quietPasses = 0; -+ settle.expiresAt = now2 + SETTLE_TTL_MS; ++ settle.expiresAt = now + SETTLE_TTL_MS; + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} @@ -9534,8 +8796,17 @@ index 914d2da..f97cffb 100644 + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); + } + } -+ } -+} + } +- return { end, start }; + } +-function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); +- if (range) { +- ctx.state.scrollTargetPinnedRange = range; +- ctx.state.scrollForNextCalculateItemsInView = void 0; +- } else { +- ctx.state.scrollTargetPinnedRange = void 0; +- } + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -9543,7 +8814,9 @@ index 914d2da..f97cffb 100644 + state.scroll = offset; + state.scrollPending = offset; + state.scrollPrev = offset; -+} + } +-function scrollTo(ctx, params) { +- var _a3, _b; +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -9554,24 +8827,6 @@ index 914d2da..f97cffb 100644 + setInitialScrollSession(state); +} +function supersedeInitialScroll(ctx) { -+ var _a3, _b, _c; -+ const state = ctx.state; -+ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; -+ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { -+ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { -+ cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -+ } -+ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { -+ cancelScrollCompletionChecks(state); -+ state.scrollingTo = void 0; -+ state.scrollTargetPinnedRange = void 0; -+ } -+ initialScrollCompletion.resetFlags(state); -+ setInitialScrollSession(state, { bootstrap: null }); -+ finishInitialScroll(ctx); -+ } -+} -+function finishInitialScroll(ctx, options) { + var _a3, _b, _c; const state = ctx.state; - const { noScrollingTo, forceScroll, ...scrollTarget } = params; @@ -9594,7 +8849,11 @@ index 914d2da..f97cffb 100644 - if (!noScrollingTo) { - if (isInitialScroll) { - initialScrollCompletion.resetFlags(state); -- } ++ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; ++ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { ++ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { ++ cancelAnimationFrame(bootstrapInitialScroll.frameHandle); + } - const averageSizeSnapshot = getAverageSizeSnapshot(state); - state.scrollingTo = { - ...scrollTarget, @@ -9604,13 +8863,14 @@ index 914d2da..f97cffb 100644 - }; - if (!isInitialScroll) { - pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); -+ if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { -+ syncInitialScrollOffset(state, options.resolvedOffset); -+ } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { -+ const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); -+ if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { -+ syncInitialScrollOffset(state, observedOffset); ++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { ++ cancelScrollCompletionChecks(state); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; } ++ initialScrollCompletion.resetFlags(state); ++ setInitialScrollSession(state, { bootstrap: null }); ++ finishInitialScroll(ctx); } - state.scrollPending = targetOffset; - syncInitialScrollNativeWatchdog(state, { isInitialScroll, requestedOffset: offset, targetOffset }); @@ -9618,6 +8878,18 @@ index 914d2da..f97cffb 100644 - if (animated) { - if (state.scrollTargetPinnedRange) { - (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++} ++function finishInitialScroll(ctx, options) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { ++ syncInitialScrollOffset(state, options.resolvedOffset); ++ } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { ++ const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); ++ if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { ++ syncInitialScrollOffset(state, observedOffset); ++ } ++ } + const complete = () => { + var _a4, _b2, _c2, _d, _e; + const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; @@ -9672,7 +8944,7 @@ index 914d2da..f97cffb 100644 } // src/core/scrollToIndex.ts -@@ -4350,6 +4498,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4350,6 +4495,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -9680,7 +8952,7 @@ index 914d2da..f97cffb 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4367,8 +4516,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4367,8 +4513,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -9701,7 +8973,7 @@ index 914d2da..f97cffb 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5927,6 +6086,21 @@ function getDocumentScrollerNode() { +@@ -5927,6 +6083,21 @@ function getDocumentScrollerNode() { } return document.scrollingElement || document.documentElement || document.body; } @@ -9723,7 +8995,7 @@ index 914d2da..f97cffb 100644 function getWindowScrollPosition() { var _a3, _b, _c, _d; if (typeof window === "undefined") { -@@ -6003,9 +6177,219 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -6003,9 +6174,155 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll }; } @@ -9871,87 +9143,15 @@ index 914d2da..f97cffb 100644 + // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; -+var SCROLLER_MARKER_ATTRIBUTE = "data-legend-list-scroller"; var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; -+var SMOOTH_SCROLL_MAX_MS = 2e3; -+var BORROW_WATCH_FRAME_INTERVAL = 3; -+var BORROW_MEASURE_ATTEMPTS = 2; -+var USER_DRAG_SLOP = 8; -+var WHEEL_MOMENTUM_GRACE_MS = 150; -+var SCROLL_KEYS = /* @__PURE__ */ new Set([ -+ " ", -+ "ArrowDown", -+ "ArrowLeft", -+ "ArrowRight", -+ "ArrowUp", -+ "End", -+ "Home", -+ "PageDown", -+ "PageUp" -+]); -+var NON_SCROLLING_KEY_TARGETS = "input,textarea,select,button,[contenteditable],[role=textbox],[role=button]"; -+function isTextEntryTarget(target) { -+ var _a3; -+ const element = target; -+ if (!element) { -+ return false; -+ } -+ if (element.isContentEditable) { -+ return true; -+ } -+ return !!((_a3 = element.closest) == null ? void 0 : _a3.call(element, NON_SCROLLING_KEY_TARGETS)); -+} -+function pointerPosition(event, id) { -+ const touches = event.touches; -+ if (touches == null ? void 0 : touches.length) { -+ const touch = id === void 0 ? touches[0] : [...touches].find((t) => t.identifier === id); -+ return touch ? { id: touch.identifier, x: touch.clientX, y: touch.clientY } : void 0; -+ } -+ const pointer = event; -+ if (typeof pointer.clientX !== "number") { -+ return void 0; -+ } -+ if (id !== void 0 && pointer.pointerId !== void 0 && pointer.pointerId !== id) { -+ return void 0; -+ } -+ return { id: pointer.pointerId, x: pointer.clientX, y: pointer.clientY }; -+} -+var POINTER_EVENT_HANDLERS = (onDown, onMove, onUp) => [ -+ ["pointerdown", onDown], -+ ["pointermove", onMove], -+ ["pointerup", onUp], -+ ["pointercancel", onUp], -+ ["touchstart", onDown], -+ ["touchmove", onMove], -+ ["touchend", onUp], -+ ["touchcancel", onUp] -+]; -+function now() { -+ return typeof performance !== "undefined" ? performance.now() : 0; -+} -+function isHover(event) { -+ const pointer = event; -+ return pointer.pointerType === "mouse" && pointer.buttons === 0; -+} -+function isScrollKey(event) { -+ if (event.altKey || event.ctrlKey || event.metaKey) { -+ return false; -+ } -+ return SCROLL_KEYS.has(event.key) && !isTextEntryTarget(event.target); -+} ++var SMOOTH_SCROLL_MAX_MS = 2e3; ++var BORROW_WATCH_FRAME_INTERVAL = 3; ++var BORROW_MEASURE_ATTEMPTS = 2; var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6055,6 +6439,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - contentOffset, - maintainVisibleContentPosition, - onScroll: onScroll2, -+ onUserInteraction, - onInternalScrollEnd, - onMomentumScrollEnd: _onMomentumScrollEnd, - showsHorizontalScrollIndicator = true, -@@ -6074,6 +6459,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6074,6 +6391,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -9963,7 +9163,7 @@ index 914d2da..f97cffb 100644 const getMaxScrollOffset = React3.useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6094,8 +6484,170 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,6 +6416,99 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -10059,83 +9259,11 @@ index 914d2da..f97cffb 100644 + } + }, + [] -+ ); -+ const interactionArmedAtRef = React3.useRef(Number.NEGATIVE_INFINITY); -+ const dragOriginRef = React3.useRef(void 0); -+ const ownsEvent = React3.useCallback((event) => { -+ const scroller = scrollRef.current; -+ const target = event.target; -+ if (!scroller || !(target == null ? void 0 : target.closest)) { -+ return true; -+ } -+ return target.closest(`[${SCROLLER_MARKER_ATTRIBUTE}]`) === scroller; -+ }, []); -+ const reportUserInteraction = React3.useCallback(() => { -+ dragOriginRef.current = void 0; -+ onUserInteraction == null ? void 0 : onUserInteraction(); -+ }, [onUserInteraction]); -+ const onWheel = React3.useCallback( -+ (event) => { -+ if (!isWindowScroll && !ownsEvent(event)) { -+ return; -+ } -+ if (now() - interactionArmedAtRef.current < WHEEL_MOMENTUM_GRACE_MS) { -+ return; -+ } -+ reportUserInteraction(); -+ }, -+ [isWindowScroll, ownsEvent, reportUserInteraction] -+ ); -+ const onPointerDown = React3.useCallback( -+ (event) => { -+ if (dragOriginRef.current) { -+ return; -+ } -+ dragOriginRef.current = ownsEvent(event) ? pointerPosition(event, void 0) : void 0; -+ }, -+ [ownsEvent] -+ ); -+ const onPointerUp = React3.useCallback(() => { -+ dragOriginRef.current = void 0; -+ }, []); -+ const onPointerMove = React3.useCallback( -+ (event) => { -+ const origin = dragOriginRef.current; -+ if (!origin) { -+ return; -+ } -+ if (isHover(event)) { -+ dragOriginRef.current = void 0; -+ return; -+ } -+ const point = pointerPosition(event, origin.id); -+ if (!point) { -+ return; -+ } -+ if (Math.abs(point.x - origin.x) > USER_DRAG_SLOP || Math.abs(point.y - origin.y) > USER_DRAG_SLOP) { -+ reportUserInteraction(); -+ } -+ }, -+ [reportUserInteraction] -+ ); -+ const onKeyDown = React3.useCallback( -+ (event) => { -+ if (isScrollKey(event)) { -+ reportUserInteraction(); -+ } -+ }, -+ [reportUserInteraction] + ); const scrollToLocalOffset = React3.useCallback( -- (offset, animated) => { -+ (offset, animated, isCorrection) => { -+ if (!isCorrection) { -+ interactionArmedAtRef.current = now(); -+ } + (offset, animated) => { const scrollElement = scrollRef.current; - const target = getScrollTarget(); - if (!target || typeof target.scrollTo !== "function") { -@@ -6116,14 +6668,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6116,14 +6531,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); options.left = left; options.top = top; @@ -10162,14 +9290,7 @@ index 914d2da..f97cffb 100644 ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6144,13 +6703,12 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - target.scrollBy({ behavior: "auto", left: x, top: y }); - }, - scrollTo: (options) => { -- const { x = 0, y = 0, animated = true } = options; -- scrollToLocalOffset(horizontal ? x : y, animated); -+ const { x = 0, y = 0, animated = true, isCorrection } = options; -+ scrollToLocalOffset(horizontal ? x : y, animated, isCorrection); +@@ -6149,8 +6571,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -10179,7 +9300,7 @@ index 914d2da..f97cffb 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6163,7 +6721,12 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6163,7 +6584,12 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView if (!onScroll2 || !scrollRef.current) { return; } @@ -10193,7 +9314,7 @@ index 914d2da..f97cffb 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6183,7 +6746,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6183,7 +6609,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } }; onScroll2(scrollEvent); @@ -10202,63 +9323,7 @@ index 914d2da..f97cffb 100644 const scrollEventCoalescer = useRafCoalescer(emitScroll); const scrollEndFallbackRef = React3.useRef(void 0); const emitScrollEnd = React3.useCallback(() => { -@@ -6218,14 +6781,29 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] - ); - React3.useLayoutEffect(() => { -+ var _a4; - const target = getScrollTarget(); - if (!target) return; - target.addEventListener("scroll", handleScroll, { passive: true }); -+ const listenerOptions = { capture: true, passive: true }; -+ const removeOptions = { capture: true }; -+ const interactionTarget = (_a4 = scrollRef.current) != null ? _a4 : void 0; -+ const keyTarget = isWindowScroll ? target : interactionTarget; -+ target.addEventListener("wheel", onWheel, listenerOptions); -+ keyTarget == null ? void 0 : keyTarget.addEventListener("keydown", onKeyDown, listenerOptions); -+ for (const [type, handler] of POINTER_EVENT_HANDLERS(onPointerDown, onPointerMove, onPointerUp)) { -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener(type, handler, listenerOptions); -+ } - if ("onscrollend" in target) { - target.addEventListener("scrollend", emitScrollEnd); - } - return () => { - target.removeEventListener("scroll", handleScroll); -+ target.removeEventListener("wheel", onWheel, removeOptions); -+ keyTarget == null ? void 0 : keyTarget.removeEventListener("keydown", onKeyDown, removeOptions); -+ for (const [type, handler] of POINTER_EVENT_HANDLERS(onPointerDown, onPointerMove, onPointerUp)) { -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener(type, handler, removeOptions); -+ } - if ("onscrollend" in target) { - target.removeEventListener("scrollend", emitScrollEnd); - } -@@ -6236,7 +6814,16 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - } - scrollEventCoalescer.cancel(); - }; -- }, [emitScrollEnd, getScrollTarget, handleScroll, scrollEventCoalescer]); -+ }, [ -+ emitScrollEnd, -+ getScrollTarget, -+ handleScroll, -+ onKeyDown, -+ onPointerDown, -+ onPointerMove, -+ onWheel, -+ scrollEventCoalescer -+ ]); - React3.useEffect(() => { - const doScroll = () => { - if (contentOffset) { -@@ -6341,6 +6928,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - { - className: scrollViewClassName, - ref: scrollRef, -+ ...{ [SCROLLER_MARKER_ATTRIBUTE]: "" }, - ...webProps, - style: scrollViewStyle - }, -@@ -6379,21 +6967,6 @@ function useValueListener$(key, callback) { +@@ -6379,21 +6805,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -10280,7 +9345,7 @@ index 914d2da..f97cffb 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6411,8 +6984,6 @@ function ScrollAdjust() { +@@ -6411,8 +6822,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3__namespace.useRef(0); const lastScrollAdjustUserOffsetRef = React3__namespace.useRef(0); @@ -10289,7 +9354,7 @@ index 914d2da..f97cffb 100644 const contentNodeRef = React3__namespace.useRef(null); const callback = React3__namespace.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6423,7 +6994,7 @@ function ScrollAdjust() { +@@ -6423,7 +6832,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -10298,7 +9363,7 @@ index 914d2da..f97cffb 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6433,34 +7004,15 @@ function ScrollAdjust() { +@@ -6433,34 +6842,15 @@ function ScrollAdjust() { const scrollBy = () => scrollAdjustBy(el, axis.x * scrollDelta, axis.y * scrollDelta); contentNodeRef.current = contentNode; if (shouldScroll && contentNode) { @@ -10336,53 +9401,16 @@ index 914d2da..f97cffb 100644 } else { scrollBy(); } -@@ -6661,7 +7213,12 @@ var ListComponent = typedMemo(function ListComponent2({ - SnapOrScroll, - { - ...rest, -- ...ScrollComponent === ListComponentScrollView ? { onInternalScrollEnd, useWindowScroll } : {} , -+ ...ScrollComponent === ListComponentScrollView ? ( -+ // onUserInteraction is the web counterpart of onScrollBeginDrag: the view -+ // reports that the user moved the list, and LegendList decides what that -+ // means for a scroll in flight. -+ { onInternalScrollEnd, onUserInteraction: onInternalScrollBeginDrag, useWindowScroll } -+ ) : {} , - contentContainerStyle: [ - horizontal ? { height: "100%" } : {}, - contentContainerStyle, -@@ -7645,10 +8202,10 @@ function useThrottleDebounce(mode) { - const execute = React3.useCallback( - (callback, delay, ...args) => { - { -- const now = Date.now(); -+ const now2 = Date.now(); - lastArgsRef.current = args; -- if (now - lastCallTimeRef.current >= delay) { -- lastCallTimeRef.current = now; -+ if (now2 - lastCallTimeRef.current >= delay) { -+ lastCallTimeRef.current = now2; - callback(...args); - clearTimeoutRef(); - } else { -@@ -7662,7 +8219,7 @@ function useThrottleDebounce(mode) { - lastArgsRef.current = null; - } - }, -- delay - (now - lastCallTimeRef.current) -+ delay - (now2 - lastCallTimeRef.current) - ); - } - } -@@ -8288,6 +8845,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8288,6 +8678,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); -+ releaseScrollTargetForUserInteraction(state); ++ clearScrollTargetSettle(state); (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs -index 95465f2..6158381 100644 +index 95465f2..e8a3fb5 100644 --- a/node_modules/@legendapp/list/react.mjs +++ b/node_modules/@legendapp/list/react.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -10991,16 +10019,18 @@ index 95465f2..6158381 100644 // src/core/getStartOffsetAdjustment.ts function getStartOffsetAdjustment(ctx) { const { state } = ctx; -@@ -1213,58 +614,273 @@ function getItemSizeAtIndex(ctx, index) { - if (index === void 0 || index < 0) { - return void 0; - } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+ const targetId = getId(ctx.state, index); -+ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+} -+ +@@ -1217,54 +618,269 @@ function getItemSizeAtIndex(ctx, index) { + return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); + } + +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +// src/core/calculateOffsetWithOffsetPosition.ts +function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { + var _a3; @@ -11204,7 +10234,12 @@ index 95465f2..6158381 100644 + const state = ctx == null ? void 0 : ctx.state; + if (!state) { + return; -+ } + } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } + const { + isStartReached, + props: { data, onStartReachedThreshold }, @@ -11219,7 +10254,22 @@ index 95465f2..6158381 100644 + if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { + state.isStartReached = false; + state.startReachedSnapshot = void 0; -+ } + } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } + set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); + set$(ctx, "isNearStart", scroll <= threshold); + const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; @@ -11246,50 +10296,19 @@ index 95465f2..6158381 100644 + state.startReachedSnapshot = snapshot; + } + ); -+ } + } +- return offset; } --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; -- } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } -- } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } -- } -- return offset; +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); - } - --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { ++} ++ +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { + var _a3, _b; @@ -11311,24 +10330,6 @@ index 95465f2..6158381 100644 } // src/core/finishScrollTo.ts -@@ -1313,7 +929,7 @@ var SCROLL_END_TARGET_EPSILON = 1; - function doScrollTo(ctx, params) { - var _a3, _b; - const state = ctx.state; -- const { animated, horizontal, offset } = params; -+ const { animated, horizontal, isCorrection, offset } = params; - state.scheduledWork.cancel("platformScrollCompletion"); - const scroller = state.refScroller.current; - const node = scroller == null ? void 0 : scroller.getScrollableNode(); -@@ -1325,7 +941,7 @@ function doScrollTo(ctx, params) { - const contentSize = isHorizontal ? getContentSize(ctx) : void 0; - const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; - const top = isHorizontal ? 0 : offset; -- scroller.scrollTo({ animated: isAnimated, x: left, y: top }); -+ scroller.scrollTo({ animated: isAnimated, isCorrection, x: left, y: top }); - if (isAnimated) { - const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; - listenForScrollEnd(ctx, { @@ -1392,16 +1008,191 @@ function listenForScrollEnd(ctx, params) { if (idleTimeout !== void 0) { clearTimeout(idleTimeout); @@ -11538,68 +10539,6 @@ index 95465f2..6158381 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1504,7 +1296,7 @@ var MVCP_POSITION_EPSILON = 0.1; - var MVCP_ANCHOR_LOCK_TTL_MS = 300; - var MVCP_ANCHOR_LOCK_QUIET_PASSES_TO_RELEASE = 2; - var NATIVE_END_CLAMP_EPSILON = 1; --function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { -+function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now2) { - if (!enableMVCPAnchorLock) { - state.mvcpAnchorLock = void 0; - return void 0; -@@ -1513,7 +1305,7 @@ function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { - if (!lock) { - return void 0; - } -- const isExpired = now > lock.expiresAt; -+ const isExpired = now2 > lock.expiresAt; - const isMissing = state.indexByKey.get(lock.id) === void 0; - if (isExpired || isMissing || !mvcpData) { - state.mvcpAnchorLock = void 0; -@@ -1523,7 +1315,7 @@ function resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) { - } - function updateAnchorLock(state, params) { - { -- const { anchorId, anchorPosition, dataChanged, now, positionDiff } = params; -+ const { anchorId, anchorPosition, dataChanged, now: now2, positionDiff } = params; - const enableMVCPAnchorLock = !!dataChanged || !!state.mvcpAnchorLock; - const mvcpData = state.props.maintainVisibleContentPosition.data; - if (!enableMVCPAnchorLock || !mvcpData || state.scrollingTo || !anchorId || anchorPosition === void 0) { -@@ -1536,7 +1328,7 @@ function updateAnchorLock(state, params) { - return; - } - state.mvcpAnchorLock = { -- expiresAt: now + MVCP_ANCHOR_LOCK_TTL_MS, -+ expiresAt: now2 + MVCP_ANCHOR_LOCK_TTL_MS, - id: anchorId, - position: anchorPosition, - quietPasses -@@ -1641,14 +1433,14 @@ function prepareMVCP(ctx, dataChanged) { - const { - maintainVisibleContentPosition: { data: mvcpData, size: mvcpScroll, shouldRestorePosition } - } = props; -- const now = Date.now(); -+ const now2 = Date.now(); - const enableMVCPAnchorLock = (!!dataChanged || !!state.mvcpAnchorLock); - const scrollingTo = state.scrollingTo; - if (dataChanged && state.pendingScrollToEnd && scrollingTo === void 0) { - state.mvcpAnchorLock = void 0; - return void 0; - } -- const anchorLock = resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now) ; -+ const anchorLock = resolveAnchorLock(state, enableMVCPAnchorLock, mvcpData, now2) ; - let prevPosition; - let targetId; - const idsInViewWithPositions = []; -@@ -1765,7 +1557,7 @@ function prepareMVCP(ctx, dataChanged) { - anchorId: anchorIdForLock, - anchorPosition: anchorPositionForLock, - dataChanged, -- now, -+ now: now2, - positionDiff - }); - if (shouldQueueNativeMVCPAdjust()) { @@ -1788,6 +1580,27 @@ function prepareMVCP(ctx, dataChanged) { } } @@ -11628,12 +10567,24 @@ index 95465f2..6158381 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2047,57 +1860,392 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { - ctx.state.scrollTargetPinnedRange = void 0; - } - } --function scrollTo(ctx, params) { -- var _a3, _b; +@@ -2034,70 +1847,402 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { + if (nextTop === void 0 || nextTop > viewportEnd) { + break; + } +- end++; ++ end++; ++ } ++ return { end, start }; ++} ++function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { ++ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); ++ if (range) { ++ ctx.state.scrollTargetPinnedRange = range; ++ ctx.state.scrollForNextCalculateItemsInView = void 0; ++ } else { ++ ctx.state.scrollTargetPinnedRange = void 0; ++ } ++} +function scrollTo(ctx, params) { + var _a3, _b, _c; + const state = ctx.state; @@ -11691,7 +10642,7 @@ index 95465f2..6158381 100644 + } + } + if (forceScroll || !isInitialScroll || Platform.OS === "android") { -+ doScrollTo(ctx, { animated, horizontal, isCorrection: noScrollingTo, offset }); ++ doScrollTo(ctx, { animated, horizontal, offset }); + } else { + state.scroll = offset; + } @@ -11703,9 +10654,6 @@ index 95465f2..6158381 100644 +var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; -+function releaseScrollTargetForUserInteraction(state) { -+ clearScrollTargetSettle(state); -+} +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; + state.scheduledWork.cancel("scrollTargetSettle"); @@ -11722,11 +10670,11 @@ index 95465f2..6158381 100644 + return; + } + clearScrollTargetSettle(state); -+ const now2 = Date.now(); ++ const now = Date.now(); + state.scrollTargetSettle = { + corrections: 0, -+ deadline: now2 + SETTLE_MAX_MS, -+ expiresAt: now2 + SETTLE_TTL_MS, ++ deadline: now + SETTLE_MAX_MS, ++ expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, + ownsScrollingTo: state.scrollingTo !== void 0, @@ -11809,8 +10757,8 @@ index 95465f2..6158381 100644 + clearScrollTargetSettle(state); + return false; + } -+ const now2 = Date.now(); -+ if (now2 > settle.expiresAt || now2 > settle.deadline) { ++ const now = Date.now(); ++ if (now > settle.expiresAt || now > settle.deadline) { + clearScrollTargetSettle(state); + return false; + } @@ -11828,7 +10776,7 @@ index 95465f2..6158381 100644 + return false; + } + settle.quietPasses = 0; -+ settle.expiresAt = now2 + SETTLE_TTL_MS; ++ settle.expiresAt = now + SETTLE_TTL_MS; + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} @@ -11927,8 +10875,17 @@ index 95465f2..6158381 100644 + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); + } + } -+ } -+} + } +- return { end, start }; + } +-function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); +- if (range) { +- ctx.state.scrollTargetPinnedRange = range; +- ctx.state.scrollForNextCalculateItemsInView = void 0; +- } else { +- ctx.state.scrollTargetPinnedRange = void 0; +- } + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -11936,7 +10893,9 @@ index 95465f2..6158381 100644 + state.scroll = offset; + state.scrollPending = offset; + state.scrollPrev = offset; -+} + } +-function scrollTo(ctx, params) { +- var _a3, _b; +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -11947,24 +10906,6 @@ index 95465f2..6158381 100644 + setInitialScrollSession(state); +} +function supersedeInitialScroll(ctx) { -+ var _a3, _b, _c; -+ const state = ctx.state; -+ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; -+ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { -+ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { -+ cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -+ } -+ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { -+ cancelScrollCompletionChecks(state); -+ state.scrollingTo = void 0; -+ state.scrollTargetPinnedRange = void 0; -+ } -+ initialScrollCompletion.resetFlags(state); -+ setInitialScrollSession(state, { bootstrap: null }); -+ finishInitialScroll(ctx); -+ } -+} -+function finishInitialScroll(ctx, options) { + var _a3, _b, _c; const state = ctx.state; - const { noScrollingTo, forceScroll, ...scrollTarget } = params; @@ -11987,7 +10928,11 @@ index 95465f2..6158381 100644 - if (!noScrollingTo) { - if (isInitialScroll) { - initialScrollCompletion.resetFlags(state); -- } ++ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; ++ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { ++ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { ++ cancelAnimationFrame(bootstrapInitialScroll.frameHandle); + } - const averageSizeSnapshot = getAverageSizeSnapshot(state); - state.scrollingTo = { - ...scrollTarget, @@ -11997,13 +10942,14 @@ index 95465f2..6158381 100644 - }; - if (!isInitialScroll) { - pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); -+ if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { -+ syncInitialScrollOffset(state, options.resolvedOffset); -+ } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { -+ const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); -+ if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { -+ syncInitialScrollOffset(state, observedOffset); ++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { ++ cancelScrollCompletionChecks(state); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; } ++ initialScrollCompletion.resetFlags(state); ++ setInitialScrollSession(state, { bootstrap: null }); ++ finishInitialScroll(ctx); } - state.scrollPending = targetOffset; - syncInitialScrollNativeWatchdog(state, { isInitialScroll, requestedOffset: offset, targetOffset }); @@ -12011,6 +10957,18 @@ index 95465f2..6158381 100644 - if (animated) { - if (state.scrollTargetPinnedRange) { - (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++} ++function finishInitialScroll(ctx, options) { ++ var _a3, _b, _c; ++ const state = ctx.state; ++ if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { ++ syncInitialScrollOffset(state, options.resolvedOffset); ++ } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { ++ const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); ++ if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { ++ syncInitialScrollOffset(state, observedOffset); ++ } ++ } + const complete = () => { + var _a4, _b2, _c2, _d, _e; + const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; @@ -12065,7 +11023,7 @@ index 95465f2..6158381 100644 } // src/core/scrollToIndex.ts -@@ -4329,6 +4477,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4329,6 +4474,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -12073,7 +11031,7 @@ index 95465f2..6158381 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4346,8 +4495,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4346,8 +4492,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -12094,7 +11052,7 @@ index 95465f2..6158381 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5906,6 +6065,21 @@ function getDocumentScrollerNode() { +@@ -5906,6 +6062,21 @@ function getDocumentScrollerNode() { } return document.scrollingElement || document.documentElement || document.body; } @@ -12116,7 +11074,7 @@ index 95465f2..6158381 100644 function getWindowScrollPosition() { var _a3, _b, _c, _d; if (typeof window === "undefined") { -@@ -5982,9 +6156,219 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -5982,9 +6153,155 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll }; } @@ -12264,87 +11222,15 @@ index 95465f2..6158381 100644 + // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; -+var SCROLLER_MARKER_ATTRIBUTE = "data-legend-list-scroller"; var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; +var SMOOTH_SCROLL_MAX_MS = 2e3; +var BORROW_WATCH_FRAME_INTERVAL = 3; +var BORROW_MEASURE_ATTEMPTS = 2; -+var USER_DRAG_SLOP = 8; -+var WHEEL_MOMENTUM_GRACE_MS = 150; -+var SCROLL_KEYS = /* @__PURE__ */ new Set([ -+ " ", -+ "ArrowDown", -+ "ArrowLeft", -+ "ArrowRight", -+ "ArrowUp", -+ "End", -+ "Home", -+ "PageDown", -+ "PageUp" -+]); -+var NON_SCROLLING_KEY_TARGETS = "input,textarea,select,button,[contenteditable],[role=textbox],[role=button]"; -+function isTextEntryTarget(target) { -+ var _a3; -+ const element = target; -+ if (!element) { -+ return false; -+ } -+ if (element.isContentEditable) { -+ return true; -+ } -+ return !!((_a3 = element.closest) == null ? void 0 : _a3.call(element, NON_SCROLLING_KEY_TARGETS)); -+} -+function pointerPosition(event, id) { -+ const touches = event.touches; -+ if (touches == null ? void 0 : touches.length) { -+ const touch = id === void 0 ? touches[0] : [...touches].find((t) => t.identifier === id); -+ return touch ? { id: touch.identifier, x: touch.clientX, y: touch.clientY } : void 0; -+ } -+ const pointer = event; -+ if (typeof pointer.clientX !== "number") { -+ return void 0; -+ } -+ if (id !== void 0 && pointer.pointerId !== void 0 && pointer.pointerId !== id) { -+ return void 0; -+ } -+ return { id: pointer.pointerId, x: pointer.clientX, y: pointer.clientY }; -+} -+var POINTER_EVENT_HANDLERS = (onDown, onMove, onUp) => [ -+ ["pointerdown", onDown], -+ ["pointermove", onMove], -+ ["pointerup", onUp], -+ ["pointercancel", onUp], -+ ["touchstart", onDown], -+ ["touchmove", onMove], -+ ["touchend", onUp], -+ ["touchcancel", onUp] -+]; -+function now() { -+ return typeof performance !== "undefined" ? performance.now() : 0; -+} -+function isHover(event) { -+ const pointer = event; -+ return pointer.pointerType === "mouse" && pointer.buttons === 0; -+} -+function isScrollKey(event) { -+ if (event.altKey || event.ctrlKey || event.metaKey) { -+ return false; -+ } -+ return SCROLL_KEYS.has(event.key) && !isTextEntryTarget(event.target); -+} var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6034,6 +6418,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - contentOffset, - maintainVisibleContentPosition, - onScroll: onScroll2, -+ onUserInteraction, - onInternalScrollEnd, - onMomentumScrollEnd: _onMomentumScrollEnd, - showsHorizontalScrollIndicator = true, -@@ -6053,6 +6438,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6053,6 +6370,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), [isWindowScroll] ); @@ -12356,7 +11242,7 @@ index 95465f2..6158381 100644 const getMaxScrollOffset = useCallback(() => { const scrollElement = scrollRef.current; const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6073,8 +6463,170 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,6 +6395,99 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -12452,83 +11338,11 @@ index 95465f2..6158381 100644 + } + }, + [] -+ ); -+ const interactionArmedAtRef = useRef(Number.NEGATIVE_INFINITY); -+ const dragOriginRef = useRef(void 0); -+ const ownsEvent = useCallback((event) => { -+ const scroller = scrollRef.current; -+ const target = event.target; -+ if (!scroller || !(target == null ? void 0 : target.closest)) { -+ return true; -+ } -+ return target.closest(`[${SCROLLER_MARKER_ATTRIBUTE}]`) === scroller; -+ }, []); -+ const reportUserInteraction = useCallback(() => { -+ dragOriginRef.current = void 0; -+ onUserInteraction == null ? void 0 : onUserInteraction(); -+ }, [onUserInteraction]); -+ const onWheel = useCallback( -+ (event) => { -+ if (!isWindowScroll && !ownsEvent(event)) { -+ return; -+ } -+ if (now() - interactionArmedAtRef.current < WHEEL_MOMENTUM_GRACE_MS) { -+ return; -+ } -+ reportUserInteraction(); -+ }, -+ [isWindowScroll, ownsEvent, reportUserInteraction] -+ ); -+ const onPointerDown = useCallback( -+ (event) => { -+ if (dragOriginRef.current) { -+ return; -+ } -+ dragOriginRef.current = ownsEvent(event) ? pointerPosition(event, void 0) : void 0; -+ }, -+ [ownsEvent] -+ ); -+ const onPointerUp = useCallback(() => { -+ dragOriginRef.current = void 0; -+ }, []); -+ const onPointerMove = useCallback( -+ (event) => { -+ const origin = dragOriginRef.current; -+ if (!origin) { -+ return; -+ } -+ if (isHover(event)) { -+ dragOriginRef.current = void 0; -+ return; -+ } -+ const point = pointerPosition(event, origin.id); -+ if (!point) { -+ return; -+ } -+ if (Math.abs(point.x - origin.x) > USER_DRAG_SLOP || Math.abs(point.y - origin.y) > USER_DRAG_SLOP) { -+ reportUserInteraction(); -+ } -+ }, -+ [reportUserInteraction] -+ ); -+ const onKeyDown = useCallback( -+ (event) => { -+ if (isScrollKey(event)) { -+ reportUserInteraction(); -+ } -+ }, -+ [reportUserInteraction] + ); const scrollToLocalOffset = useCallback( -- (offset, animated) => { -+ (offset, animated, isCorrection) => { -+ if (!isCorrection) { -+ interactionArmedAtRef.current = now(); -+ } + (offset, animated) => { const scrollElement = scrollRef.current; - const target = getScrollTarget(); - if (!target || typeof target.scrollTo !== "function") { -@@ -6095,14 +6647,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6095,14 +6510,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); options.left = left; options.top = top; @@ -12555,14 +11369,7 @@ index 95465f2..6158381 100644 ); useImperativeHandle(ref, () => { const api = { -@@ -6123,13 +6682,12 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - target.scrollBy({ behavior: "auto", left: x, top: y }); - }, - scrollTo: (options) => { -- const { x = 0, y = 0, animated = true } = options; -- scrollToLocalOffset(horizontal ? x : y, animated); -+ const { x = 0, y = 0, animated = true, isCorrection } = options; -+ scrollToLocalOffset(horizontal ? x : y, animated, isCorrection); +@@ -6128,8 +6550,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -12572,7 +11379,7 @@ index 95465f2..6158381 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6142,7 +6700,12 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6142,7 +6563,12 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ if (!onScroll2 || !scrollRef.current) { return; } @@ -12586,7 +11393,7 @@ index 95465f2..6158381 100644 const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); const offset = getCurrentScrollOffset(); const scrollEvent = { -@@ -6162,7 +6725,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6162,7 +6588,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } }; onScroll2(scrollEvent); @@ -12595,63 +11402,7 @@ index 95465f2..6158381 100644 const scrollEventCoalescer = useRafCoalescer(emitScroll); const scrollEndFallbackRef = useRef(void 0); const emitScrollEnd = useCallback(() => { -@@ -6197,14 +6760,29 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - [ctx.state, emitScrollEnd, onInternalScrollEnd, onScroll2, scrollEventCoalescer] - ); - useLayoutEffect(() => { -+ var _a4; - const target = getScrollTarget(); - if (!target) return; - target.addEventListener("scroll", handleScroll, { passive: true }); -+ const listenerOptions = { capture: true, passive: true }; -+ const removeOptions = { capture: true }; -+ const interactionTarget = (_a4 = scrollRef.current) != null ? _a4 : void 0; -+ const keyTarget = isWindowScroll ? target : interactionTarget; -+ target.addEventListener("wheel", onWheel, listenerOptions); -+ keyTarget == null ? void 0 : keyTarget.addEventListener("keydown", onKeyDown, listenerOptions); -+ for (const [type, handler] of POINTER_EVENT_HANDLERS(onPointerDown, onPointerMove, onPointerUp)) { -+ interactionTarget == null ? void 0 : interactionTarget.addEventListener(type, handler, listenerOptions); -+ } - if ("onscrollend" in target) { - target.addEventListener("scrollend", emitScrollEnd); - } - return () => { - target.removeEventListener("scroll", handleScroll); -+ target.removeEventListener("wheel", onWheel, removeOptions); -+ keyTarget == null ? void 0 : keyTarget.removeEventListener("keydown", onKeyDown, removeOptions); -+ for (const [type, handler] of POINTER_EVENT_HANDLERS(onPointerDown, onPointerMove, onPointerUp)) { -+ interactionTarget == null ? void 0 : interactionTarget.removeEventListener(type, handler, removeOptions); -+ } - if ("onscrollend" in target) { - target.removeEventListener("scrollend", emitScrollEnd); - } -@@ -6215,7 +6793,16 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - } - scrollEventCoalescer.cancel(); - }; -- }, [emitScrollEnd, getScrollTarget, handleScroll, scrollEventCoalescer]); -+ }, [ -+ emitScrollEnd, -+ getScrollTarget, -+ handleScroll, -+ onKeyDown, -+ onPointerDown, -+ onPointerMove, -+ onWheel, -+ scrollEventCoalescer -+ ]); - useEffect(() => { - const doScroll = () => { - if (contentOffset) { -@@ -6320,6 +6907,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - { - className: scrollViewClassName, - ref: scrollRef, -+ ...{ [SCROLLER_MARKER_ATTRIBUTE]: "" }, - ...webProps, - style: scrollViewStyle - }, -@@ -6358,21 +6946,6 @@ function useValueListener$(key, callback) { +@@ -6358,21 +6784,6 @@ function useValueListener$(key, callback) { } // src/components/ScrollAdjust.tsx @@ -12673,7 +11424,7 @@ index 95465f2..6158381 100644 function getScrollAdjustTarget(ctx, contentNode) { var _a3, _b, _c; const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6390,8 +6963,6 @@ function ScrollAdjust() { +@@ -6390,8 +6801,6 @@ function ScrollAdjust() { const ctx = useStateContext(); const lastScrollOffsetRef = React3.useRef(0); const lastScrollAdjustUserOffsetRef = React3.useRef(0); @@ -12682,7 +11433,7 @@ index 95465f2..6158381 100644 const contentNodeRef = React3.useRef(null); const callback = React3.useCallback(() => { const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6402,7 +6973,7 @@ function ScrollAdjust() { +@@ -6402,7 +6811,7 @@ function ScrollAdjust() { const target = getScrollAdjustTarget(ctx, contentNodeRef.current); if (target) { const horizontal = !!ctx.state.props.horizontal; @@ -12691,7 +11442,7 @@ index 95465f2..6158381 100644 const { contentNode, scrollElement: el } = target; const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6412,34 +6983,15 @@ function ScrollAdjust() { +@@ -6412,34 +6821,15 @@ function ScrollAdjust() { const scrollBy = () => scrollAdjustBy(el, axis.x * scrollDelta, axis.y * scrollDelta); contentNodeRef.current = contentNode; if (shouldScroll && contentNode) { @@ -12729,48 +11480,11 @@ index 95465f2..6158381 100644 } else { scrollBy(); } -@@ -6640,7 +7192,12 @@ var ListComponent = typedMemo(function ListComponent2({ - SnapOrScroll, - { - ...rest, -- ...ScrollComponent === ListComponentScrollView ? { onInternalScrollEnd, useWindowScroll } : {} , -+ ...ScrollComponent === ListComponentScrollView ? ( -+ // onUserInteraction is the web counterpart of onScrollBeginDrag: the view -+ // reports that the user moved the list, and LegendList decides what that -+ // means for a scroll in flight. -+ { onInternalScrollEnd, onUserInteraction: onInternalScrollBeginDrag, useWindowScroll } -+ ) : {} , - contentContainerStyle: [ - horizontal ? { height: "100%" } : {}, - contentContainerStyle, -@@ -7624,10 +8181,10 @@ function useThrottleDebounce(mode) { - const execute = useCallback( - (callback, delay, ...args) => { - { -- const now = Date.now(); -+ const now2 = Date.now(); - lastArgsRef.current = args; -- if (now - lastCallTimeRef.current >= delay) { -- lastCallTimeRef.current = now; -+ if (now2 - lastCallTimeRef.current >= delay) { -+ lastCallTimeRef.current = now2; - callback(...args); - clearTimeoutRef(); - } else { -@@ -7641,7 +8198,7 @@ function useThrottleDebounce(mode) { - lastArgsRef.current = null; - } - }, -- delay - (now - lastCallTimeRef.current) -+ delay - (now2 - lastCallTimeRef.current) - ); - } - } -@@ -8267,6 +8824,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8267,6 +8657,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); -+ releaseScrollTargetForUserInteraction(state); ++ clearScrollTargetSettle(state); (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) From 30da1e9e9e78016dc9c187daac3db9388ecc3eae Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 12 Aug 2026 18:56:45 -0400 Subject: [PATCH 24/38] fix(chat): replace the borrowed-room scroll fix with re-issuing the scroll MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reaching a scroll offset the committed content cannot satisfy yet was done by extending the content with temporary end padding, scrolling into the room that bought, and giving it back once the scroll had happened — sharing the bookkeeping with the scroll adjustment that pads the same node, measuring what each borrow actually bought, and holding the room for the length of an animated scroll. Re-issuing the scroll on later frames until the content commits does the same job. Same defect, same coverage, four hundred fewer lines of library source: the padding module, its shared accounting, the animated-scroll release with its scrollend listener and watchdog, and the scroll-adjust changes that existed only to share with it are all gone. Verified the way the rest of this branch was: iPhone suite 61 passing, the desktop search flow green twice over, and the re-issue proven load-bearing by a build with it disabled, where a hit lands at y=1524 in a viewport of y=80..867. --- shared/patches/@legendapp+list+3.3.5.patch | 2882 ++++---------------- 1 file changed, 535 insertions(+), 2347 deletions(-) diff --git a/shared/patches/@legendapp+list+3.3.5.patch b/shared/patches/@legendapp+list+3.3.5.patch index 18131d79b669..2814a7b36e20 100644 --- a/shared/patches/@legendapp+list+3.3.5.patch +++ b/shared/patches/@legendapp+list+3.3.5.patch @@ -3173,7 +3173,7 @@ index 40e87cd..877e5d4 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js -index 914d2da..c235070 100644 +index 914d2da..eed3566 100644 --- a/node_modules/@legendapp/list/react-native.web.js +++ b/node_modules/@legendapp/list/react-native.web.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -3782,18 +3782,16 @@ index 914d2da..c235070 100644 // src/core/getStartOffsetAdjustment.ts function getStartOffsetAdjustment(ctx) { const { state } = ctx; -@@ -1238,54 +639,269 @@ function getItemSizeAtIndex(ctx, index) { - return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); - } - --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; +@@ -1234,58 +635,273 @@ function getItemSizeAtIndex(ctx, index) { + if (index === void 0 || index < 0) { + return void 0; + } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ +// src/core/calculateOffsetWithOffsetPosition.ts +function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { + var _a3; @@ -3997,12 +3995,7 @@ index 914d2da..c235070 100644 + const state = ctx == null ? void 0 : ctx.state; + if (!state) { + return; - } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } ++ } + const { + isStartReached, + props: { data, onStartReachedThreshold }, @@ -4017,22 +4010,7 @@ index 914d2da..c235070 100644 + if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { + state.isStartReached = false; + state.startReachedSnapshot = void 0; - } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } ++ } + set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); + set$(ctx, "isNearStart", scroll <= threshold); + const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; @@ -4059,19 +4037,50 @@ index 914d2da..c235070 100644 + state.startReachedSnapshot = snapshot; + } + ); - } -- return offset; ++ } } --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +- } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } +- } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } +- } +- return offset; +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+} -+ + } + +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { + var _a3, _b; @@ -4093,30 +4102,10 @@ index 914d2da..c235070 100644 } // src/core/finishScrollTo.ts -@@ -1413,16 +1029,191 @@ function listenForScrollEnd(ctx, params) { - if (idleTimeout !== void 0) { - clearTimeout(idleTimeout); - } -- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); -- }; -- const onScrollEnd = () => finish("scrollend"); -- target.addEventListener("scroll", onScroll2); -- if (supportsScrollEnd) { -- target.addEventListener("scrollend", onScrollEnd); -- } else { -- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); -+ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); -+ }; -+ const onScrollEnd = () => finish("scrollend"); -+ target.addEventListener("scroll", onScroll2); -+ if (supportsScrollEnd) { -+ target.addEventListener("scrollend", onScrollEnd); -+ } else { -+ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); -+ } -+ scheduledWork.register("platformScrollCompletion", cancel); -+} -+ +@@ -1425,6 +1041,181 @@ function listenForScrollEnd(ctx, params) { + scheduledWork.register("platformScrollCompletion", cancel); + } + +// src/core/initialScrollSession.ts +var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +function hasInitialScrollSessionCompletion(completion) { @@ -4289,11 +4278,12 @@ index 914d2da..c235070 100644 + } else { + resetAdaptiveRender(ctx); + } - } -- scheduledWork.register("platformScrollCompletion", cancel); - } - ++ } ++} ++ // src/core/doMaintainScrollAtEnd.ts + function doMaintainScrollAtEnd(ctx) { + const state = ctx.state; @@ -1451,6 +1242,7 @@ function doMaintainScrollAtEnd(ctx) { const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; @@ -4330,58 +4320,27 @@ index 914d2da..c235070 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2055,70 +1868,402 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { - if (nextTop === void 0 || nextTop > viewportEnd) { - break; - } -- end++; -+ end++; -+ } -+ return { end, start }; -+} -+function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -+ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -+ if (range) { -+ ctx.state.scrollTargetPinnedRange = range; -+ ctx.state.scrollForNextCalculateItemsInView = void 0; -+ } else { -+ ctx.state.scrollTargetPinnedRange = void 0; -+ } -+} -+function scrollTo(ctx, params) { +@@ -2069,7 +1882,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + } + } + function scrollTo(ctx, params) { +- var _a3, _b; + var _a3, _b, _c; -+ const state = ctx.state; -+ const { noScrollingTo, forceScroll, ...scrollTarget } = params; -+ const { -+ animated, -+ isInitialScroll, -+ offset: scrollTargetOffset, -+ precomputedWithViewOffset, -+ waitForInitialScrollCompletionFrame -+ } = scrollTarget; -+ const { -+ props: { horizontal } -+ } = state; -+ cancelScrollCompletionChecks(state); -+ const requestedOffset = precomputedWithViewOffset ? scrollTargetOffset : calculateOffsetWithOffsetPosition(ctx, scrollTargetOffset, scrollTarget); -+ const shouldPreserveRawInitialOffsetRequest = !!isInitialScroll && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; -+ const targetOffset = clampScrollOffset(ctx, requestedOffset, scrollTarget); -+ const offset = shouldPreserveRawInitialOffsetRequest ? requestedOffset : targetOffset; -+ state.scrollHistory.length = 0; -+ if (!noScrollingTo) { -+ if (isInitialScroll) { -+ initialScrollCompletion.resetFlags(state); + const state = ctx.state; + const { noScrollingTo, forceScroll, ...scrollTarget } = params; + const { +@@ -2091,6 +1904,7 @@ function scrollTo(ctx, params) { + if (!noScrollingTo) { + if (isInitialScroll) { + initialScrollCompletion.resetFlags(state); + clearScrollTargetSettle(state); -+ } -+ const averageSizeSnapshot = getAverageSizeSnapshot(state); -+ state.scrollingTo = { -+ ...scrollTarget, -+ ...averageSizeSnapshot ? { averageSizeSnapshot } : {}, -+ targetOffset, -+ waitForInitialScrollCompletionFrame -+ }; -+ if (!isInitialScroll) { -+ pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); + } + const averageSizeSnapshot = getAverageSizeSnapshot(state); + state.scrollingTo = { +@@ -2101,6 +1915,15 @@ function scrollTo(ctx, params) { + }; + if (!isInitialScroll) { + pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); + if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { + beginScrollTargetSettle(ctx, { + index: scrollTarget.index, @@ -4391,26 +4350,22 @@ index 914d2da..c235070 100644 + } else { + clearScrollTargetSettle(state); + } -+ } -+ } -+ state.scrollPending = targetOffset; -+ syncInitialScrollNativeWatchdog(state, { isInitialScroll, requestedOffset: offset, targetOffset }); -+ if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { -+ if (animated) { -+ if (state.scrollTargetPinnedRange) { + } + } + state.scrollPending = targetOffset; +@@ -2108,7 +1931,7 @@ function scrollTo(ctx, params) { + if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { + if (animated) { + if (state.scrollTargetPinnedRange) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); + (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); -+ } -+ } else { -+ updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -+ } -+ } -+ if (forceScroll || !isInitialScroll || Platform.OS === "android") { -+ doScrollTo(ctx, { animated, horizontal, offset }); -+ } else { -+ state.scroll = offset; -+ } -+} -+ + } + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); +@@ -2121,6 +1944,328 @@ function scrollTo(ctx, params) { + } + } + +// src/core/scrollTargetSettle.ts +var SETTLE_POSITION_EPSILON = 1; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; @@ -4638,17 +4593,8 @@ index 914d2da..c235070 100644 + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); + } + } - } -- return { end, start }; - } --function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -- if (range) { -- ctx.state.scrollTargetPinnedRange = range; -- ctx.state.scrollForNextCalculateItemsInView = void 0; -- } else { -- ctx.state.scrollTargetPinnedRange = void 0; -- } ++ } ++} + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -4656,9 +4602,7 @@ index 914d2da..c235070 100644 + state.scroll = offset; + state.scrollPending = offset; + state.scrollPrev = offset; - } --function scrollTo(ctx, params) { -- var _a3, _b; ++} +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -4670,56 +4614,21 @@ index 914d2da..c235070 100644 +} +function supersedeInitialScroll(ctx) { + var _a3, _b, _c; - const state = ctx.state; -- const { noScrollingTo, forceScroll, ...scrollTarget } = params; -- const { -- animated, -- isInitialScroll, -- offset: scrollTargetOffset, -- precomputedWithViewOffset, -- waitForInitialScrollCompletionFrame -- } = scrollTarget; -- const { -- props: { horizontal } -- } = state; -- cancelScrollCompletionChecks(state); -- const requestedOffset = precomputedWithViewOffset ? scrollTargetOffset : calculateOffsetWithOffsetPosition(ctx, scrollTargetOffset, scrollTarget); -- const shouldPreserveRawInitialOffsetRequest = !!isInitialScroll && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; -- const targetOffset = clampScrollOffset(ctx, requestedOffset, scrollTarget); -- const offset = shouldPreserveRawInitialOffsetRequest ? requestedOffset : targetOffset; -- state.scrollHistory.length = 0; -- if (!noScrollingTo) { -- if (isInitialScroll) { -- initialScrollCompletion.resetFlags(state); ++ const state = ctx.state; + const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; + if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { + if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { + cancelAnimationFrame(bootstrapInitialScroll.frameHandle); - } -- const averageSizeSnapshot = getAverageSizeSnapshot(state); -- state.scrollingTo = { -- ...scrollTarget, -- ...averageSizeSnapshot ? { averageSizeSnapshot } : {}, -- targetOffset, -- waitForInitialScrollCompletionFrame -- }; -- if (!isInitialScroll) { -- pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); ++ } + if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { + cancelScrollCompletionChecks(state); + state.scrollingTo = void 0; + state.scrollTargetPinnedRange = void 0; - } ++ } + initialScrollCompletion.resetFlags(state); + setInitialScrollSession(state, { bootstrap: null }); + finishInitialScroll(ctx); - } -- state.scrollPending = targetOffset; -- syncInitialScrollNativeWatchdog(state, { isInitialScroll, requestedOffset: offset, targetOffset }); -- if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { -- if (animated) { -- if (state.scrollTargetPinnedRange) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++ } +} +function finishInitialScroll(ctx, options) { + var _a3, _b, _c; @@ -4753,11 +4662,10 @@ index 914d2da..c235070 100644 + PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, + "preservedInitialScroll" + ); - } - } else { -- updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); ++ } ++ } else { + clearPreservedInitialScrollTarget(state); - } ++ } + if (options == null ? void 0 : options.recalculateItems) { + recalculateSettledScroll(ctx); + } @@ -4770,12 +4678,7 @@ index 914d2da..c235070 100644 + if (options == null ? void 0 : options.waitForCompletionFrame) { + requestAnimationFrame(complete); + return; - } -- if (forceScroll || !isInitialScroll || Platform.OS === "android") { -- doScrollTo(ctx, { animated, horizontal, offset }); -- } else { -- state.scroll = offset; -- } ++ } + complete(); +} + @@ -4783,9 +4686,11 @@ index 914d2da..c235070 100644 +function calculateOffsetForIndex(ctx, index) { + const state = ctx.state; + return index !== void 0 ? state.positions[index] || 0 : 0; - } - ++} ++ // src/core/scrollToIndex.ts + function clampScrollIndex(index, dataLength) { + if (dataLength <= 0) { @@ -4350,6 +4495,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); @@ -4815,297 +4720,40 @@ index 914d2da..c235070 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5927,6 +6083,21 @@ function getDocumentScrollerNode() { - } - return document.scrollingElement || document.documentElement || document.body; - } -+function getScrollAxis(horizontal) { -+ return horizontal ? { -+ contentSizeKey: "scrollWidth", -+ paddingEndProp: "paddingRight", -+ viewportSizeKey: "clientWidth", -+ x: 1, -+ y: 0 -+ } : { -+ contentSizeKey: "scrollHeight", -+ paddingEndProp: "paddingBottom", -+ viewportSizeKey: "clientHeight", -+ x: 0, -+ y: 1 -+ }; -+} - function getWindowScrollPosition() { - var _a3, _b, _c, _d; - if (typeof window === "undefined") { -@@ -6003,9 +6174,155 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll - }; - } - -+// src/components/webTemporaryEndPadding.ts -+var entriesByNode = /* @__PURE__ */ new WeakMap(); -+var nextRequestId = 1; -+var RELEASE_ALL_MAX_PASSES = 5; -+function readResolvedPadding(node, prop) { -+ return Number.parseFloat(window.getComputedStyle(node)[prop]) || 0; -+} -+function totalRequested(entry) { -+ let total = 0; -+ for (const size of entry.requests.values()) { -+ total += size; -+ } -+ return total; -+} -+function isOwnedByUs(node, prop, entry) { -+ return entry.lastApplied === "" || node.style[prop] === entry.lastApplied; -+} -+function measureNodeExtent(node, prop) { -+ return prop === "paddingBottom" ? node.scrollHeight : node.scrollWidth; -+} -+function applyPadding(node, prop, entry) { -+ const total = totalRequested(entry); -+ const before = measureNodeExtent(node, prop); -+ node.style[prop] = total === 0 ? entry.baseline : `${entry.baselineSize + total}px`; -+ entry.boughtTotal = Math.max(0, entry.boughtTotal + (measureNodeExtent(node, prop) - before)); -+ entry.lastApplied = node.style[prop]; -+} -+function drainPendingReleases(entry) { -+ if (entry.resetHandle !== void 0) { -+ cancelAnimationFrame(entry.resetHandle); -+ entry.resetHandle = void 0; -+ } -+ if (entry.pendingReleases.size === 0) { -+ return; -+ } -+ const releases = [...entry.pendingReleases]; -+ entry.pendingReleases.clear(); -+ for (const pending of releases) { -+ pending(); -+ } -+} -+function releaseEntry(node, prop, requestId) { -+ const entries = entriesByNode.get(node); -+ const entry = entries == null ? void 0 : entries[prop]; -+ if (!entry || !entry.requests.delete(requestId)) { -+ return; -+ } -+ if (isOwnedByUs(node, prop, entry)) { -+ applyPadding(node, prop, entry); -+ } -+ if (entry.requests.size === 0) { -+ entries == null ? true : delete entries[prop]; -+ drainPendingReleases(entry); -+ } -+} -+function addTemporaryEndPadding(node, prop, extraSize) { -+ var _a3; -+ const entries = (_a3 = entriesByNode.get(node)) != null ? _a3 : {}; -+ let entry = entries[prop]; -+ if (entry && !isOwnedByUs(node, prop, entry)) { -+ entry.baseline = node.style[prop]; -+ entry.baselineSize = readResolvedPadding(node, prop); -+ entry.boughtTotal = 0; -+ } -+ if (!entry) { -+ entry = { -+ baseline: node.style[prop], -+ baselineSize: readResolvedPadding(node, prop), -+ boughtTotal: 0, -+ lastApplied: "", -+ pendingReleases: /* @__PURE__ */ new Set(), -+ requests: /* @__PURE__ */ new Map(), -+ resetHandle: void 0 -+ }; -+ entries[prop] = entry; -+ entriesByNode.set(node, entries); -+ } -+ const requestId = nextRequestId++; -+ entry.requests.set(requestId, extraSize); -+ applyPadding(node, prop, entry); -+ void node.offsetHeight; -+ return function releaseTemporaryEndPadding() { -+ releaseEntry(node, prop, requestId); -+ }; -+} -+function scheduleTemporaryEndPaddingRelease(node, prop, release) { -+ var _a3; -+ const entry = (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop]; -+ if (!entry) { -+ release(); -+ return; -+ } -+ entry.pendingReleases.add(release); -+ if (entry.resetHandle !== void 0) { -+ return; -+ } -+ entry.resetHandle = requestAnimationFrame(() => { -+ entry.resetHandle = void 0; -+ const releases = [...entry.pendingReleases]; -+ entry.pendingReleases.clear(); -+ for (const pending of releases) { -+ pending(); -+ } -+ }); -+} -+function getTemporaryEndPadding(node, prop) { -+ var _a3; -+ if (!node) { -+ return 0; -+ } -+ const entry = (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop]; -+ if (!entry || !isOwnedByUs(node, prop, entry)) { -+ return 0; -+ } -+ return entry.boughtTotal; -+} -+function releaseAllTemporaryEndPadding(node) { -+ const entries = entriesByNode.get(node); -+ if (!entries) { -+ return; -+ } -+ for (let pass = 0; pass < RELEASE_ALL_MAX_PASSES; pass++) { -+ const props = Object.keys(entries); -+ if (props.length === 0) { -+ break; -+ } -+ for (const prop of props) { -+ const entry = entries[prop]; -+ if (!entry) { -+ continue; -+ } -+ if (isOwnedByUs(node, prop, entry)) { -+ node.style[prop] = entry.baseline; -+ } -+ delete entries[prop]; -+ entry.requests.clear(); -+ drainPendingReleases(entry); -+ } -+ } -+ entriesByNode.delete(node); -+} -+ +@@ -6006,6 +6162,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; -+var SMOOTH_SCROLL_MAX_MS = 2e3; -+var BORROW_WATCH_FRAME_INTERVAL = 3; -+var BORROW_MEASURE_ATTEMPTS = 2; ++var REACHABLE_RETRY_FRAMES = 30; var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6074,6 +6391,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), - [isWindowScroll] - ); -+ const paddingEndProp = getScrollAxis(horizontal).paddingEndProp; -+ const getCommittedMaxScrollOffset = React3.useCallback( -+ (maxOffset) => Math.max(0, maxOffset - getTemporaryEndPadding(contentRef.current, paddingEndProp)), -+ [paddingEndProp] -+ ); - const getMaxScrollOffset = React3.useCallback(() => { - const scrollElement = scrollRef.current; - const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6094,6 +6416,99 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,6 +6252,23 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); -+ const paddedNodeRef = React3.useRef(null); -+ const borrowWatchRef = React3.useRef(0); -+ const animatedPaddingReleaseRef = React3.useRef(void 0); -+ const withReachableExtent = React3.useCallback( -+ (offset, maxOffset, animated, run) => { -+ var _a4; -+ const contentNode = contentRef.current; -+ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); -+ const committedMaxOffset = getCommittedMaxScrollOffset(getMaxScrollOffset()); -+ if (!contentNode || !Number.isFinite(offset)) { -+ run(clampOffset(offset, maxOffset)); -+ return; -+ } -+ if (offset <= committedMaxOffset + SCROLL_EXTENT_EPSILON) { -+ run(clampOffset(offset, maxOffset)); -+ return; -+ } -+ const releases = []; -+ for (let attempt = 0; attempt < BORROW_MEASURE_ATTEMPTS; attempt++) { -+ const shortfall = offset - getMaxScrollOffset(); -+ if (shortfall <= 0) { -+ break; -+ } -+ releases.push(addTemporaryEndPadding(contentNode, paddingEndProp, shortfall + SCROLL_EXTENT_EPSILON)); -+ } -+ if (releases.length === 0) { -+ run(offset); -+ return; -+ } -+ const release = () => { -+ for (const releaseOne of releases) { -+ releaseOne(); -+ } -+ }; -+ paddedNodeRef.current = contentNode; -+ run(offset); -+ if (!animated) { -+ scheduleTemporaryEndPaddingRelease(contentNode, paddingEndProp, release); -+ return; -+ } -+ const scrollTarget = getScrollTarget(); -+ const supportsScrollEnd = !!scrollTarget && "onscrollend" in scrollTarget; -+ const timers = {}; -+ const finish = () => { -+ if (animatedPaddingReleaseRef.current !== finish) { -+ return; ++ const reissueHandleRef = React3.useRef(0); ++ const scrollUntilReachable = React3.useCallback( ++ (offset, animated, run) => { ++ cancelAnimationFrame(reissueHandleRef.current); ++ let attempts = 0; ++ const attempt = () => { ++ const liveMaxOffset = getMaxScrollOffset(); ++ run(clampOffset(offset, liveMaxOffset)); ++ if (!animated && Number.isFinite(offset) && offset - liveMaxOffset > SCROLL_EXTENT_EPSILON && ++attempts < REACHABLE_RETRY_FRAMES) { ++ reissueHandleRef.current = requestAnimationFrame(attempt); + } -+ animatedPaddingReleaseRef.current = void 0; -+ clearTimeout(timers.settle); -+ scrollTarget == null ? void 0 : scrollTarget.removeEventListener("scrollend", finishIfArrived); -+ cancelAnimationFrame(borrowWatchRef.current); -+ release(); + }; -+ const finishIfArrived = () => { -+ if (Math.abs(getCurrentScrollOffset() - offset) <= SCROLL_EXTENT_EPSILON) { -+ finish(); -+ } -+ }; -+ let framesSeen = 0; -+ const releaseWhenContentCommits = () => { -+ if (animatedPaddingReleaseRef.current !== finish) { -+ return; -+ } -+ if (++framesSeen % BORROW_WATCH_FRAME_INTERVAL === 0) { -+ if (getCommittedMaxScrollOffset(getMaxScrollOffset()) + SCROLL_EXTENT_EPSILON >= offset) { -+ finish(); -+ return; -+ } -+ } -+ borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); -+ }; -+ animatedPaddingReleaseRef.current = finish; -+ cancelAnimationFrame(borrowWatchRef.current); -+ borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); -+ timers.settle = setTimeout(finish, SMOOTH_SCROLL_MAX_MS); -+ if (supportsScrollEnd) { -+ scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finishIfArrived); -+ } -+ }, -+ [getCommittedMaxScrollOffset, getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, paddingEndProp] -+ ); -+ React3.useEffect( -+ () => () => { -+ var _a4; -+ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); -+ cancelAnimationFrame(borrowWatchRef.current); -+ const paddedNode = paddedNodeRef.current; -+ if (paddedNode) { -+ releaseAllTemporaryEndPadding(paddedNode); -+ } ++ attempt(); + }, -+ [] ++ [getMaxScrollOffset] + ); ++ React3.useEffect(() => () => cancelAnimationFrame(reissueHandleRef.current), []); const scrollToLocalOffset = React3.useCallback( (offset, animated) => { const scrollElement = scrollRef.current; -@@ -6116,14 +6531,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6116,14 +6291,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); options.left = left; options.top = top; @@ -5116,7 +4764,7 @@ index 914d2da..c235070 100644 + target.scrollTo(options); } else { - options.top = clampedOffset; -+ withReachableExtent(offset, maxOffset, animated, (reachableOffset) => { ++ scrollUntilReachable(offset, animated, (reachableOffset) => { + if (horizontal) { + options.left = reachableOffset; + } else { @@ -5128,122 +4776,21 @@ index 914d2da..c235070 100644 - target.scrollTo(options); }, - [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] -+ [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, withReachableExtent] ++ [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable] ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6571,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6149,8 +6331,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; - const endOffset = getMaxScrollOffset(); - scrollToLocalOffset(endOffset, animated); -+ scrollToLocalOffset(getCommittedMaxScrollOffset(getMaxScrollOffset()), animated); ++ scrollToLocalOffset(getMaxScrollOffset(), animated); }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6163,7 +6584,12 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - if (!onScroll2 || !scrollRef.current) { - return; - } -- const contentSize = getContentSize2(contentRef.current); -+ const rawContentSize = getContentSize2(contentRef.current); -+ const temporaryPadding = getTemporaryEndPadding(contentRef.current, paddingEndProp); -+ const contentSize = temporaryPadding ? { -+ height: horizontal ? rawContentSize.height : rawContentSize.height - temporaryPadding, -+ width: horizontal ? rawContentSize.width - temporaryPadding : rawContentSize.width -+ } : rawContentSize; - const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); - const offset = getCurrentScrollOffset(); - const scrollEvent = { -@@ -6183,7 +6609,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - } - }; - onScroll2(scrollEvent); -- }, [getCurrentScrollOffset, horizontal, isWindowScroll, onScroll2]); -+ }, [getCurrentScrollOffset, horizontal, isWindowScroll, onScroll2, paddingEndProp]); - const scrollEventCoalescer = useRafCoalescer(emitScroll); - const scrollEndFallbackRef = React3.useRef(void 0); - const emitScrollEnd = React3.useCallback(() => { -@@ -6379,21 +6805,6 @@ function useValueListener$(key, callback) { - } - - // src/components/ScrollAdjust.tsx --function getScrollAdjustAxis(horizontal) { -- return horizontal ? { -- contentSizeKey: "scrollWidth", -- paddingEndProp: "paddingRight", -- viewportSizeKey: "clientWidth", -- x: 1, -- y: 0 -- } : { -- contentSizeKey: "scrollHeight", -- paddingEndProp: "paddingBottom", -- viewportSizeKey: "clientHeight", -- x: 0, -- y: 1 -- }; --} - function getScrollAdjustTarget(ctx, contentNode) { - var _a3, _b, _c; - const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6411,8 +6822,6 @@ function ScrollAdjust() { - const ctx = useStateContext(); - const lastScrollOffsetRef = React3__namespace.useRef(0); - const lastScrollAdjustUserOffsetRef = React3__namespace.useRef(0); -- const resetPaddingRafRef = React3__namespace.useRef(void 0); -- const temporaryPaddingRef = React3__namespace.useRef(void 0); - const contentNodeRef = React3__namespace.useRef(null); - const callback = React3__namespace.useCallback(() => { - const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6423,7 +6832,7 @@ function ScrollAdjust() { - const target = getScrollAdjustTarget(ctx, contentNodeRef.current); - if (target) { - const horizontal = !!ctx.state.props.horizontal; -- const axis = getScrollAdjustAxis(horizontal); -+ const axis = getScrollAxis(horizontal); - const { contentNode, scrollElement: el } = target; - const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; - const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6433,34 +6842,15 @@ function ScrollAdjust() { - const scrollBy = () => scrollAdjustBy(el, axis.x * scrollDelta, axis.y * scrollDelta); - contentNodeRef.current = contentNode; - if (shouldScroll && contentNode) { -- const totalSize = contentNode[axis.contentSizeKey]; -+ const totalSize = contentNode[axis.contentSizeKey] - getTemporaryEndPadding(contentNode, axis.paddingEndProp); - const viewportSize = el[axis.viewportSizeKey]; - const nextScroll = currentScroll + scrollDelta; - const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; - if (needsTemporaryPadding) { -- const currentInlinePaddingEnd = contentNode.style[axis.paddingEndProp]; -- const previousTemporaryPadding = temporaryPaddingRef.current; -- const baselinePaddingEnd = (previousTemporaryPadding == null ? void 0 : previousTemporaryPadding.value) === currentInlinePaddingEnd ? previousTemporaryPadding.baseline : currentInlinePaddingEnd; - const pad = (nextScroll + viewportSize - totalSize) * 2; -- const currentPaddingEnd = Number.parseFloat( -- window.getComputedStyle(contentNode)[axis.paddingEndProp] -- ); -- const temporaryPaddingEnd = `${(currentPaddingEnd || 0) + pad}px`; -- temporaryPaddingRef.current = { baseline: baselinePaddingEnd, value: temporaryPaddingEnd }; -- contentNode.style[axis.paddingEndProp] = temporaryPaddingEnd; -- void contentNode.offsetHeight; -+ const release = addTemporaryEndPadding(contentNode, axis.paddingEndProp, pad); - scrollBy(); -- if (resetPaddingRafRef.current !== void 0) { -- cancelAnimationFrame(resetPaddingRafRef.current); -- } -- resetPaddingRafRef.current = requestAnimationFrame(() => { -- const temporaryPadding = temporaryPaddingRef.current; -- resetPaddingRafRef.current = void 0; -- temporaryPaddingRef.current = void 0; -- if (contentNode.style[axis.paddingEndProp] === (temporaryPadding == null ? void 0 : temporaryPadding.value)) { -- contentNode.style[axis.paddingEndProp] = temporaryPadding.baseline; -- } -- }); -+ scheduleTemporaryEndPaddingRelease(contentNode, axis.paddingEndProp, release); - } else { - scrollBy(); - } -@@ -8288,6 +8678,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8288,6 +8469,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -5252,7 +4799,7 @@ index 914d2da..c235070 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs -index 95465f2..e8a3fb5 100644 +index 95465f2..31c9d25 100644 --- a/node_modules/@legendapp/list/react-native.web.mjs +++ b/node_modules/@legendapp/list/react-native.web.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -5861,18 +5408,16 @@ index 95465f2..e8a3fb5 100644 // src/core/getStartOffsetAdjustment.ts function getStartOffsetAdjustment(ctx) { const { state } = ctx; -@@ -1217,54 +618,269 @@ function getItemSizeAtIndex(ctx, index) { - return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); - } - --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; +@@ -1213,58 +614,273 @@ function getItemSizeAtIndex(ctx, index) { + if (index === void 0 || index < 0) { + return void 0; + } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ +// src/core/calculateOffsetWithOffsetPosition.ts +function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { + var _a3; @@ -6076,12 +5621,7 @@ index 95465f2..e8a3fb5 100644 + const state = ctx == null ? void 0 : ctx.state; + if (!state) { + return; - } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } ++ } + const { + isStartReached, + props: { data, onStartReachedThreshold }, @@ -6090,28 +5630,13 @@ index 95465f2..e8a3fb5 100644 + startReachedSnapshot, + totalSize + } = state; -+ const dataLength = data.length; -+ const threshold = onStartReachedThreshold * scrollLength; -+ resetSharedEdgeGateIfOutsideHysteresis(ctx); -+ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { -+ state.isStartReached = false; -+ state.startReachedSnapshot = void 0; - } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } ++ const dataLength = data.length; ++ const threshold = onStartReachedThreshold * scrollLength; ++ resetSharedEdgeGateIfOutsideHysteresis(ctx); ++ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { ++ state.isStartReached = false; ++ state.startReachedSnapshot = void 0; ++ } + set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); + set$(ctx, "isNearStart", scroll <= threshold); + const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; @@ -6138,19 +5663,50 @@ index 95465f2..e8a3fb5 100644 + state.startReachedSnapshot = snapshot; + } + ); - } -- return offset; ++ } } --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +- } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } +- } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } +- } +- return offset; +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+} -+ + } + +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { + var _a3, _b; @@ -6172,30 +5728,10 @@ index 95465f2..e8a3fb5 100644 } // src/core/finishScrollTo.ts -@@ -1392,16 +1008,191 @@ function listenForScrollEnd(ctx, params) { - if (idleTimeout !== void 0) { - clearTimeout(idleTimeout); - } -- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); -- }; -- const onScrollEnd = () => finish("scrollend"); -- target.addEventListener("scroll", onScroll2); -- if (supportsScrollEnd) { -- target.addEventListener("scrollend", onScrollEnd); -- } else { -- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); -+ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); -+ }; -+ const onScrollEnd = () => finish("scrollend"); -+ target.addEventListener("scroll", onScroll2); -+ if (supportsScrollEnd) { -+ target.addEventListener("scrollend", onScrollEnd); -+ } else { -+ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); -+ } -+ scheduledWork.register("platformScrollCompletion", cancel); -+} -+ +@@ -1404,6 +1020,181 @@ function listenForScrollEnd(ctx, params) { + scheduledWork.register("platformScrollCompletion", cancel); + } + +// src/core/initialScrollSession.ts +var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +function hasInitialScrollSessionCompletion(completion) { @@ -6368,11 +5904,12 @@ index 95465f2..e8a3fb5 100644 + } else { + resetAdaptiveRender(ctx); + } - } -- scheduledWork.register("platformScrollCompletion", cancel); - } - ++ } ++} ++ // src/core/doMaintainScrollAtEnd.ts + function doMaintainScrollAtEnd(ctx) { + const state = ctx.state; @@ -1430,6 +1221,7 @@ function doMaintainScrollAtEnd(ctx) { const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; @@ -6409,58 +5946,27 @@ index 95465f2..e8a3fb5 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2034,70 +1847,402 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { - if (nextTop === void 0 || nextTop > viewportEnd) { - break; - } -- end++; -+ end++; -+ } -+ return { end, start }; -+} -+function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -+ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -+ if (range) { -+ ctx.state.scrollTargetPinnedRange = range; -+ ctx.state.scrollForNextCalculateItemsInView = void 0; -+ } else { -+ ctx.state.scrollTargetPinnedRange = void 0; -+ } -+} -+function scrollTo(ctx, params) { +@@ -2048,7 +1861,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + } + } + function scrollTo(ctx, params) { +- var _a3, _b; + var _a3, _b, _c; -+ const state = ctx.state; -+ const { noScrollingTo, forceScroll, ...scrollTarget } = params; -+ const { -+ animated, -+ isInitialScroll, -+ offset: scrollTargetOffset, -+ precomputedWithViewOffset, -+ waitForInitialScrollCompletionFrame -+ } = scrollTarget; -+ const { -+ props: { horizontal } -+ } = state; -+ cancelScrollCompletionChecks(state); -+ const requestedOffset = precomputedWithViewOffset ? scrollTargetOffset : calculateOffsetWithOffsetPosition(ctx, scrollTargetOffset, scrollTarget); -+ const shouldPreserveRawInitialOffsetRequest = !!isInitialScroll && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; -+ const targetOffset = clampScrollOffset(ctx, requestedOffset, scrollTarget); -+ const offset = shouldPreserveRawInitialOffsetRequest ? requestedOffset : targetOffset; -+ state.scrollHistory.length = 0; -+ if (!noScrollingTo) { -+ if (isInitialScroll) { -+ initialScrollCompletion.resetFlags(state); + const state = ctx.state; + const { noScrollingTo, forceScroll, ...scrollTarget } = params; + const { +@@ -2070,6 +1883,7 @@ function scrollTo(ctx, params) { + if (!noScrollingTo) { + if (isInitialScroll) { + initialScrollCompletion.resetFlags(state); + clearScrollTargetSettle(state); -+ } -+ const averageSizeSnapshot = getAverageSizeSnapshot(state); -+ state.scrollingTo = { -+ ...scrollTarget, -+ ...averageSizeSnapshot ? { averageSizeSnapshot } : {}, -+ targetOffset, -+ waitForInitialScrollCompletionFrame -+ }; -+ if (!isInitialScroll) { -+ pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); + } + const averageSizeSnapshot = getAverageSizeSnapshot(state); + state.scrollingTo = { +@@ -2080,6 +1894,15 @@ function scrollTo(ctx, params) { + }; + if (!isInitialScroll) { + pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); + if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { + beginScrollTargetSettle(ctx, { + index: scrollTarget.index, @@ -6470,26 +5976,22 @@ index 95465f2..e8a3fb5 100644 + } else { + clearScrollTargetSettle(state); + } -+ } -+ } -+ state.scrollPending = targetOffset; -+ syncInitialScrollNativeWatchdog(state, { isInitialScroll, requestedOffset: offset, targetOffset }); -+ if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { -+ if (animated) { -+ if (state.scrollTargetPinnedRange) { + } + } + state.scrollPending = targetOffset; +@@ -2087,7 +1910,7 @@ function scrollTo(ctx, params) { + if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { + if (animated) { + if (state.scrollTargetPinnedRange) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); + (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); -+ } -+ } else { -+ updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -+ } -+ } -+ if (forceScroll || !isInitialScroll || Platform.OS === "android") { -+ doScrollTo(ctx, { animated, horizontal, offset }); -+ } else { -+ state.scroll = offset; -+ } -+} -+ + } + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); +@@ -2100,6 +1923,328 @@ function scrollTo(ctx, params) { + } + } + +// src/core/scrollTargetSettle.ts +var SETTLE_POSITION_EPSILON = 1; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; @@ -6717,17 +6219,8 @@ index 95465f2..e8a3fb5 100644 + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); + } + } - } -- return { end, start }; - } --function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -- if (range) { -- ctx.state.scrollTargetPinnedRange = range; -- ctx.state.scrollForNextCalculateItemsInView = void 0; -- } else { -- ctx.state.scrollTargetPinnedRange = void 0; -- } ++ } ++} + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -6735,9 +6228,7 @@ index 95465f2..e8a3fb5 100644 + state.scroll = offset; + state.scrollPending = offset; + state.scrollPrev = offset; - } --function scrollTo(ctx, params) { -- var _a3, _b; ++} +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -6749,56 +6240,21 @@ index 95465f2..e8a3fb5 100644 +} +function supersedeInitialScroll(ctx) { + var _a3, _b, _c; - const state = ctx.state; -- const { noScrollingTo, forceScroll, ...scrollTarget } = params; -- const { -- animated, -- isInitialScroll, -- offset: scrollTargetOffset, -- precomputedWithViewOffset, -- waitForInitialScrollCompletionFrame -- } = scrollTarget; -- const { -- props: { horizontal } -- } = state; -- cancelScrollCompletionChecks(state); -- const requestedOffset = precomputedWithViewOffset ? scrollTargetOffset : calculateOffsetWithOffsetPosition(ctx, scrollTargetOffset, scrollTarget); -- const shouldPreserveRawInitialOffsetRequest = !!isInitialScroll && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; -- const targetOffset = clampScrollOffset(ctx, requestedOffset, scrollTarget); -- const offset = shouldPreserveRawInitialOffsetRequest ? requestedOffset : targetOffset; -- state.scrollHistory.length = 0; -- if (!noScrollingTo) { -- if (isInitialScroll) { -- initialScrollCompletion.resetFlags(state); ++ const state = ctx.state; + const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; + if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { + if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { + cancelAnimationFrame(bootstrapInitialScroll.frameHandle); - } -- const averageSizeSnapshot = getAverageSizeSnapshot(state); -- state.scrollingTo = { -- ...scrollTarget, -- ...averageSizeSnapshot ? { averageSizeSnapshot } : {}, -- targetOffset, -- waitForInitialScrollCompletionFrame -- }; -- if (!isInitialScroll) { -- pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); ++ } + if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { + cancelScrollCompletionChecks(state); + state.scrollingTo = void 0; + state.scrollTargetPinnedRange = void 0; - } ++ } + initialScrollCompletion.resetFlags(state); + setInitialScrollSession(state, { bootstrap: null }); + finishInitialScroll(ctx); - } -- state.scrollPending = targetOffset; -- syncInitialScrollNativeWatchdog(state, { isInitialScroll, requestedOffset: offset, targetOffset }); -- if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { -- if (animated) { -- if (state.scrollTargetPinnedRange) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++ } +} +function finishInitialScroll(ctx, options) { + var _a3, _b, _c; @@ -6832,11 +6288,10 @@ index 95465f2..e8a3fb5 100644 + PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, + "preservedInitialScroll" + ); - } - } else { -- updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); ++ } ++ } else { + clearPreservedInitialScrollTarget(state); - } ++ } + if (options == null ? void 0 : options.recalculateItems) { + recalculateSettledScroll(ctx); + } @@ -6849,12 +6304,7 @@ index 95465f2..e8a3fb5 100644 + if (options == null ? void 0 : options.waitForCompletionFrame) { + requestAnimationFrame(complete); + return; - } -- if (forceScroll || !isInitialScroll || Platform.OS === "android") { -- doScrollTo(ctx, { animated, horizontal, offset }); -- } else { -- state.scroll = offset; -- } ++ } + complete(); +} + @@ -6862,9 +6312,11 @@ index 95465f2..e8a3fb5 100644 +function calculateOffsetForIndex(ctx, index) { + const state = ctx.state; + return index !== void 0 ? state.positions[index] || 0 : 0; - } - ++} ++ // src/core/scrollToIndex.ts + function clampScrollIndex(index, dataLength) { + if (dataLength <= 0) { @@ -4329,6 +4474,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); @@ -6894,297 +6346,40 @@ index 95465f2..e8a3fb5 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5906,6 +6062,21 @@ function getDocumentScrollerNode() { - } - return document.scrollingElement || document.documentElement || document.body; - } -+function getScrollAxis(horizontal) { -+ return horizontal ? { -+ contentSizeKey: "scrollWidth", -+ paddingEndProp: "paddingRight", -+ viewportSizeKey: "clientWidth", -+ x: 1, -+ y: 0 -+ } : { -+ contentSizeKey: "scrollHeight", -+ paddingEndProp: "paddingBottom", -+ viewportSizeKey: "clientHeight", -+ x: 0, -+ y: 1 -+ }; -+} - function getWindowScrollPosition() { - var _a3, _b, _c, _d; - if (typeof window === "undefined") { -@@ -5982,9 +6153,155 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll - }; - } - -+// src/components/webTemporaryEndPadding.ts -+var entriesByNode = /* @__PURE__ */ new WeakMap(); -+var nextRequestId = 1; -+var RELEASE_ALL_MAX_PASSES = 5; -+function readResolvedPadding(node, prop) { -+ return Number.parseFloat(window.getComputedStyle(node)[prop]) || 0; -+} -+function totalRequested(entry) { -+ let total = 0; -+ for (const size of entry.requests.values()) { -+ total += size; -+ } -+ return total; -+} -+function isOwnedByUs(node, prop, entry) { -+ return entry.lastApplied === "" || node.style[prop] === entry.lastApplied; -+} -+function measureNodeExtent(node, prop) { -+ return prop === "paddingBottom" ? node.scrollHeight : node.scrollWidth; -+} -+function applyPadding(node, prop, entry) { -+ const total = totalRequested(entry); -+ const before = measureNodeExtent(node, prop); -+ node.style[prop] = total === 0 ? entry.baseline : `${entry.baselineSize + total}px`; -+ entry.boughtTotal = Math.max(0, entry.boughtTotal + (measureNodeExtent(node, prop) - before)); -+ entry.lastApplied = node.style[prop]; -+} -+function drainPendingReleases(entry) { -+ if (entry.resetHandle !== void 0) { -+ cancelAnimationFrame(entry.resetHandle); -+ entry.resetHandle = void 0; -+ } -+ if (entry.pendingReleases.size === 0) { -+ return; -+ } -+ const releases = [...entry.pendingReleases]; -+ entry.pendingReleases.clear(); -+ for (const pending of releases) { -+ pending(); -+ } -+} -+function releaseEntry(node, prop, requestId) { -+ const entries = entriesByNode.get(node); -+ const entry = entries == null ? void 0 : entries[prop]; -+ if (!entry || !entry.requests.delete(requestId)) { -+ return; -+ } -+ if (isOwnedByUs(node, prop, entry)) { -+ applyPadding(node, prop, entry); -+ } -+ if (entry.requests.size === 0) { -+ entries == null ? true : delete entries[prop]; -+ drainPendingReleases(entry); -+ } -+} -+function addTemporaryEndPadding(node, prop, extraSize) { -+ var _a3; -+ const entries = (_a3 = entriesByNode.get(node)) != null ? _a3 : {}; -+ let entry = entries[prop]; -+ if (entry && !isOwnedByUs(node, prop, entry)) { -+ entry.baseline = node.style[prop]; -+ entry.baselineSize = readResolvedPadding(node, prop); -+ entry.boughtTotal = 0; -+ } -+ if (!entry) { -+ entry = { -+ baseline: node.style[prop], -+ baselineSize: readResolvedPadding(node, prop), -+ boughtTotal: 0, -+ lastApplied: "", -+ pendingReleases: /* @__PURE__ */ new Set(), -+ requests: /* @__PURE__ */ new Map(), -+ resetHandle: void 0 -+ }; -+ entries[prop] = entry; -+ entriesByNode.set(node, entries); -+ } -+ const requestId = nextRequestId++; -+ entry.requests.set(requestId, extraSize); -+ applyPadding(node, prop, entry); -+ void node.offsetHeight; -+ return function releaseTemporaryEndPadding() { -+ releaseEntry(node, prop, requestId); -+ }; -+} -+function scheduleTemporaryEndPaddingRelease(node, prop, release) { -+ var _a3; -+ const entry = (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop]; -+ if (!entry) { -+ release(); -+ return; -+ } -+ entry.pendingReleases.add(release); -+ if (entry.resetHandle !== void 0) { -+ return; -+ } -+ entry.resetHandle = requestAnimationFrame(() => { -+ entry.resetHandle = void 0; -+ const releases = [...entry.pendingReleases]; -+ entry.pendingReleases.clear(); -+ for (const pending of releases) { -+ pending(); -+ } -+ }); -+} -+function getTemporaryEndPadding(node, prop) { -+ var _a3; -+ if (!node) { -+ return 0; -+ } -+ const entry = (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop]; -+ if (!entry || !isOwnedByUs(node, prop, entry)) { -+ return 0; -+ } -+ return entry.boughtTotal; -+} -+function releaseAllTemporaryEndPadding(node) { -+ const entries = entriesByNode.get(node); -+ if (!entries) { -+ return; -+ } -+ for (let pass = 0; pass < RELEASE_ALL_MAX_PASSES; pass++) { -+ const props = Object.keys(entries); -+ if (props.length === 0) { -+ break; -+ } -+ for (const prop of props) { -+ const entry = entries[prop]; -+ if (!entry) { -+ continue; -+ } -+ if (isOwnedByUs(node, prop, entry)) { -+ node.style[prop] = entry.baseline; -+ } -+ delete entries[prop]; -+ entry.requests.clear(); -+ drainPendingReleases(entry); -+ } -+ } -+ entriesByNode.delete(node); -+} -+ +@@ -5985,6 +6141,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; -+var SMOOTH_SCROLL_MAX_MS = 2e3; -+var BORROW_WATCH_FRAME_INTERVAL = 3; -+var BORROW_MEASURE_ATTEMPTS = 2; ++var REACHABLE_RETRY_FRAMES = 30; var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6053,6 +6370,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), - [isWindowScroll] - ); -+ const paddingEndProp = getScrollAxis(horizontal).paddingEndProp; -+ const getCommittedMaxScrollOffset = useCallback( -+ (maxOffset) => Math.max(0, maxOffset - getTemporaryEndPadding(contentRef.current, paddingEndProp)), -+ [paddingEndProp] -+ ); - const getMaxScrollOffset = useCallback(() => { - const scrollElement = scrollRef.current; - const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6073,6 +6395,99 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,6 +6231,23 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); -+ const paddedNodeRef = useRef(null); -+ const borrowWatchRef = useRef(0); -+ const animatedPaddingReleaseRef = useRef(void 0); -+ const withReachableExtent = useCallback( -+ (offset, maxOffset, animated, run) => { -+ var _a4; -+ const contentNode = contentRef.current; -+ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); -+ const committedMaxOffset = getCommittedMaxScrollOffset(getMaxScrollOffset()); -+ if (!contentNode || !Number.isFinite(offset)) { -+ run(clampOffset(offset, maxOffset)); -+ return; -+ } -+ if (offset <= committedMaxOffset + SCROLL_EXTENT_EPSILON) { -+ run(clampOffset(offset, maxOffset)); -+ return; -+ } -+ const releases = []; -+ for (let attempt = 0; attempt < BORROW_MEASURE_ATTEMPTS; attempt++) { -+ const shortfall = offset - getMaxScrollOffset(); -+ if (shortfall <= 0) { -+ break; -+ } -+ releases.push(addTemporaryEndPadding(contentNode, paddingEndProp, shortfall + SCROLL_EXTENT_EPSILON)); -+ } -+ if (releases.length === 0) { -+ run(offset); -+ return; -+ } -+ const release = () => { -+ for (const releaseOne of releases) { -+ releaseOne(); -+ } -+ }; -+ paddedNodeRef.current = contentNode; -+ run(offset); -+ if (!animated) { -+ scheduleTemporaryEndPaddingRelease(contentNode, paddingEndProp, release); -+ return; -+ } -+ const scrollTarget = getScrollTarget(); -+ const supportsScrollEnd = !!scrollTarget && "onscrollend" in scrollTarget; -+ const timers = {}; -+ const finish = () => { -+ if (animatedPaddingReleaseRef.current !== finish) { -+ return; -+ } -+ animatedPaddingReleaseRef.current = void 0; -+ clearTimeout(timers.settle); -+ scrollTarget == null ? void 0 : scrollTarget.removeEventListener("scrollend", finishIfArrived); -+ cancelAnimationFrame(borrowWatchRef.current); -+ release(); -+ }; -+ const finishIfArrived = () => { -+ if (Math.abs(getCurrentScrollOffset() - offset) <= SCROLL_EXTENT_EPSILON) { -+ finish(); -+ } -+ }; -+ let framesSeen = 0; -+ const releaseWhenContentCommits = () => { -+ if (animatedPaddingReleaseRef.current !== finish) { -+ return; -+ } -+ if (++framesSeen % BORROW_WATCH_FRAME_INTERVAL === 0) { -+ if (getCommittedMaxScrollOffset(getMaxScrollOffset()) + SCROLL_EXTENT_EPSILON >= offset) { -+ finish(); -+ return; -+ } ++ const reissueHandleRef = useRef(0); ++ const scrollUntilReachable = useCallback( ++ (offset, animated, run) => { ++ cancelAnimationFrame(reissueHandleRef.current); ++ let attempts = 0; ++ const attempt = () => { ++ const liveMaxOffset = getMaxScrollOffset(); ++ run(clampOffset(offset, liveMaxOffset)); ++ if (!animated && Number.isFinite(offset) && offset - liveMaxOffset > SCROLL_EXTENT_EPSILON && ++attempts < REACHABLE_RETRY_FRAMES) { ++ reissueHandleRef.current = requestAnimationFrame(attempt); + } -+ borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); + }; -+ animatedPaddingReleaseRef.current = finish; -+ cancelAnimationFrame(borrowWatchRef.current); -+ borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); -+ timers.settle = setTimeout(finish, SMOOTH_SCROLL_MAX_MS); -+ if (supportsScrollEnd) { -+ scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finishIfArrived); -+ } -+ }, -+ [getCommittedMaxScrollOffset, getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, paddingEndProp] -+ ); -+ useEffect( -+ () => () => { -+ var _a4; -+ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); -+ cancelAnimationFrame(borrowWatchRef.current); -+ const paddedNode = paddedNodeRef.current; -+ if (paddedNode) { -+ releaseAllTemporaryEndPadding(paddedNode); -+ } ++ attempt(); + }, -+ [] ++ [getMaxScrollOffset] + ); ++ useEffect(() => () => cancelAnimationFrame(reissueHandleRef.current), []); const scrollToLocalOffset = useCallback( (offset, animated) => { const scrollElement = scrollRef.current; -@@ -6095,14 +6510,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6095,14 +6270,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); options.left = left; options.top = top; @@ -7195,7 +6390,7 @@ index 95465f2..e8a3fb5 100644 + target.scrollTo(options); } else { - options.top = clampedOffset; -+ withReachableExtent(offset, maxOffset, animated, (reachableOffset) => { ++ scrollUntilReachable(offset, animated, (reachableOffset) => { + if (horizontal) { + options.left = reachableOffset; + } else { @@ -7207,122 +6402,21 @@ index 95465f2..e8a3fb5 100644 - target.scrollTo(options); }, - [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] -+ [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, withReachableExtent] ++ [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable] ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6550,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6128,8 +6310,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; - const endOffset = getMaxScrollOffset(); - scrollToLocalOffset(endOffset, animated); -+ scrollToLocalOffset(getCommittedMaxScrollOffset(getMaxScrollOffset()), animated); ++ scrollToLocalOffset(getMaxScrollOffset(), animated); }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6142,7 +6563,12 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - if (!onScroll2 || !scrollRef.current) { - return; - } -- const contentSize = getContentSize2(contentRef.current); -+ const rawContentSize = getContentSize2(contentRef.current); -+ const temporaryPadding = getTemporaryEndPadding(contentRef.current, paddingEndProp); -+ const contentSize = temporaryPadding ? { -+ height: horizontal ? rawContentSize.height : rawContentSize.height - temporaryPadding, -+ width: horizontal ? rawContentSize.width - temporaryPadding : rawContentSize.width -+ } : rawContentSize; - const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); - const offset = getCurrentScrollOffset(); - const scrollEvent = { -@@ -6162,7 +6588,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - } - }; - onScroll2(scrollEvent); -- }, [getCurrentScrollOffset, horizontal, isWindowScroll, onScroll2]); -+ }, [getCurrentScrollOffset, horizontal, isWindowScroll, onScroll2, paddingEndProp]); - const scrollEventCoalescer = useRafCoalescer(emitScroll); - const scrollEndFallbackRef = useRef(void 0); - const emitScrollEnd = useCallback(() => { -@@ -6358,21 +6784,6 @@ function useValueListener$(key, callback) { - } - - // src/components/ScrollAdjust.tsx --function getScrollAdjustAxis(horizontal) { -- return horizontal ? { -- contentSizeKey: "scrollWidth", -- paddingEndProp: "paddingRight", -- viewportSizeKey: "clientWidth", -- x: 1, -- y: 0 -- } : { -- contentSizeKey: "scrollHeight", -- paddingEndProp: "paddingBottom", -- viewportSizeKey: "clientHeight", -- x: 0, -- y: 1 -- }; --} - function getScrollAdjustTarget(ctx, contentNode) { - var _a3, _b, _c; - const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6390,8 +6801,6 @@ function ScrollAdjust() { - const ctx = useStateContext(); - const lastScrollOffsetRef = React3.useRef(0); - const lastScrollAdjustUserOffsetRef = React3.useRef(0); -- const resetPaddingRafRef = React3.useRef(void 0); -- const temporaryPaddingRef = React3.useRef(void 0); - const contentNodeRef = React3.useRef(null); - const callback = React3.useCallback(() => { - const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6402,7 +6811,7 @@ function ScrollAdjust() { - const target = getScrollAdjustTarget(ctx, contentNodeRef.current); - if (target) { - const horizontal = !!ctx.state.props.horizontal; -- const axis = getScrollAdjustAxis(horizontal); -+ const axis = getScrollAxis(horizontal); - const { contentNode, scrollElement: el } = target; - const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; - const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6412,34 +6821,15 @@ function ScrollAdjust() { - const scrollBy = () => scrollAdjustBy(el, axis.x * scrollDelta, axis.y * scrollDelta); - contentNodeRef.current = contentNode; - if (shouldScroll && contentNode) { -- const totalSize = contentNode[axis.contentSizeKey]; -+ const totalSize = contentNode[axis.contentSizeKey] - getTemporaryEndPadding(contentNode, axis.paddingEndProp); - const viewportSize = el[axis.viewportSizeKey]; - const nextScroll = currentScroll + scrollDelta; - const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; - if (needsTemporaryPadding) { -- const currentInlinePaddingEnd = contentNode.style[axis.paddingEndProp]; -- const previousTemporaryPadding = temporaryPaddingRef.current; -- const baselinePaddingEnd = (previousTemporaryPadding == null ? void 0 : previousTemporaryPadding.value) === currentInlinePaddingEnd ? previousTemporaryPadding.baseline : currentInlinePaddingEnd; - const pad = (nextScroll + viewportSize - totalSize) * 2; -- const currentPaddingEnd = Number.parseFloat( -- window.getComputedStyle(contentNode)[axis.paddingEndProp] -- ); -- const temporaryPaddingEnd = `${(currentPaddingEnd || 0) + pad}px`; -- temporaryPaddingRef.current = { baseline: baselinePaddingEnd, value: temporaryPaddingEnd }; -- contentNode.style[axis.paddingEndProp] = temporaryPaddingEnd; -- void contentNode.offsetHeight; -+ const release = addTemporaryEndPadding(contentNode, axis.paddingEndProp, pad); - scrollBy(); -- if (resetPaddingRafRef.current !== void 0) { -- cancelAnimationFrame(resetPaddingRafRef.current); -- } -- resetPaddingRafRef.current = requestAnimationFrame(() => { -- const temporaryPadding = temporaryPaddingRef.current; -- resetPaddingRafRef.current = void 0; -- temporaryPaddingRef.current = void 0; -- if (contentNode.style[axis.paddingEndProp] === (temporaryPadding == null ? void 0 : temporaryPadding.value)) { -- contentNode.style[axis.paddingEndProp] = temporaryPadding.baseline; -- } -- }); -+ scheduleTemporaryEndPaddingRelease(contentNode, axis.paddingEndProp, release); - } else { - scrollBy(); - } -@@ -8267,6 +8657,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8267,6 +8448,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -7331,7 +6425,7 @@ index 95465f2..e8a3fb5 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js -index 914d2da..c235070 100644 +index 914d2da..eed3566 100644 --- a/node_modules/@legendapp/list/react.js +++ b/node_modules/@legendapp/list/react.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -7940,18 +7034,16 @@ index 914d2da..c235070 100644 // src/core/getStartOffsetAdjustment.ts function getStartOffsetAdjustment(ctx) { const { state } = ctx; -@@ -1238,54 +639,269 @@ function getItemSizeAtIndex(ctx, index) { - return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); - } - --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; +@@ -1234,58 +635,273 @@ function getItemSizeAtIndex(ctx, index) { + if (index === void 0 || index < 0) { + return void 0; + } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ +// src/core/calculateOffsetWithOffsetPosition.ts +function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { + var _a3; @@ -8150,17 +7242,12 @@ index 914d2da..c235070 100644 + } +} + -+// src/utils/checkAtTop.ts -+function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -+ const state = ctx == null ? void 0 : ctx.state; -+ if (!state) { -+ return; - } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } ++// src/utils/checkAtTop.ts ++function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { ++ const state = ctx == null ? void 0 : ctx.state; ++ if (!state) { ++ return; ++ } + const { + isStartReached, + props: { data, onStartReachedThreshold }, @@ -8175,22 +7262,7 @@ index 914d2da..c235070 100644 + if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { + state.isStartReached = false; + state.startReachedSnapshot = void 0; - } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } ++ } + set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); + set$(ctx, "isNearStart", scroll <= threshold); + const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; @@ -8217,19 +7289,50 @@ index 914d2da..c235070 100644 + state.startReachedSnapshot = snapshot; + } + ); - } -- return offset; ++ } } --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +- } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } +- } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } +- } +- return offset; +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+} -+ + } + +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { + var _a3, _b; @@ -8251,30 +7354,10 @@ index 914d2da..c235070 100644 } // src/core/finishScrollTo.ts -@@ -1413,16 +1029,191 @@ function listenForScrollEnd(ctx, params) { - if (idleTimeout !== void 0) { - clearTimeout(idleTimeout); - } -- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); -- }; -- const onScrollEnd = () => finish("scrollend"); -- target.addEventListener("scroll", onScroll2); -- if (supportsScrollEnd) { -- target.addEventListener("scrollend", onScrollEnd); -- } else { -- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); -+ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); -+ }; -+ const onScrollEnd = () => finish("scrollend"); -+ target.addEventListener("scroll", onScroll2); -+ if (supportsScrollEnd) { -+ target.addEventListener("scrollend", onScrollEnd); -+ } else { -+ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); -+ } -+ scheduledWork.register("platformScrollCompletion", cancel); -+} -+ +@@ -1425,6 +1041,181 @@ function listenForScrollEnd(ctx, params) { + scheduledWork.register("platformScrollCompletion", cancel); + } + +// src/core/initialScrollSession.ts +var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +function hasInitialScrollSessionCompletion(completion) { @@ -8447,11 +7530,12 @@ index 914d2da..c235070 100644 + } else { + resetAdaptiveRender(ctx); + } - } -- scheduledWork.register("platformScrollCompletion", cancel); - } - ++ } ++} ++ // src/core/doMaintainScrollAtEnd.ts + function doMaintainScrollAtEnd(ctx) { + const state = ctx.state; @@ -1451,6 +1242,7 @@ function doMaintainScrollAtEnd(ctx) { const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; @@ -8488,58 +7572,27 @@ index 914d2da..c235070 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2055,70 +1868,402 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { - if (nextTop === void 0 || nextTop > viewportEnd) { - break; - } -- end++; -+ end++; -+ } -+ return { end, start }; -+} -+function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -+ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -+ if (range) { -+ ctx.state.scrollTargetPinnedRange = range; -+ ctx.state.scrollForNextCalculateItemsInView = void 0; -+ } else { -+ ctx.state.scrollTargetPinnedRange = void 0; -+ } -+} -+function scrollTo(ctx, params) { +@@ -2069,7 +1882,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + } + } + function scrollTo(ctx, params) { +- var _a3, _b; + var _a3, _b, _c; -+ const state = ctx.state; -+ const { noScrollingTo, forceScroll, ...scrollTarget } = params; -+ const { -+ animated, -+ isInitialScroll, -+ offset: scrollTargetOffset, -+ precomputedWithViewOffset, -+ waitForInitialScrollCompletionFrame -+ } = scrollTarget; -+ const { -+ props: { horizontal } -+ } = state; -+ cancelScrollCompletionChecks(state); -+ const requestedOffset = precomputedWithViewOffset ? scrollTargetOffset : calculateOffsetWithOffsetPosition(ctx, scrollTargetOffset, scrollTarget); -+ const shouldPreserveRawInitialOffsetRequest = !!isInitialScroll && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; -+ const targetOffset = clampScrollOffset(ctx, requestedOffset, scrollTarget); -+ const offset = shouldPreserveRawInitialOffsetRequest ? requestedOffset : targetOffset; -+ state.scrollHistory.length = 0; -+ if (!noScrollingTo) { -+ if (isInitialScroll) { -+ initialScrollCompletion.resetFlags(state); + const state = ctx.state; + const { noScrollingTo, forceScroll, ...scrollTarget } = params; + const { +@@ -2091,6 +1904,7 @@ function scrollTo(ctx, params) { + if (!noScrollingTo) { + if (isInitialScroll) { + initialScrollCompletion.resetFlags(state); + clearScrollTargetSettle(state); -+ } -+ const averageSizeSnapshot = getAverageSizeSnapshot(state); -+ state.scrollingTo = { -+ ...scrollTarget, -+ ...averageSizeSnapshot ? { averageSizeSnapshot } : {}, -+ targetOffset, -+ waitForInitialScrollCompletionFrame -+ }; -+ if (!isInitialScroll) { -+ pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); + } + const averageSizeSnapshot = getAverageSizeSnapshot(state); + state.scrollingTo = { +@@ -2101,6 +1915,15 @@ function scrollTo(ctx, params) { + }; + if (!isInitialScroll) { + pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); + if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { + beginScrollTargetSettle(ctx, { + index: scrollTarget.index, @@ -8549,26 +7602,22 @@ index 914d2da..c235070 100644 + } else { + clearScrollTargetSettle(state); + } -+ } -+ } -+ state.scrollPending = targetOffset; -+ syncInitialScrollNativeWatchdog(state, { isInitialScroll, requestedOffset: offset, targetOffset }); -+ if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { -+ if (animated) { -+ if (state.scrollTargetPinnedRange) { + } + } + state.scrollPending = targetOffset; +@@ -2108,7 +1931,7 @@ function scrollTo(ctx, params) { + if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { + if (animated) { + if (state.scrollTargetPinnedRange) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); + (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); -+ } -+ } else { -+ updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -+ } -+ } -+ if (forceScroll || !isInitialScroll || Platform.OS === "android") { -+ doScrollTo(ctx, { animated, horizontal, offset }); -+ } else { -+ state.scroll = offset; -+ } -+} -+ + } + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); +@@ -2121,6 +1944,328 @@ function scrollTo(ctx, params) { + } + } + +// src/core/scrollTargetSettle.ts +var SETTLE_POSITION_EPSILON = 1; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; @@ -8796,17 +7845,8 @@ index 914d2da..c235070 100644 + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); + } + } - } -- return { end, start }; - } --function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -- if (range) { -- ctx.state.scrollTargetPinnedRange = range; -- ctx.state.scrollForNextCalculateItemsInView = void 0; -- } else { -- ctx.state.scrollTargetPinnedRange = void 0; -- } ++ } ++} + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -8814,9 +7854,7 @@ index 914d2da..c235070 100644 + state.scroll = offset; + state.scrollPending = offset; + state.scrollPrev = offset; - } --function scrollTo(ctx, params) { -- var _a3, _b; ++} +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -8828,56 +7866,21 @@ index 914d2da..c235070 100644 +} +function supersedeInitialScroll(ctx) { + var _a3, _b, _c; - const state = ctx.state; -- const { noScrollingTo, forceScroll, ...scrollTarget } = params; -- const { -- animated, -- isInitialScroll, -- offset: scrollTargetOffset, -- precomputedWithViewOffset, -- waitForInitialScrollCompletionFrame -- } = scrollTarget; -- const { -- props: { horizontal } -- } = state; -- cancelScrollCompletionChecks(state); -- const requestedOffset = precomputedWithViewOffset ? scrollTargetOffset : calculateOffsetWithOffsetPosition(ctx, scrollTargetOffset, scrollTarget); -- const shouldPreserveRawInitialOffsetRequest = !!isInitialScroll && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; -- const targetOffset = clampScrollOffset(ctx, requestedOffset, scrollTarget); -- const offset = shouldPreserveRawInitialOffsetRequest ? requestedOffset : targetOffset; -- state.scrollHistory.length = 0; -- if (!noScrollingTo) { -- if (isInitialScroll) { -- initialScrollCompletion.resetFlags(state); ++ const state = ctx.state; + const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; + if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { + if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { + cancelAnimationFrame(bootstrapInitialScroll.frameHandle); - } -- const averageSizeSnapshot = getAverageSizeSnapshot(state); -- state.scrollingTo = { -- ...scrollTarget, -- ...averageSizeSnapshot ? { averageSizeSnapshot } : {}, -- targetOffset, -- waitForInitialScrollCompletionFrame -- }; -- if (!isInitialScroll) { -- pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); ++ } + if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { + cancelScrollCompletionChecks(state); + state.scrollingTo = void 0; + state.scrollTargetPinnedRange = void 0; - } ++ } + initialScrollCompletion.resetFlags(state); + setInitialScrollSession(state, { bootstrap: null }); + finishInitialScroll(ctx); - } -- state.scrollPending = targetOffset; -- syncInitialScrollNativeWatchdog(state, { isInitialScroll, requestedOffset: offset, targetOffset }); -- if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { -- if (animated) { -- if (state.scrollTargetPinnedRange) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++ } +} +function finishInitialScroll(ctx, options) { + var _a3, _b, _c; @@ -8911,11 +7914,10 @@ index 914d2da..c235070 100644 + PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, + "preservedInitialScroll" + ); - } - } else { -- updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); ++ } ++ } else { + clearPreservedInitialScrollTarget(state); - } ++ } + if (options == null ? void 0 : options.recalculateItems) { + recalculateSettledScroll(ctx); + } @@ -8928,12 +7930,7 @@ index 914d2da..c235070 100644 + if (options == null ? void 0 : options.waitForCompletionFrame) { + requestAnimationFrame(complete); + return; - } -- if (forceScroll || !isInitialScroll || Platform.OS === "android") { -- doScrollTo(ctx, { animated, horizontal, offset }); -- } else { -- state.scroll = offset; -- } ++ } + complete(); +} + @@ -8941,9 +7938,11 @@ index 914d2da..c235070 100644 +function calculateOffsetForIndex(ctx, index) { + const state = ctx.state; + return index !== void 0 ? state.positions[index] || 0 : 0; - } - ++} ++ // src/core/scrollToIndex.ts + function clampScrollIndex(index, dataLength) { + if (dataLength <= 0) { @@ -4350,6 +4495,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); @@ -8973,297 +7972,40 @@ index 914d2da..c235070 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5927,6 +6083,21 @@ function getDocumentScrollerNode() { - } - return document.scrollingElement || document.documentElement || document.body; - } -+function getScrollAxis(horizontal) { -+ return horizontal ? { -+ contentSizeKey: "scrollWidth", -+ paddingEndProp: "paddingRight", -+ viewportSizeKey: "clientWidth", -+ x: 1, -+ y: 0 -+ } : { -+ contentSizeKey: "scrollHeight", -+ paddingEndProp: "paddingBottom", -+ viewportSizeKey: "clientHeight", -+ x: 0, -+ y: 1 -+ }; -+} - function getWindowScrollPosition() { - var _a3, _b, _c, _d; - if (typeof window === "undefined") { -@@ -6003,9 +6174,155 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll - }; - } - -+// src/components/webTemporaryEndPadding.ts -+var entriesByNode = /* @__PURE__ */ new WeakMap(); -+var nextRequestId = 1; -+var RELEASE_ALL_MAX_PASSES = 5; -+function readResolvedPadding(node, prop) { -+ return Number.parseFloat(window.getComputedStyle(node)[prop]) || 0; -+} -+function totalRequested(entry) { -+ let total = 0; -+ for (const size of entry.requests.values()) { -+ total += size; -+ } -+ return total; -+} -+function isOwnedByUs(node, prop, entry) { -+ return entry.lastApplied === "" || node.style[prop] === entry.lastApplied; -+} -+function measureNodeExtent(node, prop) { -+ return prop === "paddingBottom" ? node.scrollHeight : node.scrollWidth; -+} -+function applyPadding(node, prop, entry) { -+ const total = totalRequested(entry); -+ const before = measureNodeExtent(node, prop); -+ node.style[prop] = total === 0 ? entry.baseline : `${entry.baselineSize + total}px`; -+ entry.boughtTotal = Math.max(0, entry.boughtTotal + (measureNodeExtent(node, prop) - before)); -+ entry.lastApplied = node.style[prop]; -+} -+function drainPendingReleases(entry) { -+ if (entry.resetHandle !== void 0) { -+ cancelAnimationFrame(entry.resetHandle); -+ entry.resetHandle = void 0; -+ } -+ if (entry.pendingReleases.size === 0) { -+ return; -+ } -+ const releases = [...entry.pendingReleases]; -+ entry.pendingReleases.clear(); -+ for (const pending of releases) { -+ pending(); -+ } -+} -+function releaseEntry(node, prop, requestId) { -+ const entries = entriesByNode.get(node); -+ const entry = entries == null ? void 0 : entries[prop]; -+ if (!entry || !entry.requests.delete(requestId)) { -+ return; -+ } -+ if (isOwnedByUs(node, prop, entry)) { -+ applyPadding(node, prop, entry); -+ } -+ if (entry.requests.size === 0) { -+ entries == null ? true : delete entries[prop]; -+ drainPendingReleases(entry); -+ } -+} -+function addTemporaryEndPadding(node, prop, extraSize) { -+ var _a3; -+ const entries = (_a3 = entriesByNode.get(node)) != null ? _a3 : {}; -+ let entry = entries[prop]; -+ if (entry && !isOwnedByUs(node, prop, entry)) { -+ entry.baseline = node.style[prop]; -+ entry.baselineSize = readResolvedPadding(node, prop); -+ entry.boughtTotal = 0; -+ } -+ if (!entry) { -+ entry = { -+ baseline: node.style[prop], -+ baselineSize: readResolvedPadding(node, prop), -+ boughtTotal: 0, -+ lastApplied: "", -+ pendingReleases: /* @__PURE__ */ new Set(), -+ requests: /* @__PURE__ */ new Map(), -+ resetHandle: void 0 -+ }; -+ entries[prop] = entry; -+ entriesByNode.set(node, entries); -+ } -+ const requestId = nextRequestId++; -+ entry.requests.set(requestId, extraSize); -+ applyPadding(node, prop, entry); -+ void node.offsetHeight; -+ return function releaseTemporaryEndPadding() { -+ releaseEntry(node, prop, requestId); -+ }; -+} -+function scheduleTemporaryEndPaddingRelease(node, prop, release) { -+ var _a3; -+ const entry = (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop]; -+ if (!entry) { -+ release(); -+ return; -+ } -+ entry.pendingReleases.add(release); -+ if (entry.resetHandle !== void 0) { -+ return; -+ } -+ entry.resetHandle = requestAnimationFrame(() => { -+ entry.resetHandle = void 0; -+ const releases = [...entry.pendingReleases]; -+ entry.pendingReleases.clear(); -+ for (const pending of releases) { -+ pending(); -+ } -+ }); -+} -+function getTemporaryEndPadding(node, prop) { -+ var _a3; -+ if (!node) { -+ return 0; -+ } -+ const entry = (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop]; -+ if (!entry || !isOwnedByUs(node, prop, entry)) { -+ return 0; -+ } -+ return entry.boughtTotal; -+} -+function releaseAllTemporaryEndPadding(node) { -+ const entries = entriesByNode.get(node); -+ if (!entries) { -+ return; -+ } -+ for (let pass = 0; pass < RELEASE_ALL_MAX_PASSES; pass++) { -+ const props = Object.keys(entries); -+ if (props.length === 0) { -+ break; -+ } -+ for (const prop of props) { -+ const entry = entries[prop]; -+ if (!entry) { -+ continue; -+ } -+ if (isOwnedByUs(node, prop, entry)) { -+ node.style[prop] = entry.baseline; -+ } -+ delete entries[prop]; -+ entry.requests.clear(); -+ drainPendingReleases(entry); -+ } -+ } -+ entriesByNode.delete(node); -+} -+ +@@ -6006,6 +6162,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; -+var SMOOTH_SCROLL_MAX_MS = 2e3; -+var BORROW_WATCH_FRAME_INTERVAL = 3; -+var BORROW_MEASURE_ATTEMPTS = 2; ++var REACHABLE_RETRY_FRAMES = 30; var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6074,6 +6391,11 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), - [isWindowScroll] - ); -+ const paddingEndProp = getScrollAxis(horizontal).paddingEndProp; -+ const getCommittedMaxScrollOffset = React3.useCallback( -+ (maxOffset) => Math.max(0, maxOffset - getTemporaryEndPadding(contentRef.current, paddingEndProp)), -+ [paddingEndProp] -+ ); - const getMaxScrollOffset = React3.useCallback(() => { - const scrollElement = scrollRef.current; - const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6094,6 +6416,99 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,6 +6252,23 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); -+ const paddedNodeRef = React3.useRef(null); -+ const borrowWatchRef = React3.useRef(0); -+ const animatedPaddingReleaseRef = React3.useRef(void 0); -+ const withReachableExtent = React3.useCallback( -+ (offset, maxOffset, animated, run) => { -+ var _a4; -+ const contentNode = contentRef.current; -+ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); -+ const committedMaxOffset = getCommittedMaxScrollOffset(getMaxScrollOffset()); -+ if (!contentNode || !Number.isFinite(offset)) { -+ run(clampOffset(offset, maxOffset)); -+ return; -+ } -+ if (offset <= committedMaxOffset + SCROLL_EXTENT_EPSILON) { -+ run(clampOffset(offset, maxOffset)); -+ return; -+ } -+ const releases = []; -+ for (let attempt = 0; attempt < BORROW_MEASURE_ATTEMPTS; attempt++) { -+ const shortfall = offset - getMaxScrollOffset(); -+ if (shortfall <= 0) { -+ break; -+ } -+ releases.push(addTemporaryEndPadding(contentNode, paddingEndProp, shortfall + SCROLL_EXTENT_EPSILON)); -+ } -+ if (releases.length === 0) { -+ run(offset); -+ return; -+ } -+ const release = () => { -+ for (const releaseOne of releases) { -+ releaseOne(); -+ } -+ }; -+ paddedNodeRef.current = contentNode; -+ run(offset); -+ if (!animated) { -+ scheduleTemporaryEndPaddingRelease(contentNode, paddingEndProp, release); -+ return; -+ } -+ const scrollTarget = getScrollTarget(); -+ const supportsScrollEnd = !!scrollTarget && "onscrollend" in scrollTarget; -+ const timers = {}; -+ const finish = () => { -+ if (animatedPaddingReleaseRef.current !== finish) { -+ return; -+ } -+ animatedPaddingReleaseRef.current = void 0; -+ clearTimeout(timers.settle); -+ scrollTarget == null ? void 0 : scrollTarget.removeEventListener("scrollend", finishIfArrived); -+ cancelAnimationFrame(borrowWatchRef.current); -+ release(); -+ }; -+ const finishIfArrived = () => { -+ if (Math.abs(getCurrentScrollOffset() - offset) <= SCROLL_EXTENT_EPSILON) { -+ finish(); -+ } -+ }; -+ let framesSeen = 0; -+ const releaseWhenContentCommits = () => { -+ if (animatedPaddingReleaseRef.current !== finish) { -+ return; -+ } -+ if (++framesSeen % BORROW_WATCH_FRAME_INTERVAL === 0) { -+ if (getCommittedMaxScrollOffset(getMaxScrollOffset()) + SCROLL_EXTENT_EPSILON >= offset) { -+ finish(); -+ return; -+ } ++ const reissueHandleRef = React3.useRef(0); ++ const scrollUntilReachable = React3.useCallback( ++ (offset, animated, run) => { ++ cancelAnimationFrame(reissueHandleRef.current); ++ let attempts = 0; ++ const attempt = () => { ++ const liveMaxOffset = getMaxScrollOffset(); ++ run(clampOffset(offset, liveMaxOffset)); ++ if (!animated && Number.isFinite(offset) && offset - liveMaxOffset > SCROLL_EXTENT_EPSILON && ++attempts < REACHABLE_RETRY_FRAMES) { ++ reissueHandleRef.current = requestAnimationFrame(attempt); + } -+ borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); + }; -+ animatedPaddingReleaseRef.current = finish; -+ cancelAnimationFrame(borrowWatchRef.current); -+ borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); -+ timers.settle = setTimeout(finish, SMOOTH_SCROLL_MAX_MS); -+ if (supportsScrollEnd) { -+ scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finishIfArrived); -+ } -+ }, -+ [getCommittedMaxScrollOffset, getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, paddingEndProp] -+ ); -+ React3.useEffect( -+ () => () => { -+ var _a4; -+ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); -+ cancelAnimationFrame(borrowWatchRef.current); -+ const paddedNode = paddedNodeRef.current; -+ if (paddedNode) { -+ releaseAllTemporaryEndPadding(paddedNode); -+ } ++ attempt(); + }, -+ [] ++ [getMaxScrollOffset] + ); ++ React3.useEffect(() => () => cancelAnimationFrame(reissueHandleRef.current), []); const scrollToLocalOffset = React3.useCallback( (offset, animated) => { const scrollElement = scrollRef.current; -@@ -6116,14 +6531,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6116,14 +6291,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); options.left = left; options.top = top; @@ -9274,7 +8016,7 @@ index 914d2da..c235070 100644 + target.scrollTo(options); } else { - options.top = clampedOffset; -+ withReachableExtent(offset, maxOffset, animated, (reachableOffset) => { ++ scrollUntilReachable(offset, animated, (reachableOffset) => { + if (horizontal) { + options.left = reachableOffset; + } else { @@ -9286,122 +8028,21 @@ index 914d2da..c235070 100644 - target.scrollTo(options); }, - [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] -+ [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, withReachableExtent] ++ [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable] ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6571,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6149,8 +6331,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; - const endOffset = getMaxScrollOffset(); - scrollToLocalOffset(endOffset, animated); -+ scrollToLocalOffset(getCommittedMaxScrollOffset(getMaxScrollOffset()), animated); ++ scrollToLocalOffset(getMaxScrollOffset(), animated); }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6163,7 +6584,12 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - if (!onScroll2 || !scrollRef.current) { - return; - } -- const contentSize = getContentSize2(contentRef.current); -+ const rawContentSize = getContentSize2(contentRef.current); -+ const temporaryPadding = getTemporaryEndPadding(contentRef.current, paddingEndProp); -+ const contentSize = temporaryPadding ? { -+ height: horizontal ? rawContentSize.height : rawContentSize.height - temporaryPadding, -+ width: horizontal ? rawContentSize.width - temporaryPadding : rawContentSize.width -+ } : rawContentSize; - const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); - const offset = getCurrentScrollOffset(); - const scrollEvent = { -@@ -6183,7 +6609,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - } - }; - onScroll2(scrollEvent); -- }, [getCurrentScrollOffset, horizontal, isWindowScroll, onScroll2]); -+ }, [getCurrentScrollOffset, horizontal, isWindowScroll, onScroll2, paddingEndProp]); - const scrollEventCoalescer = useRafCoalescer(emitScroll); - const scrollEndFallbackRef = React3.useRef(void 0); - const emitScrollEnd = React3.useCallback(() => { -@@ -6379,21 +6805,6 @@ function useValueListener$(key, callback) { - } - - // src/components/ScrollAdjust.tsx --function getScrollAdjustAxis(horizontal) { -- return horizontal ? { -- contentSizeKey: "scrollWidth", -- paddingEndProp: "paddingRight", -- viewportSizeKey: "clientWidth", -- x: 1, -- y: 0 -- } : { -- contentSizeKey: "scrollHeight", -- paddingEndProp: "paddingBottom", -- viewportSizeKey: "clientHeight", -- x: 0, -- y: 1 -- }; --} - function getScrollAdjustTarget(ctx, contentNode) { - var _a3, _b, _c; - const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6411,8 +6822,6 @@ function ScrollAdjust() { - const ctx = useStateContext(); - const lastScrollOffsetRef = React3__namespace.useRef(0); - const lastScrollAdjustUserOffsetRef = React3__namespace.useRef(0); -- const resetPaddingRafRef = React3__namespace.useRef(void 0); -- const temporaryPaddingRef = React3__namespace.useRef(void 0); - const contentNodeRef = React3__namespace.useRef(null); - const callback = React3__namespace.useCallback(() => { - const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6423,7 +6832,7 @@ function ScrollAdjust() { - const target = getScrollAdjustTarget(ctx, contentNodeRef.current); - if (target) { - const horizontal = !!ctx.state.props.horizontal; -- const axis = getScrollAdjustAxis(horizontal); -+ const axis = getScrollAxis(horizontal); - const { contentNode, scrollElement: el } = target; - const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; - const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6433,34 +6842,15 @@ function ScrollAdjust() { - const scrollBy = () => scrollAdjustBy(el, axis.x * scrollDelta, axis.y * scrollDelta); - contentNodeRef.current = contentNode; - if (shouldScroll && contentNode) { -- const totalSize = contentNode[axis.contentSizeKey]; -+ const totalSize = contentNode[axis.contentSizeKey] - getTemporaryEndPadding(contentNode, axis.paddingEndProp); - const viewportSize = el[axis.viewportSizeKey]; - const nextScroll = currentScroll + scrollDelta; - const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; - if (needsTemporaryPadding) { -- const currentInlinePaddingEnd = contentNode.style[axis.paddingEndProp]; -- const previousTemporaryPadding = temporaryPaddingRef.current; -- const baselinePaddingEnd = (previousTemporaryPadding == null ? void 0 : previousTemporaryPadding.value) === currentInlinePaddingEnd ? previousTemporaryPadding.baseline : currentInlinePaddingEnd; - const pad = (nextScroll + viewportSize - totalSize) * 2; -- const currentPaddingEnd = Number.parseFloat( -- window.getComputedStyle(contentNode)[axis.paddingEndProp] -- ); -- const temporaryPaddingEnd = `${(currentPaddingEnd || 0) + pad}px`; -- temporaryPaddingRef.current = { baseline: baselinePaddingEnd, value: temporaryPaddingEnd }; -- contentNode.style[axis.paddingEndProp] = temporaryPaddingEnd; -- void contentNode.offsetHeight; -+ const release = addTemporaryEndPadding(contentNode, axis.paddingEndProp, pad); - scrollBy(); -- if (resetPaddingRafRef.current !== void 0) { -- cancelAnimationFrame(resetPaddingRafRef.current); -- } -- resetPaddingRafRef.current = requestAnimationFrame(() => { -- const temporaryPadding = temporaryPaddingRef.current; -- resetPaddingRafRef.current = void 0; -- temporaryPaddingRef.current = void 0; -- if (contentNode.style[axis.paddingEndProp] === (temporaryPadding == null ? void 0 : temporaryPadding.value)) { -- contentNode.style[axis.paddingEndProp] = temporaryPadding.baseline; -- } -- }); -+ scheduleTemporaryEndPaddingRelease(contentNode, axis.paddingEndProp, release); - } else { - scrollBy(); - } -@@ -8288,6 +8678,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8288,6 +8469,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -9410,7 +8051,7 @@ index 914d2da..c235070 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs -index 95465f2..e8a3fb5 100644 +index 95465f2..31c9d25 100644 --- a/node_modules/@legendapp/list/react.mjs +++ b/node_modules/@legendapp/list/react.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -10015,22 +8656,20 @@ index 95465f2..e8a3fb5 100644 - const state = ctx.state; - return index !== void 0 ? state.positions[index] || 0 : 0; -} -- - // src/core/getStartOffsetAdjustment.ts - function getStartOffsetAdjustment(ctx) { - const { state } = ctx; -@@ -1217,54 +618,269 @@ function getItemSizeAtIndex(ctx, index) { - return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); - } - --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; +- + // src/core/getStartOffsetAdjustment.ts + function getStartOffsetAdjustment(ctx) { + const { state } = ctx; +@@ -1213,58 +614,273 @@ function getItemSizeAtIndex(ctx, index) { + if (index === void 0 || index < 0) { + return void 0; + } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ +// src/core/calculateOffsetWithOffsetPosition.ts +function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { + var _a3; @@ -10234,12 +8873,7 @@ index 95465f2..e8a3fb5 100644 + const state = ctx == null ? void 0 : ctx.state; + if (!state) { + return; - } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } ++ } + const { + isStartReached, + props: { data, onStartReachedThreshold }, @@ -10254,22 +8888,7 @@ index 95465f2..e8a3fb5 100644 + if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { + state.isStartReached = false; + state.startReachedSnapshot = void 0; - } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } ++ } + set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); + set$(ctx, "isNearStart", scroll <= threshold); + const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; @@ -10296,19 +8915,50 @@ index 95465f2..e8a3fb5 100644 + state.startReachedSnapshot = snapshot; + } + ); - } -- return offset; ++ } } --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +- } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } +- } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; +- } +- } +- return offset; +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+} -+ + } + +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { + var _a3, _b; @@ -10330,30 +8980,10 @@ index 95465f2..e8a3fb5 100644 } // src/core/finishScrollTo.ts -@@ -1392,16 +1008,191 @@ function listenForScrollEnd(ctx, params) { - if (idleTimeout !== void 0) { - clearTimeout(idleTimeout); - } -- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); -- }; -- const onScrollEnd = () => finish("scrollend"); -- target.addEventListener("scroll", onScroll2); -- if (supportsScrollEnd) { -- target.addEventListener("scrollend", onScrollEnd); -- } else { -- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); -+ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); -+ }; -+ const onScrollEnd = () => finish("scrollend"); -+ target.addEventListener("scroll", onScroll2); -+ if (supportsScrollEnd) { -+ target.addEventListener("scrollend", onScrollEnd); -+ } else { -+ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); -+ } -+ scheduledWork.register("platformScrollCompletion", cancel); -+} -+ +@@ -1404,6 +1020,181 @@ function listenForScrollEnd(ctx, params) { + scheduledWork.register("platformScrollCompletion", cancel); + } + +// src/core/initialScrollSession.ts +var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +function hasInitialScrollSessionCompletion(completion) { @@ -10526,11 +9156,12 @@ index 95465f2..e8a3fb5 100644 + } else { + resetAdaptiveRender(ctx); + } - } -- scheduledWork.register("platformScrollCompletion", cancel); - } - ++ } ++} ++ // src/core/doMaintainScrollAtEnd.ts + function doMaintainScrollAtEnd(ctx) { + const state = ctx.state; @@ -1430,6 +1221,7 @@ function doMaintainScrollAtEnd(ctx) { const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; @@ -10567,58 +9198,27 @@ index 95465f2..e8a3fb5 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2034,70 +1847,402 @@ function getTargetViewportRenderRange(ctx, targetOffset, targetIndex) { - if (nextTop === void 0 || nextTop > viewportEnd) { - break; - } -- end++; -+ end++; -+ } -+ return { end, start }; -+} -+function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -+ const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -+ if (range) { -+ ctx.state.scrollTargetPinnedRange = range; -+ ctx.state.scrollForNextCalculateItemsInView = void 0; -+ } else { -+ ctx.state.scrollTargetPinnedRange = void 0; -+ } -+} -+function scrollTo(ctx, params) { +@@ -2048,7 +1861,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + } + } + function scrollTo(ctx, params) { +- var _a3, _b; + var _a3, _b, _c; -+ const state = ctx.state; -+ const { noScrollingTo, forceScroll, ...scrollTarget } = params; -+ const { -+ animated, -+ isInitialScroll, -+ offset: scrollTargetOffset, -+ precomputedWithViewOffset, -+ waitForInitialScrollCompletionFrame -+ } = scrollTarget; -+ const { -+ props: { horizontal } -+ } = state; -+ cancelScrollCompletionChecks(state); -+ const requestedOffset = precomputedWithViewOffset ? scrollTargetOffset : calculateOffsetWithOffsetPosition(ctx, scrollTargetOffset, scrollTarget); -+ const shouldPreserveRawInitialOffsetRequest = !!isInitialScroll && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; -+ const targetOffset = clampScrollOffset(ctx, requestedOffset, scrollTarget); -+ const offset = shouldPreserveRawInitialOffsetRequest ? requestedOffset : targetOffset; -+ state.scrollHistory.length = 0; -+ if (!noScrollingTo) { -+ if (isInitialScroll) { -+ initialScrollCompletion.resetFlags(state); + const state = ctx.state; + const { noScrollingTo, forceScroll, ...scrollTarget } = params; + const { +@@ -2070,6 +1883,7 @@ function scrollTo(ctx, params) { + if (!noScrollingTo) { + if (isInitialScroll) { + initialScrollCompletion.resetFlags(state); + clearScrollTargetSettle(state); -+ } -+ const averageSizeSnapshot = getAverageSizeSnapshot(state); -+ state.scrollingTo = { -+ ...scrollTarget, -+ ...averageSizeSnapshot ? { averageSizeSnapshot } : {}, -+ targetOffset, -+ waitForInitialScrollCompletionFrame -+ }; -+ if (!isInitialScroll) { -+ pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); + } + const averageSizeSnapshot = getAverageSizeSnapshot(state); + state.scrollingTo = { +@@ -2080,6 +1894,15 @@ function scrollTo(ctx, params) { + }; + if (!isInitialScroll) { + pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); + if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { + beginScrollTargetSettle(ctx, { + index: scrollTarget.index, @@ -10628,26 +9228,22 @@ index 95465f2..e8a3fb5 100644 + } else { + clearScrollTargetSettle(state); + } -+ } -+ } -+ state.scrollPending = targetOffset; -+ syncInitialScrollNativeWatchdog(state, { isInitialScroll, requestedOffset: offset, targetOffset }); -+ if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { -+ if (animated) { -+ if (state.scrollTargetPinnedRange) { + } + } + state.scrollPending = targetOffset; +@@ -2087,7 +1910,7 @@ function scrollTo(ctx, params) { + if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { + if (animated) { + if (state.scrollTargetPinnedRange) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); + (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); -+ } -+ } else { -+ updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -+ } -+ } -+ if (forceScroll || !isInitialScroll || Platform.OS === "android") { -+ doScrollTo(ctx, { animated, horizontal, offset }); -+ } else { -+ state.scroll = offset; -+ } -+} -+ + } + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); +@@ -2100,6 +1923,328 @@ function scrollTo(ctx, params) { + } + } + +// src/core/scrollTargetSettle.ts +var SETTLE_POSITION_EPSILON = 1; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; @@ -10875,17 +9471,8 @@ index 95465f2..e8a3fb5 100644 + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); + } + } - } -- return { end, start }; - } --function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { -- const range = getTargetViewportRenderRange(ctx, targetOffset, targetIndex); -- if (range) { -- ctx.state.scrollTargetPinnedRange = range; -- ctx.state.scrollForNextCalculateItemsInView = void 0; -- } else { -- ctx.state.scrollTargetPinnedRange = void 0; -- } ++ } ++} + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -10893,9 +9480,7 @@ index 95465f2..e8a3fb5 100644 + state.scroll = offset; + state.scrollPending = offset; + state.scrollPrev = offset; - } --function scrollTo(ctx, params) { -- var _a3, _b; ++} +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -10907,56 +9492,21 @@ index 95465f2..e8a3fb5 100644 +} +function supersedeInitialScroll(ctx) { + var _a3, _b, _c; - const state = ctx.state; -- const { noScrollingTo, forceScroll, ...scrollTarget } = params; -- const { -- animated, -- isInitialScroll, -- offset: scrollTargetOffset, -- precomputedWithViewOffset, -- waitForInitialScrollCompletionFrame -- } = scrollTarget; -- const { -- props: { horizontal } -- } = state; -- cancelScrollCompletionChecks(state); -- const requestedOffset = precomputedWithViewOffset ? scrollTargetOffset : calculateOffsetWithOffsetPosition(ctx, scrollTargetOffset, scrollTarget); -- const shouldPreserveRawInitialOffsetRequest = !!isInitialScroll && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; -- const targetOffset = clampScrollOffset(ctx, requestedOffset, scrollTarget); -- const offset = shouldPreserveRawInitialOffsetRequest ? requestedOffset : targetOffset; -- state.scrollHistory.length = 0; -- if (!noScrollingTo) { -- if (isInitialScroll) { -- initialScrollCompletion.resetFlags(state); ++ const state = ctx.state; + const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; + if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { + if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { + cancelAnimationFrame(bootstrapInitialScroll.frameHandle); - } -- const averageSizeSnapshot = getAverageSizeSnapshot(state); -- state.scrollingTo = { -- ...scrollTarget, -- ...averageSizeSnapshot ? { averageSizeSnapshot } : {}, -- targetOffset, -- waitForInitialScrollCompletionFrame -- }; -- if (!isInitialScroll) { -- pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); ++ } + if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { + cancelScrollCompletionChecks(state); + state.scrollingTo = void 0; + state.scrollTargetPinnedRange = void 0; - } ++ } + initialScrollCompletion.resetFlags(state); + setInitialScrollSession(state, { bootstrap: null }); + finishInitialScroll(ctx); - } -- state.scrollPending = targetOffset; -- syncInitialScrollNativeWatchdog(state, { isInitialScroll, requestedOffset: offset, targetOffset }); -- if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { -- if (animated) { -- if (state.scrollTargetPinnedRange) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++ } +} +function finishInitialScroll(ctx, options) { + var _a3, _b, _c; @@ -10990,11 +9540,10 @@ index 95465f2..e8a3fb5 100644 + PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, + "preservedInitialScroll" + ); - } - } else { -- updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); ++ } ++ } else { + clearPreservedInitialScrollTarget(state); - } ++ } + if (options == null ? void 0 : options.recalculateItems) { + recalculateSettledScroll(ctx); + } @@ -11007,12 +9556,7 @@ index 95465f2..e8a3fb5 100644 + if (options == null ? void 0 : options.waitForCompletionFrame) { + requestAnimationFrame(complete); + return; - } -- if (forceScroll || !isInitialScroll || Platform.OS === "android") { -- doScrollTo(ctx, { animated, horizontal, offset }); -- } else { -- state.scroll = offset; -- } ++ } + complete(); +} + @@ -11020,9 +9564,11 @@ index 95465f2..e8a3fb5 100644 +function calculateOffsetForIndex(ctx, index) { + const state = ctx.state; + return index !== void 0 ? state.positions[index] || 0 : 0; - } - ++} ++ // src/core/scrollToIndex.ts + function clampScrollIndex(index, dataLength) { + if (dataLength <= 0) { @@ -4329,6 +4474,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); @@ -11052,297 +9598,40 @@ index 95465f2..e8a3fb5 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5906,6 +6062,21 @@ function getDocumentScrollerNode() { - } - return document.scrollingElement || document.documentElement || document.body; - } -+function getScrollAxis(horizontal) { -+ return horizontal ? { -+ contentSizeKey: "scrollWidth", -+ paddingEndProp: "paddingRight", -+ viewportSizeKey: "clientWidth", -+ x: 1, -+ y: 0 -+ } : { -+ contentSizeKey: "scrollHeight", -+ paddingEndProp: "paddingBottom", -+ viewportSizeKey: "clientHeight", -+ x: 0, -+ y: 1 -+ }; -+} - function getWindowScrollPosition() { - var _a3, _b, _c, _d; - if (typeof window === "undefined") { -@@ -5982,9 +6153,155 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll - }; - } - -+// src/components/webTemporaryEndPadding.ts -+var entriesByNode = /* @__PURE__ */ new WeakMap(); -+var nextRequestId = 1; -+var RELEASE_ALL_MAX_PASSES = 5; -+function readResolvedPadding(node, prop) { -+ return Number.parseFloat(window.getComputedStyle(node)[prop]) || 0; -+} -+function totalRequested(entry) { -+ let total = 0; -+ for (const size of entry.requests.values()) { -+ total += size; -+ } -+ return total; -+} -+function isOwnedByUs(node, prop, entry) { -+ return entry.lastApplied === "" || node.style[prop] === entry.lastApplied; -+} -+function measureNodeExtent(node, prop) { -+ return prop === "paddingBottom" ? node.scrollHeight : node.scrollWidth; -+} -+function applyPadding(node, prop, entry) { -+ const total = totalRequested(entry); -+ const before = measureNodeExtent(node, prop); -+ node.style[prop] = total === 0 ? entry.baseline : `${entry.baselineSize + total}px`; -+ entry.boughtTotal = Math.max(0, entry.boughtTotal + (measureNodeExtent(node, prop) - before)); -+ entry.lastApplied = node.style[prop]; -+} -+function drainPendingReleases(entry) { -+ if (entry.resetHandle !== void 0) { -+ cancelAnimationFrame(entry.resetHandle); -+ entry.resetHandle = void 0; -+ } -+ if (entry.pendingReleases.size === 0) { -+ return; -+ } -+ const releases = [...entry.pendingReleases]; -+ entry.pendingReleases.clear(); -+ for (const pending of releases) { -+ pending(); -+ } -+} -+function releaseEntry(node, prop, requestId) { -+ const entries = entriesByNode.get(node); -+ const entry = entries == null ? void 0 : entries[prop]; -+ if (!entry || !entry.requests.delete(requestId)) { -+ return; -+ } -+ if (isOwnedByUs(node, prop, entry)) { -+ applyPadding(node, prop, entry); -+ } -+ if (entry.requests.size === 0) { -+ entries == null ? true : delete entries[prop]; -+ drainPendingReleases(entry); -+ } -+} -+function addTemporaryEndPadding(node, prop, extraSize) { -+ var _a3; -+ const entries = (_a3 = entriesByNode.get(node)) != null ? _a3 : {}; -+ let entry = entries[prop]; -+ if (entry && !isOwnedByUs(node, prop, entry)) { -+ entry.baseline = node.style[prop]; -+ entry.baselineSize = readResolvedPadding(node, prop); -+ entry.boughtTotal = 0; -+ } -+ if (!entry) { -+ entry = { -+ baseline: node.style[prop], -+ baselineSize: readResolvedPadding(node, prop), -+ boughtTotal: 0, -+ lastApplied: "", -+ pendingReleases: /* @__PURE__ */ new Set(), -+ requests: /* @__PURE__ */ new Map(), -+ resetHandle: void 0 -+ }; -+ entries[prop] = entry; -+ entriesByNode.set(node, entries); -+ } -+ const requestId = nextRequestId++; -+ entry.requests.set(requestId, extraSize); -+ applyPadding(node, prop, entry); -+ void node.offsetHeight; -+ return function releaseTemporaryEndPadding() { -+ releaseEntry(node, prop, requestId); -+ }; -+} -+function scheduleTemporaryEndPaddingRelease(node, prop, release) { -+ var _a3; -+ const entry = (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop]; -+ if (!entry) { -+ release(); -+ return; -+ } -+ entry.pendingReleases.add(release); -+ if (entry.resetHandle !== void 0) { -+ return; -+ } -+ entry.resetHandle = requestAnimationFrame(() => { -+ entry.resetHandle = void 0; -+ const releases = [...entry.pendingReleases]; -+ entry.pendingReleases.clear(); -+ for (const pending of releases) { -+ pending(); -+ } -+ }); -+} -+function getTemporaryEndPadding(node, prop) { -+ var _a3; -+ if (!node) { -+ return 0; -+ } -+ const entry = (_a3 = entriesByNode.get(node)) == null ? void 0 : _a3[prop]; -+ if (!entry || !isOwnedByUs(node, prop, entry)) { -+ return 0; -+ } -+ return entry.boughtTotal; -+} -+function releaseAllTemporaryEndPadding(node) { -+ const entries = entriesByNode.get(node); -+ if (!entries) { -+ return; -+ } -+ for (let pass = 0; pass < RELEASE_ALL_MAX_PASSES; pass++) { -+ const props = Object.keys(entries); -+ if (props.length === 0) { -+ break; -+ } -+ for (const prop of props) { -+ const entry = entries[prop]; -+ if (!entry) { -+ continue; -+ } -+ if (isOwnedByUs(node, prop, entry)) { -+ node.style[prop] = entry.baseline; -+ } -+ delete entries[prop]; -+ entry.requests.clear(); -+ drainPendingReleases(entry); -+ } -+ } -+ entriesByNode.delete(node); -+} -+ +@@ -5985,6 +6141,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; +var SCROLL_EXTENT_EPSILON = 1; -+var SMOOTH_SCROLL_MAX_MS = 2e3; -+var BORROW_WATCH_FRAME_INTERVAL = 3; -+var BORROW_MEASURE_ATTEMPTS = 2; ++var REACHABLE_RETRY_FRAMES = 30; var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6053,6 +6370,11 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - () => resolveScrollEventTarget(scrollRef.current, isWindowScroll), - [isWindowScroll] - ); -+ const paddingEndProp = getScrollAxis(horizontal).paddingEndProp; -+ const getCommittedMaxScrollOffset = useCallback( -+ (maxOffset) => Math.max(0, maxOffset - getTemporaryEndPadding(contentRef.current, paddingEndProp)), -+ [paddingEndProp] -+ ); - const getMaxScrollOffset = useCallback(() => { - const scrollElement = scrollRef.current; - const contentSize = getScrollContentSize(scrollElement, contentRef.current, isWindowScroll); -@@ -6073,6 +6395,99 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,6 +6231,23 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); -+ const paddedNodeRef = useRef(null); -+ const borrowWatchRef = useRef(0); -+ const animatedPaddingReleaseRef = useRef(void 0); -+ const withReachableExtent = useCallback( -+ (offset, maxOffset, animated, run) => { -+ var _a4; -+ const contentNode = contentRef.current; -+ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); -+ const committedMaxOffset = getCommittedMaxScrollOffset(getMaxScrollOffset()); -+ if (!contentNode || !Number.isFinite(offset)) { -+ run(clampOffset(offset, maxOffset)); -+ return; -+ } -+ if (offset <= committedMaxOffset + SCROLL_EXTENT_EPSILON) { -+ run(clampOffset(offset, maxOffset)); -+ return; -+ } -+ const releases = []; -+ for (let attempt = 0; attempt < BORROW_MEASURE_ATTEMPTS; attempt++) { -+ const shortfall = offset - getMaxScrollOffset(); -+ if (shortfall <= 0) { -+ break; -+ } -+ releases.push(addTemporaryEndPadding(contentNode, paddingEndProp, shortfall + SCROLL_EXTENT_EPSILON)); -+ } -+ if (releases.length === 0) { -+ run(offset); -+ return; -+ } -+ const release = () => { -+ for (const releaseOne of releases) { -+ releaseOne(); -+ } -+ }; -+ paddedNodeRef.current = contentNode; -+ run(offset); -+ if (!animated) { -+ scheduleTemporaryEndPaddingRelease(contentNode, paddingEndProp, release); -+ return; -+ } -+ const scrollTarget = getScrollTarget(); -+ const supportsScrollEnd = !!scrollTarget && "onscrollend" in scrollTarget; -+ const timers = {}; -+ const finish = () => { -+ if (animatedPaddingReleaseRef.current !== finish) { -+ return; -+ } -+ animatedPaddingReleaseRef.current = void 0; -+ clearTimeout(timers.settle); -+ scrollTarget == null ? void 0 : scrollTarget.removeEventListener("scrollend", finishIfArrived); -+ cancelAnimationFrame(borrowWatchRef.current); -+ release(); -+ }; -+ const finishIfArrived = () => { -+ if (Math.abs(getCurrentScrollOffset() - offset) <= SCROLL_EXTENT_EPSILON) { -+ finish(); -+ } -+ }; -+ let framesSeen = 0; -+ const releaseWhenContentCommits = () => { -+ if (animatedPaddingReleaseRef.current !== finish) { -+ return; -+ } -+ if (++framesSeen % BORROW_WATCH_FRAME_INTERVAL === 0) { -+ if (getCommittedMaxScrollOffset(getMaxScrollOffset()) + SCROLL_EXTENT_EPSILON >= offset) { -+ finish(); -+ return; -+ } ++ const reissueHandleRef = useRef(0); ++ const scrollUntilReachable = useCallback( ++ (offset, animated, run) => { ++ cancelAnimationFrame(reissueHandleRef.current); ++ let attempts = 0; ++ const attempt = () => { ++ const liveMaxOffset = getMaxScrollOffset(); ++ run(clampOffset(offset, liveMaxOffset)); ++ if (!animated && Number.isFinite(offset) && offset - liveMaxOffset > SCROLL_EXTENT_EPSILON && ++attempts < REACHABLE_RETRY_FRAMES) { ++ reissueHandleRef.current = requestAnimationFrame(attempt); + } -+ borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); + }; -+ animatedPaddingReleaseRef.current = finish; -+ cancelAnimationFrame(borrowWatchRef.current); -+ borrowWatchRef.current = requestAnimationFrame(releaseWhenContentCommits); -+ timers.settle = setTimeout(finish, SMOOTH_SCROLL_MAX_MS); -+ if (supportsScrollEnd) { -+ scrollTarget == null ? void 0 : scrollTarget.addEventListener("scrollend", finishIfArrived); -+ } ++ attempt(); + }, -+ [getCommittedMaxScrollOffset, getCurrentScrollOffset, getMaxScrollOffset, getScrollTarget, paddingEndProp] -+ ); -+ useEffect( -+ () => () => { -+ var _a4; -+ (_a4 = animatedPaddingReleaseRef.current) == null ? void 0 : _a4.call(animatedPaddingReleaseRef); -+ cancelAnimationFrame(borrowWatchRef.current); -+ const paddedNode = paddedNodeRef.current; -+ if (paddedNode) { -+ releaseAllTemporaryEndPadding(paddedNode); -+ } -+ }, -+ [] ++ [getMaxScrollOffset] + ); ++ useEffect(() => () => cancelAnimationFrame(reissueHandleRef.current), []); const scrollToLocalOffset = useCallback( (offset, animated) => { const scrollElement = scrollRef.current; -@@ -6095,14 +6510,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6095,14 +6270,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); options.left = left; options.top = top; @@ -11353,7 +9642,7 @@ index 95465f2..e8a3fb5 100644 + target.scrollTo(options); } else { - options.top = clampedOffset; -+ withReachableExtent(offset, maxOffset, animated, (reachableOffset) => { ++ scrollUntilReachable(offset, animated, (reachableOffset) => { + if (horizontal) { + options.left = reachableOffset; + } else { @@ -11365,122 +9654,21 @@ index 95465f2..e8a3fb5 100644 - target.scrollTo(options); }, - [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] -+ [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, withReachableExtent] ++ [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable] ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6550,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6128,8 +6310,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; - const endOffset = getMaxScrollOffset(); - scrollToLocalOffset(endOffset, animated); -+ scrollToLocalOffset(getCommittedMaxScrollOffset(getMaxScrollOffset()), animated); ++ scrollToLocalOffset(getMaxScrollOffset(), animated); }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -6142,7 +6563,12 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - if (!onScroll2 || !scrollRef.current) { - return; - } -- const contentSize = getContentSize2(contentRef.current); -+ const rawContentSize = getContentSize2(contentRef.current); -+ const temporaryPadding = getTemporaryEndPadding(contentRef.current, paddingEndProp); -+ const contentSize = temporaryPadding ? { -+ height: horizontal ? rawContentSize.height : rawContentSize.height - temporaryPadding, -+ width: horizontal ? rawContentSize.width - temporaryPadding : rawContentSize.width -+ } : rawContentSize; - const layoutMeasurement = getLayoutMeasurement(scrollRef.current, isWindowScroll, horizontal); - const offset = getCurrentScrollOffset(); - const scrollEvent = { -@@ -6162,7 +6588,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - } - }; - onScroll2(scrollEvent); -- }, [getCurrentScrollOffset, horizontal, isWindowScroll, onScroll2]); -+ }, [getCurrentScrollOffset, horizontal, isWindowScroll, onScroll2, paddingEndProp]); - const scrollEventCoalescer = useRafCoalescer(emitScroll); - const scrollEndFallbackRef = useRef(void 0); - const emitScrollEnd = useCallback(() => { -@@ -6358,21 +6784,6 @@ function useValueListener$(key, callback) { - } - - // src/components/ScrollAdjust.tsx --function getScrollAdjustAxis(horizontal) { -- return horizontal ? { -- contentSizeKey: "scrollWidth", -- paddingEndProp: "paddingRight", -- viewportSizeKey: "clientWidth", -- x: 1, -- y: 0 -- } : { -- contentSizeKey: "scrollHeight", -- paddingEndProp: "paddingBottom", -- viewportSizeKey: "clientHeight", -- x: 0, -- y: 1 -- }; --} - function getScrollAdjustTarget(ctx, contentNode) { - var _a3, _b, _c; - const scrollView = (_a3 = ctx.state) == null ? void 0 : _a3.refScroller.current; -@@ -6390,8 +6801,6 @@ function ScrollAdjust() { - const ctx = useStateContext(); - const lastScrollOffsetRef = React3.useRef(0); - const lastScrollAdjustUserOffsetRef = React3.useRef(0); -- const resetPaddingRafRef = React3.useRef(void 0); -- const temporaryPaddingRef = React3.useRef(void 0); - const contentNodeRef = React3.useRef(null); - const callback = React3.useCallback(() => { - const scrollAdjust = peek$(ctx, "scrollAdjust"); -@@ -6402,7 +6811,7 @@ function ScrollAdjust() { - const target = getScrollAdjustTarget(ctx, contentNodeRef.current); - if (target) { - const horizontal = !!ctx.state.props.horizontal; -- const axis = getScrollAdjustAxis(horizontal); -+ const axis = getScrollAxis(horizontal); - const { contentNode, scrollElement: el } = target; - const currentScroll = horizontal ? el.scrollLeft : el.scrollTop; - const userOffsetDelta = (scrollAdjustUserOffset || 0) - lastScrollAdjustUserOffsetRef.current; -@@ -6412,34 +6821,15 @@ function ScrollAdjust() { - const scrollBy = () => scrollAdjustBy(el, axis.x * scrollDelta, axis.y * scrollDelta); - contentNodeRef.current = contentNode; - if (shouldScroll && contentNode) { -- const totalSize = contentNode[axis.contentSizeKey]; -+ const totalSize = contentNode[axis.contentSizeKey] - getTemporaryEndPadding(contentNode, axis.paddingEndProp); - const viewportSize = el[axis.viewportSizeKey]; - const nextScroll = currentScroll + scrollDelta; - const needsTemporaryPadding = scrollDelta > 0 && !ctx.state.adjustingFromInitialMount && totalSize < nextScroll + viewportSize; - if (needsTemporaryPadding) { -- const currentInlinePaddingEnd = contentNode.style[axis.paddingEndProp]; -- const previousTemporaryPadding = temporaryPaddingRef.current; -- const baselinePaddingEnd = (previousTemporaryPadding == null ? void 0 : previousTemporaryPadding.value) === currentInlinePaddingEnd ? previousTemporaryPadding.baseline : currentInlinePaddingEnd; - const pad = (nextScroll + viewportSize - totalSize) * 2; -- const currentPaddingEnd = Number.parseFloat( -- window.getComputedStyle(contentNode)[axis.paddingEndProp] -- ); -- const temporaryPaddingEnd = `${(currentPaddingEnd || 0) + pad}px`; -- temporaryPaddingRef.current = { baseline: baselinePaddingEnd, value: temporaryPaddingEnd }; -- contentNode.style[axis.paddingEndProp] = temporaryPaddingEnd; -- void contentNode.offsetHeight; -+ const release = addTemporaryEndPadding(contentNode, axis.paddingEndProp, pad); - scrollBy(); -- if (resetPaddingRafRef.current !== void 0) { -- cancelAnimationFrame(resetPaddingRafRef.current); -- } -- resetPaddingRafRef.current = requestAnimationFrame(() => { -- const temporaryPadding = temporaryPaddingRef.current; -- resetPaddingRafRef.current = void 0; -- temporaryPaddingRef.current = void 0; -- if (contentNode.style[axis.paddingEndProp] === (temporaryPadding == null ? void 0 : temporaryPadding.value)) { -- contentNode.style[axis.paddingEndProp] = temporaryPadding.baseline; -- } -- }); -+ scheduleTemporaryEndPaddingRelease(contentNode, axis.paddingEndProp, release); - } else { - scrollBy(); - } -@@ -8267,6 +8657,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8267,6 +8448,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); From 191de70333c8f779a43fd9d89c3b7d879105c458 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 12 Aug 2026 20:04:09 -0400 Subject: [PATCH 25/38] fix(chat): apply what the reviewers found MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three reviewers went over the library fork and the app side asking for simplification. Two library findings held up and are folded into the commits they belong to. The re-issue was scoped to every non-window scroll, so the thread's own page-down — scrollToOffset(scroll + scrollLength) — asks past the end at the bottom of a thread every time, and burned up to thirty frames of redundant scrollTo on top of the reader. It now stops as soon as the extent stops growing. And ownsScrollingTo, added earlier the same day, could only ever be true: its one caller sits thirteen lines below the assignment it was checking. The desktop flow's second half could assert nothing: when the drag pushed the hit out of the render window both readings were null and neither branch fired, which is the same hole the iOS flow was already fixed for. It falls back to the top of the thread now and fails when neither can be read. Its on-screen check also accepted a one-pixel overlap where iOS requires twenty-four. Reported and deliberately not changed: closing the thread-search bar clears the center, and the desktop cleared branch then scrolls to the newest message when the thread contains it — which contradicts leaving the thread where the reader put it. It predates this branch and changing it is a UX call. --- shared/patches/@legendapp+list+3.3.5.patch | 130 +++++++++++---------- 1 file changed, 68 insertions(+), 62 deletions(-) diff --git a/shared/patches/@legendapp+list+3.3.5.patch b/shared/patches/@legendapp+list+3.3.5.patch index 2814a7b36e20..c03d4d95f655 100644 --- a/shared/patches/@legendapp+list+3.3.5.patch +++ b/shared/patches/@legendapp+list+3.3.5.patch @@ -3,7 +3,7 @@ deleted file mode 100644 index b86e710..0000000 Binary files a/node_modules/@legendapp/list/.DS_Store and /dev/null differ diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js -index b3c5a30..6d323a3 100644 +index b3c5a30..a53b48c 100644 --- a/node_modules/@legendapp/list/react-native.js +++ b/node_modules/@legendapp/list/react-native.js @@ -457,611 +457,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -1227,7 +1227,7 @@ index b3c5a30..6d323a3 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2276,6 +2105,322 @@ function scrollTo(ctx, params) { +@@ -2276,6 +2105,321 @@ function scrollTo(ctx, params) { } } @@ -1260,7 +1260,6 @@ index b3c5a30..6d323a3 100644 + expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, -+ ownsScrollingTo: state.scrollingTo !== void 0, + quietPasses: 0, + viewOffset, + viewPosition @@ -1303,7 +1302,7 @@ index b3c5a30..6d323a3 100644 + viewPosition: settle.viewPosition + }); + const scrollingTo = state.scrollingTo; -+ if (scrollingTo && settle.ownsScrollingTo) { ++ if (scrollingTo) { + const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); + scrollingTo.targetOffset = correctedOffset; + scrollingTo.offset = position; @@ -1550,7 +1549,7 @@ index b3c5a30..6d323a3 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4320,6 +4465,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4320,6 +4464,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -1558,7 +1557,7 @@ index b3c5a30..6d323a3 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4337,8 +4483,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4337,8 +4482,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -1579,7 +1578,7 @@ index b3c5a30..6d323a3 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -7652,6 +7808,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7652,6 +7807,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -1588,7 +1587,7 @@ index b3c5a30..6d323a3 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs -index 40e87cd..877e5d4 100644 +index 40e87cd..1efaf26 100644 --- a/node_modules/@legendapp/list/react-native.mjs +++ b/node_modules/@legendapp/list/react-native.mjs @@ -436,611 +436,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -2812,7 +2811,7 @@ index 40e87cd..877e5d4 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2255,6 +2084,322 @@ function scrollTo(ctx, params) { +@@ -2255,6 +2084,321 @@ function scrollTo(ctx, params) { } } @@ -2845,7 +2844,6 @@ index 40e87cd..877e5d4 100644 + expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, -+ ownsScrollingTo: state.scrollingTo !== void 0, + quietPasses: 0, + viewOffset, + viewPosition @@ -2888,7 +2886,7 @@ index 40e87cd..877e5d4 100644 + viewPosition: settle.viewPosition + }); + const scrollingTo = state.scrollingTo; -+ if (scrollingTo && settle.ownsScrollingTo) { ++ if (scrollingTo) { + const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); + scrollingTo.targetOffset = correctedOffset; + scrollingTo.offset = position; @@ -3135,7 +3133,7 @@ index 40e87cd..877e5d4 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4299,6 +4444,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4299,6 +4443,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -3143,7 +3141,7 @@ index 40e87cd..877e5d4 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4316,8 +4462,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4316,8 +4461,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -3164,7 +3162,7 @@ index 40e87cd..877e5d4 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -7631,6 +7787,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7631,6 +7786,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -3173,7 +3171,7 @@ index 40e87cd..877e5d4 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js -index 914d2da..eed3566 100644 +index 914d2da..8a87d1e 100644 --- a/node_modules/@legendapp/list/react-native.web.js +++ b/node_modules/@legendapp/list/react-native.web.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -4362,7 +4360,7 @@ index 914d2da..eed3566 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2121,6 +1944,328 @@ function scrollTo(ctx, params) { +@@ -2121,6 +1944,327 @@ function scrollTo(ctx, params) { } } @@ -4395,7 +4393,6 @@ index 914d2da..eed3566 100644 + expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, -+ ownsScrollingTo: state.scrollingTo !== void 0, + quietPasses: 0, + viewOffset, + viewPosition @@ -4438,7 +4435,7 @@ index 914d2da..eed3566 100644 + viewPosition: settle.viewPosition + }); + const scrollingTo = state.scrollingTo; -+ if (scrollingTo && settle.ownsScrollingTo) { ++ if (scrollingTo) { + const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); + scrollingTo.targetOffset = correctedOffset; + scrollingTo.offset = position; @@ -4691,7 +4688,7 @@ index 914d2da..eed3566 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4350,6 +4495,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4350,6 +4494,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -4699,7 +4696,7 @@ index 914d2da..eed3566 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4367,8 +4513,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4367,8 +4512,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -4720,7 +4717,7 @@ index 914d2da..eed3566 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -6006,6 +6162,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -6006,6 +6161,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; @@ -4729,7 +4726,7 @@ index 914d2da..eed3566 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6094,6 +6252,23 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,6 +6251,26 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -4738,10 +4735,13 @@ index 914d2da..eed3566 100644 + (offset, animated, run) => { + cancelAnimationFrame(reissueHandleRef.current); + let attempts = 0; ++ let previousMaxOffset = Number.NEGATIVE_INFINITY; + const attempt = () => { + const liveMaxOffset = getMaxScrollOffset(); + run(clampOffset(offset, liveMaxOffset)); -+ if (!animated && Number.isFinite(offset) && offset - liveMaxOffset > SCROLL_EXTENT_EPSILON && ++attempts < REACHABLE_RETRY_FRAMES) { ++ const isStillCommitting = liveMaxOffset > previousMaxOffset; ++ previousMaxOffset = liveMaxOffset; ++ if (!animated && isStillCommitting && Number.isFinite(offset) && offset - liveMaxOffset > SCROLL_EXTENT_EPSILON && ++attempts < REACHABLE_RETRY_FRAMES) { + reissueHandleRef.current = requestAnimationFrame(attempt); + } + }; @@ -4753,7 +4753,7 @@ index 914d2da..eed3566 100644 const scrollToLocalOffset = React3.useCallback( (offset, animated) => { const scrollElement = scrollRef.current; -@@ -6116,14 +6291,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6116,14 +6293,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); options.left = left; options.top = top; @@ -4780,7 +4780,7 @@ index 914d2da..eed3566 100644 ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6331,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6149,8 +6333,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -4790,7 +4790,7 @@ index 914d2da..eed3566 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -8288,6 +8469,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8288,6 +8471,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -4799,7 +4799,7 @@ index 914d2da..eed3566 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs -index 95465f2..31c9d25 100644 +index 95465f2..48d7741 100644 --- a/node_modules/@legendapp/list/react-native.web.mjs +++ b/node_modules/@legendapp/list/react-native.web.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -5988,7 +5988,7 @@ index 95465f2..31c9d25 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2100,6 +1923,328 @@ function scrollTo(ctx, params) { +@@ -2100,6 +1923,327 @@ function scrollTo(ctx, params) { } } @@ -6021,7 +6021,6 @@ index 95465f2..31c9d25 100644 + expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, -+ ownsScrollingTo: state.scrollingTo !== void 0, + quietPasses: 0, + viewOffset, + viewPosition @@ -6064,7 +6063,7 @@ index 95465f2..31c9d25 100644 + viewPosition: settle.viewPosition + }); + const scrollingTo = state.scrollingTo; -+ if (scrollingTo && settle.ownsScrollingTo) { ++ if (scrollingTo) { + const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); + scrollingTo.targetOffset = correctedOffset; + scrollingTo.offset = position; @@ -6317,7 +6316,7 @@ index 95465f2..31c9d25 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4329,6 +4474,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4329,6 +4473,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -6325,7 +6324,7 @@ index 95465f2..31c9d25 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4346,8 +4492,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4346,8 +4491,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -6346,7 +6345,7 @@ index 95465f2..31c9d25 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5985,6 +6141,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -5985,6 +6140,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; @@ -6355,7 +6354,7 @@ index 95465f2..31c9d25 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6073,6 +6231,23 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,6 +6230,26 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -6364,10 +6363,13 @@ index 95465f2..31c9d25 100644 + (offset, animated, run) => { + cancelAnimationFrame(reissueHandleRef.current); + let attempts = 0; ++ let previousMaxOffset = Number.NEGATIVE_INFINITY; + const attempt = () => { + const liveMaxOffset = getMaxScrollOffset(); + run(clampOffset(offset, liveMaxOffset)); -+ if (!animated && Number.isFinite(offset) && offset - liveMaxOffset > SCROLL_EXTENT_EPSILON && ++attempts < REACHABLE_RETRY_FRAMES) { ++ const isStillCommitting = liveMaxOffset > previousMaxOffset; ++ previousMaxOffset = liveMaxOffset; ++ if (!animated && isStillCommitting && Number.isFinite(offset) && offset - liveMaxOffset > SCROLL_EXTENT_EPSILON && ++attempts < REACHABLE_RETRY_FRAMES) { + reissueHandleRef.current = requestAnimationFrame(attempt); + } + }; @@ -6379,7 +6381,7 @@ index 95465f2..31c9d25 100644 const scrollToLocalOffset = useCallback( (offset, animated) => { const scrollElement = scrollRef.current; -@@ -6095,14 +6270,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6095,14 +6272,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); options.left = left; options.top = top; @@ -6406,7 +6408,7 @@ index 95465f2..31c9d25 100644 ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6310,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6128,8 +6312,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -6416,7 +6418,7 @@ index 95465f2..31c9d25 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -8267,6 +8448,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8267,6 +8450,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -6425,7 +6427,7 @@ index 95465f2..31c9d25 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js -index 914d2da..eed3566 100644 +index 914d2da..8a87d1e 100644 --- a/node_modules/@legendapp/list/react.js +++ b/node_modules/@legendapp/list/react.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -7614,7 +7616,7 @@ index 914d2da..eed3566 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2121,6 +1944,328 @@ function scrollTo(ctx, params) { +@@ -2121,6 +1944,327 @@ function scrollTo(ctx, params) { } } @@ -7647,7 +7649,6 @@ index 914d2da..eed3566 100644 + expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, -+ ownsScrollingTo: state.scrollingTo !== void 0, + quietPasses: 0, + viewOffset, + viewPosition @@ -7690,7 +7691,7 @@ index 914d2da..eed3566 100644 + viewPosition: settle.viewPosition + }); + const scrollingTo = state.scrollingTo; -+ if (scrollingTo && settle.ownsScrollingTo) { ++ if (scrollingTo) { + const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); + scrollingTo.targetOffset = correctedOffset; + scrollingTo.offset = position; @@ -7943,7 +7944,7 @@ index 914d2da..eed3566 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4350,6 +4495,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4350,6 +4494,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -7951,7 +7952,7 @@ index 914d2da..eed3566 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4367,8 +4513,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4367,8 +4512,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -7972,7 +7973,7 @@ index 914d2da..eed3566 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -6006,6 +6162,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -6006,6 +6161,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; @@ -7981,7 +7982,7 @@ index 914d2da..eed3566 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6094,6 +6252,23 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,6 +6251,26 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -7990,10 +7991,13 @@ index 914d2da..eed3566 100644 + (offset, animated, run) => { + cancelAnimationFrame(reissueHandleRef.current); + let attempts = 0; ++ let previousMaxOffset = Number.NEGATIVE_INFINITY; + const attempt = () => { + const liveMaxOffset = getMaxScrollOffset(); + run(clampOffset(offset, liveMaxOffset)); -+ if (!animated && Number.isFinite(offset) && offset - liveMaxOffset > SCROLL_EXTENT_EPSILON && ++attempts < REACHABLE_RETRY_FRAMES) { ++ const isStillCommitting = liveMaxOffset > previousMaxOffset; ++ previousMaxOffset = liveMaxOffset; ++ if (!animated && isStillCommitting && Number.isFinite(offset) && offset - liveMaxOffset > SCROLL_EXTENT_EPSILON && ++attempts < REACHABLE_RETRY_FRAMES) { + reissueHandleRef.current = requestAnimationFrame(attempt); + } + }; @@ -8005,7 +8009,7 @@ index 914d2da..eed3566 100644 const scrollToLocalOffset = React3.useCallback( (offset, animated) => { const scrollElement = scrollRef.current; -@@ -6116,14 +6291,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6116,14 +6293,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }); options.left = left; options.top = top; @@ -8032,7 +8036,7 @@ index 914d2da..eed3566 100644 ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6331,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6149,8 +6333,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -8042,7 +8046,7 @@ index 914d2da..eed3566 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -8288,6 +8469,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8288,6 +8471,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -8051,7 +8055,7 @@ index 914d2da..eed3566 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs -index 95465f2..31c9d25 100644 +index 95465f2..48d7741 100644 --- a/node_modules/@legendapp/list/react.mjs +++ b/node_modules/@legendapp/list/react.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -9240,7 +9244,7 @@ index 95465f2..31c9d25 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2100,6 +1923,328 @@ function scrollTo(ctx, params) { +@@ -2100,6 +1923,327 @@ function scrollTo(ctx, params) { } } @@ -9273,7 +9277,6 @@ index 95465f2..31c9d25 100644 + expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, -+ ownsScrollingTo: state.scrollingTo !== void 0, + quietPasses: 0, + viewOffset, + viewPosition @@ -9316,7 +9319,7 @@ index 95465f2..31c9d25 100644 + viewPosition: settle.viewPosition + }); + const scrollingTo = state.scrollingTo; -+ if (scrollingTo && settle.ownsScrollingTo) { ++ if (scrollingTo) { + const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); + scrollingTo.targetOffset = correctedOffset; + scrollingTo.offset = position; @@ -9569,7 +9572,7 @@ index 95465f2..31c9d25 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4329,6 +4474,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4329,6 +4473,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -9577,7 +9580,7 @@ index 95465f2..31c9d25 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4346,8 +4492,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4346,8 +4491,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -9598,7 +9601,7 @@ index 95465f2..31c9d25 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5985,6 +6141,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -5985,6 +6140,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; @@ -9607,7 +9610,7 @@ index 95465f2..31c9d25 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6073,6 +6231,23 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,6 +6230,26 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -9616,10 +9619,13 @@ index 95465f2..31c9d25 100644 + (offset, animated, run) => { + cancelAnimationFrame(reissueHandleRef.current); + let attempts = 0; ++ let previousMaxOffset = Number.NEGATIVE_INFINITY; + const attempt = () => { + const liveMaxOffset = getMaxScrollOffset(); + run(clampOffset(offset, liveMaxOffset)); -+ if (!animated && Number.isFinite(offset) && offset - liveMaxOffset > SCROLL_EXTENT_EPSILON && ++attempts < REACHABLE_RETRY_FRAMES) { ++ const isStillCommitting = liveMaxOffset > previousMaxOffset; ++ previousMaxOffset = liveMaxOffset; ++ if (!animated && isStillCommitting && Number.isFinite(offset) && offset - liveMaxOffset > SCROLL_EXTENT_EPSILON && ++attempts < REACHABLE_RETRY_FRAMES) { + reissueHandleRef.current = requestAnimationFrame(attempt); + } + }; @@ -9631,7 +9637,7 @@ index 95465f2..31c9d25 100644 const scrollToLocalOffset = useCallback( (offset, animated) => { const scrollElement = scrollRef.current; -@@ -6095,14 +6270,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6095,14 +6272,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }); options.left = left; options.top = top; @@ -9658,7 +9664,7 @@ index 95465f2..31c9d25 100644 ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6310,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6128,8 +6312,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -9668,7 +9674,7 @@ index 95465f2..31c9d25 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -8267,6 +8448,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8267,6 +8450,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); From d1911faf4e8974a42bdf4fda3dd7fed6871ed932 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 12 Aug 2026 20:05:34 -0400 Subject: [PATCH 26/38] test(e2e): make the desktop search flow able to fail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two holes the reviewers found, and the previous commit claimed these were fixed when the edit had silently not applied. The second half could assert nothing. The drag before it exists to push the hit out of the viewport, and far enough out the row unmounts — at which point both readings were null, neither branch fired, and the test went green having checked nothing. That is the same vacuity the iOS flow was already fixed for. It falls back to the top of the thread, which is mounted in every state, and fails when neither can be read. The on-screen check also passed for a row overlapping the viewport by a single pixel. It requires the same 24px the iOS flow does. --- .../electron/flows/chat-search-hit.test.ts | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/shared/tests/e2e/electron/flows/chat-search-hit.test.ts b/shared/tests/e2e/electron/flows/chat-search-hit.test.ts index 46a6fe43d1e8..f44c27d4846a 100644 --- a/shared/tests/e2e/electron/flows/chat-search-hit.test.ts +++ b/shared/tests/e2e/electron/flows/chat-search-hit.test.ts @@ -9,6 +9,9 @@ import * as T from '@/tests/e2e/shared/test-ids' // One test rather than two, because the second half depends on the state the first leaves: the // search bar is open and a hit is selected, and re-entering that from scratch would just be the // first half again. +// A row has to be visible by more than a hairline to count as landed on, matching the iOS flow. +const MIN_VISIBLE_HEIGHT = 24 + test('lands on every search hit, then stays where the reader scrolls it', async ({page}) => { test.setTimeout(120_000) // Named, not "whichever row is first". The inbox is ordered by recency and the suites send @@ -46,7 +49,10 @@ test('lands on every search hit, then stays where the reader scrolls it', async const list = await page.getByTestId(T.CHAT_MESSAGE_LIST).first().boundingBox() if (!hit || !list) continue checked++ - const onScreen = hit.y + hit.height > list.y && hit.y < list.y + list.height + // By more than a hairline: a row overlapping the viewport by a pixel has not "landed on the + // hit", and the iOS flow holds the same line. + const visibleHeight = Math.min(hit.y + hit.height, list.y + list.height) - Math.max(hit.y, list.y) + const onScreen = visibleHeight >= Math.min(hit.height, MIN_VISIBLE_HEIGHT) expect( onScreen, `step ${i}: hit at y=${Math.round(hit.y)} h=${Math.round(hit.height)} is outside the list (y=${Math.round(list.y)} h=${Math.round(list.height)})` @@ -73,9 +79,22 @@ test('lands on every search hit, then stays where the reader scrolls it', async // Watch rather than look once: a re-centre lands whenever the rows scrolled past finish // measuring, which is after the gesture rather than during it. + // The row is often scrolled clean out of the render window, so its position is not always + // readable. The top of the thread is, in every state — without a second reading like it, both + // branches below can be skipped and this half of the test asserts nothing at all. + const readThreadTop = async () => + page + .getByTestId(T.CHAT_THREAD_TOP) + .first() + .boundingBox({timeout: 3_000}) + .then(b => (b ? b.y : null)) + .catch(() => null) + const settled = await readHit() + const settledTop = await readThreadTop() await page.waitForTimeout(3_000) const after = await readHit() + const afterTop = await readThreadTop() if (settled && after) { const moved = Math.abs(after.y - settled.y) @@ -83,5 +102,10 @@ test('lands on every search hit, then stays where the reader scrolls it', async } else if (!settled && after) { // Scrolled far enough to unmount the row, and then it came back — which only a scroll does. throw new Error('the hit came back into the render window after being scrolled away from') + } else if (settledTop !== null && afterTop !== null) { + const moved = Math.abs(afterTop - settledTop) + expect(moved, `the thread moved ${Math.round(moved)}px on its own after the drag`).toBeLessThanOrEqual(8) + } else { + throw new Error('neither the hit nor the top of the thread could be measured, so nothing was checked') } }) From 2cbbf2bc78d39353829b6c354bb94ec71099dfcb Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 12 Aug 2026 21:53:28 -0400 Subject: [PATCH 27/38] fix(chat): one centred-scroll hook for both platforms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Desktop and native ran the same algorithm twice, down to a six-line comment duplicated near-verbatim, differing only in how they looked the ordinal up and which scroll call they made. Both refs point at the same list type, and it exposes scrollToItem, so the lookup and the index-vs-item split both go away. That removes a real defect with them. Closing the thread-search bar clears the centre, and the desktop copy read a cleared centre as "go to the newest message" whenever the thread contained it — so a reader who searched, landed on a hit and cancelled the bar got yanked to the bottom, which is the behaviour the flows exist to forbid. Native never had that branch, and now neither does desktop. Also gone: the native sentinel ordinal that existed so the value could be compared against -1 instead of undefined, and a ref-plus-stable-closure pair whose comment said it kept the calling effect from re-running on every centred-ordinal change — that effect listed the ordinal in its own dependencies, so it re-ran regardless. The library patch loses the settle's time to live, which was refreshed on every pass that could act and so could only elapse on a pass that had already returned without doing anything; the hard deadline is what ends an idle one. --- shared/chat/conversation/list-area/index.tsx | 125 +++--- shared/patches/@legendapp+list+3.3.5.patch | 382 +++++++++---------- 2 files changed, 221 insertions(+), 286 deletions(-) diff --git a/shared/chat/conversation/list-area/index.tsx b/shared/chat/conversation/list-area/index.tsx index c4b88172978f..14194b30f21f 100644 --- a/shared/chat/conversation/list-area/index.tsx +++ b/shared/chat/conversation/list-area/index.tsx @@ -1,7 +1,7 @@ import * as C from '@/constants' import * as Kb from '@/common-adapters' import * as React from 'react' -import * as T from '@/constants/types' +import type * as T from '@/constants/types' import * as TestIDs from '@/tests/e2e/shared/test-ids' import Separator from '../messages/separator' import SpecialBottomMessage from '../messages/special-bottom-message' @@ -217,6 +217,44 @@ const HighlightableRow = React.memo(({ordinal}: {ordinal: T.Chat.Ordinal}) => { }) HighlightableRow.displayName = 'HighlightableRow' +// Sending the list to the centered ordinal: a search hit, a reply-quote jump, a pinned message. +// +// Centring on the raw ordinal change is unreliable: navigating to a hit reloads the thread centred +// on it, so the target is briefly absent from messageOrdinals when the ordinal changes. Wait for it +// to arrive, then scroll once per target and no more. Re-issuing when the target's index moves looks +// reasonable - a prepend does shift it - but scrolling is what triggers that prepend, so it would +// re-centre the list out from under someone reading around the hit. The list holds the target in +// place while rows measure, and maintainVisibleContentPosition holds it across prepends. +// +// Reset per dataset rather than per conversation: re-centring on the ordinal already stored still +// clears and reloads the thread, so the list has to be sent to it again. +const useScrollToCentered = (p: { + centeredOrdinal: T.Chat.Ordinal | undefined + datasetKey: string + listRef: React.RefObject + messageOrdinals: ReadonlyArray + ready: boolean +}) => { + const {centeredOrdinal, datasetKey, listRef, messageOrdinals, ready} = p + const lastScrolledRef = React.useRef(undefined) + React.useLayoutEffect(() => { + lastScrolledRef.current = undefined + }, [datasetKey]) + + React.useEffect(() => { + if (!ready || centeredOrdinal === undefined) { + lastScrolledRef.current = undefined + return + } + if (lastScrolledRef.current === centeredOrdinal) return + if (sortedIndexOf(messageOrdinals as unknown as number[], centeredOrdinal as unknown as number) < 0) { + return + } + lastScrolledRef.current = centeredOrdinal + void listRef.current?.scrollToItem({animated: false, item: centeredOrdinal, viewPosition: 0.5}) + }, [centeredOrdinal, datasetKey, listRef, messageOrdinals, ready]) +} + const DesktopThreadWrapper = function DesktopThreadWrapper() { const desktopStyles = useDesktopStyles() const editingOrdinal = InputState.useConversationInput(s => s.editing) @@ -326,38 +364,7 @@ const DesktopThreadWrapper = function DesktopThreadWrapper() { [onScroll] ) - // Scroll to centered ordinal when it changes (search / thread navigation). - // Use a "last scrolled to" ref rather than a "did it change" ref so we still - // scroll when loaded becomes true after centeredOrdinal was already set. - // Reset per dataset, not per conversation: re-centering on the ordinal we are already parked - // on still reloads the thread, so the list has to scroll to it again. - // Scrolls once per target and no more. Re-issuing when the target's index moves looks reasonable - // - a prepend does shift it - but scrolling is what triggers that prepend, so it re-centers the - // list out from under someone reading around the hit. The list itself keeps the target in place - // while rows measure, and maintainVisibleContentPosition holds it across prepends. - const lastScrolledCenteredRef = React.useRef(undefined) - React.useLayoutEffect(() => { - lastScrolledCenteredRef.current = undefined - }, [datasetKey]) - - React.useEffect(() => { - if (!loaded) return - if (centeredOrdinal !== undefined) { - const idx = sortedIndexOf( - messageOrdinalsRef.current as unknown as number[], - centeredOrdinal as unknown as number - ) - if (idx < 0) return - if (lastScrolledCenteredRef.current === centeredOrdinal) return - lastScrolledCenteredRef.current = centeredOrdinal - void listRef.current?.scrollToIndex({animated: false, index: idx, viewPosition: 0.5}) - } else if (lastScrolledCenteredRef.current !== undefined) { - lastScrolledCenteredRef.current = undefined - if (containsLatestMessage) { - void listRef.current?.scrollToEnd({animated: false}) - } - } - }, [centeredOrdinal, loaded, containsLatestMessage, messageOrdinals]) + useScrollToCentered({centeredOrdinal, datasetKey, listRef, messageOrdinals, ready: loaded}) // Scroll to the message being edited const lastEditingOrdinalRef = React.useRef(undefined) @@ -549,11 +556,9 @@ const DesktopThreadWrapperWithProfiler = () => ( // ==================== NATIVE ==================== const useNativeScrolling = (p: { - centeredOrdinal: T.Chat.Ordinal - listRef: React.RefObject scrollMessageToEnd: (o: {animated: boolean; closeKeyboard: boolean}) => Promise }) => { - const {listRef, centeredOrdinal, scrollMessageToEnd} = p + const {scrollMessageToEnd} = p // scrollMessageToEnd freezes the keyboard-aware scroll view, scrolls to the end, // then unfreezes — so the newest message stays pinned above the input bar even @@ -567,20 +572,8 @@ const useNativeScrolling = (p: { setScrollRef({scrollDown: noop, scrollToBottom, scrollUp: noop}) }, [setScrollRef, scrollToBottom]) - const centeredOrdinalRef = React.useRef(centeredOrdinal) - React.useEffect(() => { - centeredOrdinalRef.current = centeredOrdinal - }, [centeredOrdinal]) - // Stable so the effect that calls it does not re-run on every centeredOrdinal change. - const [scrollToCentered] = React.useState(() => () => { - const co = centeredOrdinalRef.current - if (T.Chat.ordinalToNumber(co) < 0) return - void listRef.current?.scrollToItem({animated: false, item: co, viewPosition: 0.5}) - }) - return { scrollToBottom, - scrollToCentered, } } @@ -603,8 +596,6 @@ const NativeConversationList = function NativeConversationList() { const conversationIDKey = useConversationThreadID() const listData = useThreadListData() const {centeredOrdinal} = useConversationCenter() - const noCenteredOrdinal = T.Chat.numberToOrdinal(-1) - const centeredOrdinalOrNone = centeredOrdinal ?? noCenteredOrdinal const {clearVersion, loaded, containsLatestMessage, messageOrdinals} = listData // Same reason as desktop: a centered load empties the thread before refilling it, and the list // needs to be told that is a new dataset rather than left waiting on layout for rows it already @@ -656,41 +647,11 @@ const NativeConversationList = function NativeConversationList() { const {freeze, scrollMessageToEnd} = useKeyboardScrollToEnd({listRef}) - const {scrollToCentered, scrollToBottom} = useNativeScrolling({ - centeredOrdinal: centeredOrdinalOrNone, - listRef, - scrollMessageToEnd, - }) + const {scrollToBottom} = useNativeScrolling({scrollMessageToEnd}) const jumpToRecent = useJumpToRecent(scrollToBottom, messageOrdinals.length) - // Center on the search hit once it actually appears in the loaded list. Centering on the raw - // centeredOrdinal change is unreliable: navigating to a hit reloads the thread centered on it, - // so messageOrdinals is briefly empty (the target not yet present) when the ordinal changes. - // Scrolls once per target and no more. Re-issuing when the target's index moves looks reasonable - // - a prepend does shift it - but scrolling up is what triggers that prepend, so it re-centers - // the list out from under someone reading around the hit. The list itself keeps the target in - // place while rows measure, and maintainVisibleContentPosition holds it across prepends. - const lastCenteredOrdinal = React.useRef(undefined) - // Reset per dataset, not per conversation: re-centering on the ordinal already stored still - // clears and reloads the thread, so the list has to be sent to it again. - React.useLayoutEffect(() => { - lastCenteredOrdinal.current = undefined - }, [datasetKey]) - React.useEffect(() => { - if (centeredOrdinalOrNone <= 0) { - lastCenteredOrdinal.current = undefined - return - } - if (lastCenteredOrdinal.current === centeredOrdinalOrNone) { - return - } - if (!messageOrdinals.includes(centeredOrdinalOrNone)) { - return - } - lastCenteredOrdinal.current = centeredOrdinalOrNone - scrollToCentered() - }, [centeredOrdinalOrNone, messageOrdinals, scrollToCentered]) + useScrollToCentered({centeredOrdinal, datasetKey, listRef, messageOrdinals, ready: true}) // These refs store the conversation they last applied to (not a boolean) so a // freeze/thaw of this screen — which re-mounts effects without a real diff --git a/shared/patches/@legendapp+list+3.3.5.patch b/shared/patches/@legendapp+list+3.3.5.patch index c03d4d95f655..afb4ee294062 100644 --- a/shared/patches/@legendapp+list+3.3.5.patch +++ b/shared/patches/@legendapp+list+3.3.5.patch @@ -3,7 +3,7 @@ deleted file mode 100644 index b86e710..0000000 Binary files a/node_modules/@legendapp/list/.DS_Store and /dev/null differ diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js -index b3c5a30..a53b48c 100644 +index b3c5a30..7f3669a 100644 --- a/node_modules/@legendapp/list/react-native.js +++ b/node_modules/@legendapp/list/react-native.js @@ -457,611 +457,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -1147,20 +1147,10 @@ index b3c5a30..a53b48c 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1972,11 +1770,32 @@ function prepareMVCP(ctx, dataChanged) { - } - } +@@ -1977,6 +1775,27 @@ var flushSync = (fn) => { + fn(); + }; --// src/platform/flushSync.native.ts --var flushSync = (fn) => { -- fn(); --}; -- -+// src/platform/flushSync.native.ts -+var flushSync = (fn) => { -+ fn(); -+}; -+ +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { @@ -1227,14 +1217,13 @@ index b3c5a30..a53b48c 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2276,6 +2105,321 @@ function scrollTo(ctx, params) { +@@ -2276,6 +2105,310 @@ function scrollTo(ctx, params) { } } +// src/core/scrollTargetSettle.ts +var SETTLE_POSITION_EPSILON = 1; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; -+var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; +function clearScrollTargetSettle(state) { @@ -1257,7 +1246,6 @@ index b3c5a30..a53b48c 100644 + state.scrollTargetSettle = { + corrections: 0, + deadline: now + SETTLE_MAX_MS, -+ expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, + quietPasses: 0, @@ -1303,8 +1291,7 @@ index b3c5a30..a53b48c 100644 + }); + const scrollingTo = state.scrollingTo; + if (scrollingTo) { -+ const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); -+ scrollingTo.targetOffset = correctedOffset; ++ scrollingTo.targetOffset = state.scrollPending; + scrollingTo.offset = position; + } + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); @@ -1319,10 +1306,6 @@ index b3c5a30..a53b48c 100644 + const measured = options == null ? void 0 : options.minIndexSizeChanged; + if (measured !== void 0) { + settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); -+ const targetIndex = state.indexByKey.get(settle.id); -+ if (targetIndex !== void 0 && measured <= targetIndex) { -+ settle.expiresAt = Date.now() + SETTLE_TTL_MS; -+ } + } + if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { @@ -1330,7 +1313,6 @@ index b3c5a30..a53b48c 100644 + return false; + } + settle.quietPasses = 0; -+ settle.expiresAt = Date.now() + SETTLE_TTL_MS; + return false; + } + const index = state.indexByKey.get(settle.id); @@ -1339,8 +1321,7 @@ index b3c5a30..a53b48c 100644 + clearScrollTargetSettle(state); + return false; + } -+ const now = Date.now(); -+ if (now > settle.expiresAt || now > settle.deadline) { ++ if (Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return false; + } @@ -1348,8 +1329,7 @@ index b3c5a30..a53b48c 100644 + return false; + } + const targetOffset = getSettleTargetOffset(ctx, settle, index, position); -+ const diff = targetOffset - state.scroll; -+ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ if (Math.abs(targetOffset - state.scroll) <= SETTLE_POSITION_EPSILON) { + settle.measuredIndex = void 0; + settle.quietPasses++; + if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { @@ -1358,7 +1338,6 @@ index b3c5a30..a53b48c 100644 + return false; + } + settle.quietPasses = 0; -+ settle.expiresAt = now + SETTLE_TTL_MS; + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} @@ -1549,7 +1528,7 @@ index b3c5a30..a53b48c 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4320,6 +4464,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4320,6 +4453,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -1557,7 +1536,7 @@ index b3c5a30..a53b48c 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4337,8 +4482,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4337,8 +4471,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -1578,7 +1557,7 @@ index b3c5a30..a53b48c 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -7652,6 +7807,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7652,6 +7796,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -1587,7 +1566,7 @@ index b3c5a30..a53b48c 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs -index 40e87cd..1efaf26 100644 +index 40e87cd..2b216b4 100644 --- a/node_modules/@legendapp/list/react-native.mjs +++ b/node_modules/@legendapp/list/react-native.mjs @@ -436,611 +436,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -2731,20 +2710,10 @@ index 40e87cd..1efaf26 100644 requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1951,11 +1749,32 @@ function prepareMVCP(ctx, dataChanged) { - } - } +@@ -1956,6 +1754,27 @@ var flushSync = (fn) => { + fn(); + }; --// src/platform/flushSync.native.ts --var flushSync = (fn) => { -- fn(); --}; -- -+// src/platform/flushSync.native.ts -+var flushSync = (fn) => { -+ fn(); -+}; -+ +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { @@ -2811,14 +2780,13 @@ index 40e87cd..1efaf26 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2255,6 +2084,321 @@ function scrollTo(ctx, params) { +@@ -2255,6 +2084,310 @@ function scrollTo(ctx, params) { } } +// src/core/scrollTargetSettle.ts +var SETTLE_POSITION_EPSILON = 1; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; -+var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; +function clearScrollTargetSettle(state) { @@ -2841,7 +2809,6 @@ index 40e87cd..1efaf26 100644 + state.scrollTargetSettle = { + corrections: 0, + deadline: now + SETTLE_MAX_MS, -+ expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, + quietPasses: 0, @@ -2887,8 +2854,7 @@ index 40e87cd..1efaf26 100644 + }); + const scrollingTo = state.scrollingTo; + if (scrollingTo) { -+ const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); -+ scrollingTo.targetOffset = correctedOffset; ++ scrollingTo.targetOffset = state.scrollPending; + scrollingTo.offset = position; + } + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); @@ -2903,10 +2869,6 @@ index 40e87cd..1efaf26 100644 + const measured = options == null ? void 0 : options.minIndexSizeChanged; + if (measured !== void 0) { + settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); -+ const targetIndex = state.indexByKey.get(settle.id); -+ if (targetIndex !== void 0 && measured <= targetIndex) { -+ settle.expiresAt = Date.now() + SETTLE_TTL_MS; -+ } + } + if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { @@ -2914,7 +2876,6 @@ index 40e87cd..1efaf26 100644 + return false; + } + settle.quietPasses = 0; -+ settle.expiresAt = Date.now() + SETTLE_TTL_MS; + return false; + } + const index = state.indexByKey.get(settle.id); @@ -2923,8 +2884,7 @@ index 40e87cd..1efaf26 100644 + clearScrollTargetSettle(state); + return false; + } -+ const now = Date.now(); -+ if (now > settle.expiresAt || now > settle.deadline) { ++ if (Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return false; + } @@ -2932,8 +2892,7 @@ index 40e87cd..1efaf26 100644 + return false; + } + const targetOffset = getSettleTargetOffset(ctx, settle, index, position); -+ const diff = targetOffset - state.scroll; -+ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ if (Math.abs(targetOffset - state.scroll) <= SETTLE_POSITION_EPSILON) { + settle.measuredIndex = void 0; + settle.quietPasses++; + if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { @@ -2942,7 +2901,6 @@ index 40e87cd..1efaf26 100644 + return false; + } + settle.quietPasses = 0; -+ settle.expiresAt = now + SETTLE_TTL_MS; + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} @@ -3133,7 +3091,7 @@ index 40e87cd..1efaf26 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4299,6 +4443,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4299,6 +4432,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -3141,7 +3099,7 @@ index 40e87cd..1efaf26 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4316,8 +4461,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4316,8 +4450,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -3162,7 +3120,7 @@ index 40e87cd..1efaf26 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -7631,6 +7786,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7631,6 +7775,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -3171,7 +3129,7 @@ index 40e87cd..1efaf26 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js -index 914d2da..8a87d1e 100644 +index 914d2da..a91aa0a 100644 --- a/node_modules/@legendapp/list/react-native.web.js +++ b/node_modules/@legendapp/list/react-native.web.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -4360,14 +4318,13 @@ index 914d2da..8a87d1e 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2121,6 +1944,327 @@ function scrollTo(ctx, params) { +@@ -2121,6 +1944,316 @@ function scrollTo(ctx, params) { } } +// src/core/scrollTargetSettle.ts +var SETTLE_POSITION_EPSILON = 1; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; -+var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; +function clearScrollTargetSettle(state) { @@ -4390,7 +4347,6 @@ index 914d2da..8a87d1e 100644 + state.scrollTargetSettle = { + corrections: 0, + deadline: now + SETTLE_MAX_MS, -+ expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, + quietPasses: 0, @@ -4436,8 +4392,7 @@ index 914d2da..8a87d1e 100644 + }); + const scrollingTo = state.scrollingTo; + if (scrollingTo) { -+ const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); -+ scrollingTo.targetOffset = correctedOffset; ++ scrollingTo.targetOffset = state.scrollPending; + scrollingTo.offset = position; + } + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); @@ -4452,10 +4407,6 @@ index 914d2da..8a87d1e 100644 + const measured = options == null ? void 0 : options.minIndexSizeChanged; + if (measured !== void 0) { + settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); -+ const targetIndex = state.indexByKey.get(settle.id); -+ if (targetIndex !== void 0 && measured <= targetIndex) { -+ settle.expiresAt = Date.now() + SETTLE_TTL_MS; -+ } + } + if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { @@ -4463,7 +4414,6 @@ index 914d2da..8a87d1e 100644 + return false; + } + settle.quietPasses = 0; -+ settle.expiresAt = Date.now() + SETTLE_TTL_MS; + return false; + } + const index = state.indexByKey.get(settle.id); @@ -4472,8 +4422,7 @@ index 914d2da..8a87d1e 100644 + clearScrollTargetSettle(state); + return false; + } -+ const now = Date.now(); -+ if (now > settle.expiresAt || now > settle.deadline) { ++ if (Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return false; + } @@ -4481,8 +4430,7 @@ index 914d2da..8a87d1e 100644 + return false; + } + const targetOffset = getSettleTargetOffset(ctx, settle, index, position); -+ const diff = targetOffset - state.scroll; -+ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ if (Math.abs(targetOffset - state.scroll) <= SETTLE_POSITION_EPSILON) { + settle.measuredIndex = void 0; + settle.quietPasses++; + if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { @@ -4491,7 +4439,6 @@ index 914d2da..8a87d1e 100644 + return false; + } + settle.quietPasses = 0; -+ settle.expiresAt = now + SETTLE_TTL_MS; + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} @@ -4688,7 +4635,7 @@ index 914d2da..8a87d1e 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4350,6 +4494,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4350,6 +4483,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -4696,7 +4643,7 @@ index 914d2da..8a87d1e 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4367,8 +4512,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4367,8 +4501,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -4717,7 +4664,7 @@ index 914d2da..8a87d1e 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -6006,6 +6161,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -6006,6 +6150,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; @@ -4726,7 +4673,7 @@ index 914d2da..8a87d1e 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6094,6 +6251,26 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,6 +6240,26 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -4753,34 +4700,49 @@ index 914d2da..8a87d1e 100644 const scrollToLocalOffset = React3.useCallback( (offset, animated) => { const scrollElement = scrollRef.current; -@@ -6116,14 +6293,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6101,29 +6267,32 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + if (!target || typeof target.scrollTo !== "function") { + return; + } +- const maxOffset = getMaxScrollOffset(); +- const clampedOffset = clampOffset(offset, maxOffset); + const behavior = animated ? "smooth" : "auto"; + const options = { behavior }; + if (isWindowScroll) { + const scroll = getWindowScrollPosition(); + const listPos = getElementDocumentPosition(scrollElement, scroll); + const { left, top } = resolveWindowScrollTarget({ +- clampedOffset, ++ clampedOffset: clampOffset(offset, getMaxScrollOffset()), + horizontal, + listPos, + scroll }); options.left = left; options.top = top; - } else if (horizontal) { - options.left = clampedOffset; -+ } -+ if (isWindowScroll) { -+ target.scrollTo(options); - } else { +- } else { - options.top = clampedOffset; -+ scrollUntilReachable(offset, animated, (reachableOffset) => { -+ if (horizontal) { -+ options.left = reachableOffset; -+ } else { -+ options.top = reachableOffset; -+ } -+ target.scrollTo(options); -+ }); ++ target.scrollTo(options); ++ return; } - target.scrollTo(options); ++ scrollUntilReachable(offset, animated, (reachableOffset) => { ++ if (horizontal) { ++ options.left = reachableOffset; ++ } else { ++ options.top = reachableOffset; ++ } ++ target.scrollTo(options); ++ }); }, - [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] + [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable] ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6333,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6149,8 +6318,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -4790,7 +4752,7 @@ index 914d2da..8a87d1e 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -8288,6 +8471,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8288,6 +8456,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -4799,7 +4761,7 @@ index 914d2da..8a87d1e 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs -index 95465f2..48d7741 100644 +index 95465f2..cd8868b 100644 --- a/node_modules/@legendapp/list/react-native.web.mjs +++ b/node_modules/@legendapp/list/react-native.web.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -5988,14 +5950,13 @@ index 95465f2..48d7741 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2100,6 +1923,327 @@ function scrollTo(ctx, params) { +@@ -2100,6 +1923,316 @@ function scrollTo(ctx, params) { } } +// src/core/scrollTargetSettle.ts +var SETTLE_POSITION_EPSILON = 1; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; -+var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; +function clearScrollTargetSettle(state) { @@ -6018,7 +5979,6 @@ index 95465f2..48d7741 100644 + state.scrollTargetSettle = { + corrections: 0, + deadline: now + SETTLE_MAX_MS, -+ expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, + quietPasses: 0, @@ -6064,8 +6024,7 @@ index 95465f2..48d7741 100644 + }); + const scrollingTo = state.scrollingTo; + if (scrollingTo) { -+ const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); -+ scrollingTo.targetOffset = correctedOffset; ++ scrollingTo.targetOffset = state.scrollPending; + scrollingTo.offset = position; + } + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); @@ -6080,10 +6039,6 @@ index 95465f2..48d7741 100644 + const measured = options == null ? void 0 : options.minIndexSizeChanged; + if (measured !== void 0) { + settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); -+ const targetIndex = state.indexByKey.get(settle.id); -+ if (targetIndex !== void 0 && measured <= targetIndex) { -+ settle.expiresAt = Date.now() + SETTLE_TTL_MS; -+ } + } + if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { @@ -6091,7 +6046,6 @@ index 95465f2..48d7741 100644 + return false; + } + settle.quietPasses = 0; -+ settle.expiresAt = Date.now() + SETTLE_TTL_MS; + return false; + } + const index = state.indexByKey.get(settle.id); @@ -6100,8 +6054,7 @@ index 95465f2..48d7741 100644 + clearScrollTargetSettle(state); + return false; + } -+ const now = Date.now(); -+ if (now > settle.expiresAt || now > settle.deadline) { ++ if (Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return false; + } @@ -6109,8 +6062,7 @@ index 95465f2..48d7741 100644 + return false; + } + const targetOffset = getSettleTargetOffset(ctx, settle, index, position); -+ const diff = targetOffset - state.scroll; -+ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ if (Math.abs(targetOffset - state.scroll) <= SETTLE_POSITION_EPSILON) { + settle.measuredIndex = void 0; + settle.quietPasses++; + if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { @@ -6119,7 +6071,6 @@ index 95465f2..48d7741 100644 + return false; + } + settle.quietPasses = 0; -+ settle.expiresAt = now + SETTLE_TTL_MS; + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} @@ -6316,7 +6267,7 @@ index 95465f2..48d7741 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4329,6 +4473,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4329,6 +4462,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -6324,7 +6275,7 @@ index 95465f2..48d7741 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4346,8 +4491,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4346,8 +4480,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -6345,7 +6296,7 @@ index 95465f2..48d7741 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5985,6 +6140,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -5985,6 +6129,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; @@ -6354,7 +6305,7 @@ index 95465f2..48d7741 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6073,6 +6230,26 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,6 +6219,26 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -6381,34 +6332,49 @@ index 95465f2..48d7741 100644 const scrollToLocalOffset = useCallback( (offset, animated) => { const scrollElement = scrollRef.current; -@@ -6095,14 +6272,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6080,29 +6246,32 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + if (!target || typeof target.scrollTo !== "function") { + return; + } +- const maxOffset = getMaxScrollOffset(); +- const clampedOffset = clampOffset(offset, maxOffset); + const behavior = animated ? "smooth" : "auto"; + const options = { behavior }; + if (isWindowScroll) { + const scroll = getWindowScrollPosition(); + const listPos = getElementDocumentPosition(scrollElement, scroll); + const { left, top } = resolveWindowScrollTarget({ +- clampedOffset, ++ clampedOffset: clampOffset(offset, getMaxScrollOffset()), + horizontal, + listPos, + scroll }); options.left = left; options.top = top; - } else if (horizontal) { - options.left = clampedOffset; -+ } -+ if (isWindowScroll) { -+ target.scrollTo(options); - } else { +- } else { - options.top = clampedOffset; -+ scrollUntilReachable(offset, animated, (reachableOffset) => { -+ if (horizontal) { -+ options.left = reachableOffset; -+ } else { -+ options.top = reachableOffset; -+ } -+ target.scrollTo(options); -+ }); ++ target.scrollTo(options); ++ return; } - target.scrollTo(options); ++ scrollUntilReachable(offset, animated, (reachableOffset) => { ++ if (horizontal) { ++ options.left = reachableOffset; ++ } else { ++ options.top = reachableOffset; ++ } ++ target.scrollTo(options); ++ }); }, - [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] + [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable] ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6312,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6128,8 +6297,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -6418,7 +6384,7 @@ index 95465f2..48d7741 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -8267,6 +8450,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8267,6 +8435,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -6427,7 +6393,7 @@ index 95465f2..48d7741 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js -index 914d2da..8a87d1e 100644 +index 914d2da..a91aa0a 100644 --- a/node_modules/@legendapp/list/react.js +++ b/node_modules/@legendapp/list/react.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -7616,14 +7582,13 @@ index 914d2da..8a87d1e 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2121,6 +1944,327 @@ function scrollTo(ctx, params) { +@@ -2121,6 +1944,316 @@ function scrollTo(ctx, params) { } } +// src/core/scrollTargetSettle.ts +var SETTLE_POSITION_EPSILON = 1; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; -+var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; +function clearScrollTargetSettle(state) { @@ -7646,7 +7611,6 @@ index 914d2da..8a87d1e 100644 + state.scrollTargetSettle = { + corrections: 0, + deadline: now + SETTLE_MAX_MS, -+ expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, + quietPasses: 0, @@ -7692,8 +7656,7 @@ index 914d2da..8a87d1e 100644 + }); + const scrollingTo = state.scrollingTo; + if (scrollingTo) { -+ const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); -+ scrollingTo.targetOffset = correctedOffset; ++ scrollingTo.targetOffset = state.scrollPending; + scrollingTo.offset = position; + } + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); @@ -7708,10 +7671,6 @@ index 914d2da..8a87d1e 100644 + const measured = options == null ? void 0 : options.minIndexSizeChanged; + if (measured !== void 0) { + settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); -+ const targetIndex = state.indexByKey.get(settle.id); -+ if (targetIndex !== void 0 && measured <= targetIndex) { -+ settle.expiresAt = Date.now() + SETTLE_TTL_MS; -+ } + } + if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { @@ -7719,7 +7678,6 @@ index 914d2da..8a87d1e 100644 + return false; + } + settle.quietPasses = 0; -+ settle.expiresAt = Date.now() + SETTLE_TTL_MS; + return false; + } + const index = state.indexByKey.get(settle.id); @@ -7728,8 +7686,7 @@ index 914d2da..8a87d1e 100644 + clearScrollTargetSettle(state); + return false; + } -+ const now = Date.now(); -+ if (now > settle.expiresAt || now > settle.deadline) { ++ if (Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return false; + } @@ -7737,8 +7694,7 @@ index 914d2da..8a87d1e 100644 + return false; + } + const targetOffset = getSettleTargetOffset(ctx, settle, index, position); -+ const diff = targetOffset - state.scroll; -+ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ if (Math.abs(targetOffset - state.scroll) <= SETTLE_POSITION_EPSILON) { + settle.measuredIndex = void 0; + settle.quietPasses++; + if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { @@ -7747,7 +7703,6 @@ index 914d2da..8a87d1e 100644 + return false; + } + settle.quietPasses = 0; -+ settle.expiresAt = now + SETTLE_TTL_MS; + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} @@ -7944,7 +7899,7 @@ index 914d2da..8a87d1e 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4350,6 +4494,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4350,6 +4483,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -7952,7 +7907,7 @@ index 914d2da..8a87d1e 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4367,8 +4512,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4367,8 +4501,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -7973,7 +7928,7 @@ index 914d2da..8a87d1e 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -6006,6 +6161,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -6006,6 +6150,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; @@ -7982,7 +7937,7 @@ index 914d2da..8a87d1e 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6094,6 +6251,26 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,6 +6240,26 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -8009,34 +7964,49 @@ index 914d2da..8a87d1e 100644 const scrollToLocalOffset = React3.useCallback( (offset, animated) => { const scrollElement = scrollRef.current; -@@ -6116,14 +6293,21 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6101,29 +6267,32 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView + if (!target || typeof target.scrollTo !== "function") { + return; + } +- const maxOffset = getMaxScrollOffset(); +- const clampedOffset = clampOffset(offset, maxOffset); + const behavior = animated ? "smooth" : "auto"; + const options = { behavior }; + if (isWindowScroll) { + const scroll = getWindowScrollPosition(); + const listPos = getElementDocumentPosition(scrollElement, scroll); + const { left, top } = resolveWindowScrollTarget({ +- clampedOffset, ++ clampedOffset: clampOffset(offset, getMaxScrollOffset()), + horizontal, + listPos, + scroll }); options.left = left; options.top = top; - } else if (horizontal) { - options.left = clampedOffset; -+ } -+ if (isWindowScroll) { -+ target.scrollTo(options); - } else { +- } else { - options.top = clampedOffset; -+ scrollUntilReachable(offset, animated, (reachableOffset) => { -+ if (horizontal) { -+ options.left = reachableOffset; -+ } else { -+ options.top = reachableOffset; -+ } -+ target.scrollTo(options); -+ }); ++ target.scrollTo(options); ++ return; } - target.scrollTo(options); ++ scrollUntilReachable(offset, animated, (reachableOffset) => { ++ if (horizontal) { ++ options.left = reachableOffset; ++ } else { ++ options.top = reachableOffset; ++ } ++ target.scrollTo(options); ++ }); }, - [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] + [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable] ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6333,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6149,8 +6318,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -8046,7 +8016,7 @@ index 914d2da..8a87d1e 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -8288,6 +8471,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8288,6 +8456,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -8055,7 +8025,7 @@ index 914d2da..8a87d1e 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs -index 95465f2..48d7741 100644 +index 95465f2..cd8868b 100644 --- a/node_modules/@legendapp/list/react.mjs +++ b/node_modules/@legendapp/list/react.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -9244,14 +9214,13 @@ index 95465f2..48d7741 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2100,6 +1923,327 @@ function scrollTo(ctx, params) { +@@ -2100,6 +1923,316 @@ function scrollTo(ctx, params) { } } +// src/core/scrollTargetSettle.ts +var SETTLE_POSITION_EPSILON = 1; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; -+var SETTLE_TTL_MS = 500; +var SETTLE_MAX_MS = 1e3; +var SETTLE_MAX_CORRECTIONS = 8; +function clearScrollTargetSettle(state) { @@ -9274,7 +9243,6 @@ index 95465f2..48d7741 100644 + state.scrollTargetSettle = { + corrections: 0, + deadline: now + SETTLE_MAX_MS, -+ expiresAt: now + SETTLE_TTL_MS, + id: getId(state, index), + measuredIndex: void 0, + quietPasses: 0, @@ -9320,8 +9288,7 @@ index 95465f2..48d7741 100644 + }); + const scrollingTo = state.scrollingTo; + if (scrollingTo) { -+ const correctedOffset = getSettleTargetOffset(ctx, settle, index, position); -+ scrollingTo.targetOffset = correctedOffset; ++ scrollingTo.targetOffset = state.scrollPending; + scrollingTo.offset = position; + } + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); @@ -9336,10 +9303,6 @@ index 95465f2..48d7741 100644 + const measured = options == null ? void 0 : options.minIndexSizeChanged; + if (measured !== void 0) { + settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); -+ const targetIndex = state.indexByKey.get(settle.id); -+ if (targetIndex !== void 0 && measured <= targetIndex) { -+ settle.expiresAt = Date.now() + SETTLE_TTL_MS; -+ } + } + if (options == null ? void 0 : options.isCompensating) { + if (Date.now() > settle.deadline) { @@ -9347,7 +9310,6 @@ index 95465f2..48d7741 100644 + return false; + } + settle.quietPasses = 0; -+ settle.expiresAt = Date.now() + SETTLE_TTL_MS; + return false; + } + const index = state.indexByKey.get(settle.id); @@ -9356,8 +9318,7 @@ index 95465f2..48d7741 100644 + clearScrollTargetSettle(state); + return false; + } -+ const now = Date.now(); -+ if (now > settle.expiresAt || now > settle.deadline) { ++ if (Date.now() > settle.deadline) { + clearScrollTargetSettle(state); + return false; + } @@ -9365,8 +9326,7 @@ index 95465f2..48d7741 100644 + return false; + } + const targetOffset = getSettleTargetOffset(ctx, settle, index, position); -+ const diff = targetOffset - state.scroll; -+ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ if (Math.abs(targetOffset - state.scroll) <= SETTLE_POSITION_EPSILON) { + settle.measuredIndex = void 0; + settle.quietPasses++; + if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { @@ -9375,7 +9335,6 @@ index 95465f2..48d7741 100644 + return false; + } + settle.quietPasses = 0; -+ settle.expiresAt = now + SETTLE_TTL_MS; + state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); + return true; +} @@ -9572,7 +9531,7 @@ index 95465f2..48d7741 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4329,6 +4473,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4329,6 +4462,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -9580,7 +9539,7 @@ index 95465f2..48d7741 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4346,8 +4491,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4346,8 +4480,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -9601,7 +9560,7 @@ index 95465f2..48d7741 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5985,6 +6140,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -5985,6 +6129,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; @@ -9610,7 +9569,7 @@ index 95465f2..48d7741 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6073,6 +6230,26 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,6 +6219,26 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -9637,34 +9596,49 @@ index 95465f2..48d7741 100644 const scrollToLocalOffset = useCallback( (offset, animated) => { const scrollElement = scrollRef.current; -@@ -6095,14 +6272,21 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6080,29 +6246,32 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ + if (!target || typeof target.scrollTo !== "function") { + return; + } +- const maxOffset = getMaxScrollOffset(); +- const clampedOffset = clampOffset(offset, maxOffset); + const behavior = animated ? "smooth" : "auto"; + const options = { behavior }; + if (isWindowScroll) { + const scroll = getWindowScrollPosition(); + const listPos = getElementDocumentPosition(scrollElement, scroll); + const { left, top } = resolveWindowScrollTarget({ +- clampedOffset, ++ clampedOffset: clampOffset(offset, getMaxScrollOffset()), + horizontal, + listPos, + scroll }); options.left = left; options.top = top; - } else if (horizontal) { - options.left = clampedOffset; -+ } -+ if (isWindowScroll) { -+ target.scrollTo(options); - } else { +- } else { - options.top = clampedOffset; -+ scrollUntilReachable(offset, animated, (reachableOffset) => { -+ if (horizontal) { -+ options.left = reachableOffset; -+ } else { -+ options.top = reachableOffset; -+ } -+ target.scrollTo(options); -+ }); ++ target.scrollTo(options); ++ return; } - target.scrollTo(options); ++ scrollUntilReachable(offset, animated, (reachableOffset) => { ++ if (horizontal) { ++ options.left = reachableOffset; ++ } else { ++ options.top = reachableOffset; ++ } ++ target.scrollTo(options); ++ }); }, - [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] + [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable] ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6312,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6128,8 +6297,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -9674,7 +9648,7 @@ index 95465f2..48d7741 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -8267,6 +8450,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -8267,6 +8435,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); From fab97c508d267202da1d5eadf47936ddc6239c8b Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 12 Aug 2026 23:44:06 -0400 Subject: [PATCH 28/38] fix(chat): open a thread on its newest message Two library fixes, both found by clicking through conversations in the running Electron app and reproduced with playwright over its CDP port. Each is content growing after the list has already anchored to the end it could see. The end anchor read its own scroll settling as the reader taking hold, and gave up the end in the frame the content grew past it - measured at 432px short of the newest message. The platform now records the offset it actually scrolled to and the anchor only stands down for a position it did not put the list in. Nothing followed the header's first measurement. SpecialTopMessage lays out taller than the size it renders with, one frame after the initial scroll, which pushes every message down by the difference - measured at 52px. maintainScrollAtEnd grew a headerLayout trigger for it, which this list opts into. Enabling maintainVisibleContentPosition's size anchoring instead was tried in a live build and still failed 2 of 6 opens: that path declines while a scroll is in flight, which is exactly when a header first measures. Fork commits b2d666bc and 1a56778e, patch rebuilt from dist. --- shared/patches/@legendapp+list+3.3.5.patch | 568 +++++++++++++++++---- 1 file changed, 472 insertions(+), 96 deletions(-) diff --git a/shared/patches/@legendapp+list+3.3.5.patch b/shared/patches/@legendapp+list+3.3.5.patch index afb4ee294062..3c66b008d707 100644 --- a/shared/patches/@legendapp+list+3.3.5.patch +++ b/shared/patches/@legendapp+list+3.3.5.patch @@ -2,8 +2,42 @@ diff --git a/node_modules/@legendapp/list/.DS_Store b/node_modules/@legendapp/li deleted file mode 100644 index b86e710..0000000 Binary files a/node_modules/@legendapp/list/.DS_Store and /dev/null differ +diff --git a/node_modules/@legendapp/list/animated.d.ts b/node_modules/@legendapp/list/animated.d.ts +index 14beb98..56eefce 100644 +--- a/node_modules/@legendapp/list/animated.d.ts ++++ b/node_modules/@legendapp/list/animated.d.ts +@@ -488,6 +488,12 @@ interface AlwaysRenderConfig { + interface MaintainScrollAtEndOnOptions { + dataChange?: boolean; + footerLayout?: boolean; ++ /** ++ * Keep the list pinned to the end when the header changes size. A header that measures larger ++ * than its estimate pushes every item down, which leaves a list that had already scrolled to ++ * its end short of it by the difference. ++ */ ++ headerLayout?: boolean; + itemLayout?: boolean; + layout?: boolean; + } +diff --git a/node_modules/@legendapp/list/react-native.d.ts b/node_modules/@legendapp/list/react-native.d.ts +index ce1fe00..9a4311c 100644 +--- a/node_modules/@legendapp/list/react-native.d.ts ++++ b/node_modules/@legendapp/list/react-native.d.ts +@@ -488,6 +488,12 @@ interface AlwaysRenderConfig { + interface MaintainScrollAtEndOnOptions { + dataChange?: boolean; + footerLayout?: boolean; ++ /** ++ * Keep the list pinned to the end when the header changes size. A header that measures larger ++ * than its estimate pushes every item down, which leaves a list that had already scrolled to ++ * its end short of it by the difference. ++ */ ++ headerLayout?: boolean; + itemLayout?: boolean; + layout?: boolean; + } diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js -index b3c5a30..7f3669a 100644 +index b3c5a30..9d7ad6d 100644 --- a/node_modules/@legendapp/list/react-native.js +++ b/node_modules/@legendapp/list/react-native.js @@ -457,611 +457,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -1139,18 +1173,34 @@ index b3c5a30..7f3669a 100644 // src/core/doMaintainScrollAtEnd.ts function doMaintainScrollAtEnd(ctx) { const state = ctx.state; -@@ -1575,6 +1372,7 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1575,9 +1372,12 @@ function doMaintainScrollAtEnd(ctx) { const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; state.maintainingScrollAtEnd = pendingState; + clearScrollTargetSettle(state); requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); - const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1977,6 +1775,27 @@ var flushSync = (fn) => { - fn(); - }; +- const didScrollSinceRequest = state.scroll !== scrollAtRequest; ++ const lastIssued = state.lastIssuedScrollOffset; ++ const isSettlingOntoIssuedScroll = lastIssued !== void 0 && Math.abs(state.scroll - lastIssued) <= EDGE_POSITION_EPSILON; ++ const didScrollSinceRequest = state.scroll !== scrollAtRequest && !isSettlingOntoIssuedScroll; + if (isStillWithinThreshold || !didScrollSinceRequest) { + state.maintainingScrollAtEnd = activeState; + const scroller = refScroller.current; +@@ -1972,11 +1772,32 @@ function prepareMVCP(ctx, dataChanged) { + } + } +-// src/platform/flushSync.native.ts +-var flushSync = (fn) => { +- fn(); +-}; +- ++// src/platform/flushSync.native.ts ++var flushSync = (fn) => { ++ fn(); ++}; ++ +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { @@ -1175,7 +1225,7 @@ index b3c5a30..7f3669a 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2224,7 +2043,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -2224,7 +2045,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } function scrollTo(ctx, params) { @@ -1184,7 +1234,7 @@ index b3c5a30..7f3669a 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2246,6 +2065,7 @@ function scrollTo(ctx, params) { +@@ -2246,6 +2067,7 @@ function scrollTo(ctx, params) { if (!noScrollingTo) { if (isInitialScroll) { initialScrollCompletion.resetFlags(state); @@ -1192,7 +1242,7 @@ index b3c5a30..7f3669a 100644 } const averageSizeSnapshot = getAverageSizeSnapshot(state); state.scrollingTo = { -@@ -2256,6 +2076,15 @@ function scrollTo(ctx, params) { +@@ -2256,6 +2078,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); @@ -1208,7 +1258,7 @@ index b3c5a30..7f3669a 100644 } } state.scrollPending = targetOffset; -@@ -2263,7 +2092,7 @@ function scrollTo(ctx, params) { +@@ -2263,7 +2094,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -1217,7 +1267,7 @@ index b3c5a30..7f3669a 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2276,6 +2105,310 @@ function scrollTo(ctx, params) { +@@ -2276,6 +2107,310 @@ function scrollTo(ctx, params) { } } @@ -1528,7 +1578,7 @@ index b3c5a30..7f3669a 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4320,6 +4453,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4320,6 +4455,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -1536,7 +1586,7 @@ index b3c5a30..7f3669a 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4337,8 +4471,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4337,8 +4473,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -1557,7 +1607,42 @@ index b3c5a30..7f3669a 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -7652,6 +7796,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -5831,6 +5977,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize + return Platform.OS === "web" && props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; + } + function setHeaderSize(ctx, size) { ++ var _a3; + const { state } = ctx; + const previousHeaderSize = peek$(ctx, "headerSize") || 0; + const didChange = previousHeaderSize !== size; +@@ -5840,6 +5987,8 @@ function setHeaderSize(ctx, size) { + updateContentMetricsState(ctx); + if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { + requestAdjust(ctx, size - previousHeaderSize); ++ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) { ++ doMaintainScrollAtEnd(ctx); + } + } + state.didMeasureHeader = true; +@@ -6926,13 +7075,14 @@ function getRenderedItem(ctx, key) { + + // src/utils/normalizeMaintainScrollAtEnd.ts + function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) { +- var _a3, _b, _c, _d; ++ var _a3, _b, _c, _d, _e; + return { + animated: false, + onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true, + onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true, +- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true, +- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true ++ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true, ++ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true, ++ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true + }; + } + function normalizeMaintainScrollAtEnd(value) { +@@ -7652,6 +7802,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -1566,7 +1651,7 @@ index b3c5a30..7f3669a 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs -index 40e87cd..2b216b4 100644 +index 40e87cd..8e6f6d0 100644 --- a/node_modules/@legendapp/list/react-native.mjs +++ b/node_modules/@legendapp/list/react-native.mjs @@ -436,611 +436,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -2702,18 +2787,34 @@ index 40e87cd..2b216b4 100644 // src/core/doMaintainScrollAtEnd.ts function doMaintainScrollAtEnd(ctx) { const state = ctx.state; -@@ -1554,6 +1351,7 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1554,9 +1351,12 @@ function doMaintainScrollAtEnd(ctx) { const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; state.maintainingScrollAtEnd = pendingState; + clearScrollTargetSettle(state); requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); - const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1956,6 +1754,27 @@ var flushSync = (fn) => { - fn(); - }; +- const didScrollSinceRequest = state.scroll !== scrollAtRequest; ++ const lastIssued = state.lastIssuedScrollOffset; ++ const isSettlingOntoIssuedScroll = lastIssued !== void 0 && Math.abs(state.scroll - lastIssued) <= EDGE_POSITION_EPSILON; ++ const didScrollSinceRequest = state.scroll !== scrollAtRequest && !isSettlingOntoIssuedScroll; + if (isStillWithinThreshold || !didScrollSinceRequest) { + state.maintainingScrollAtEnd = activeState; + const scroller = refScroller.current; +@@ -1951,11 +1751,32 @@ function prepareMVCP(ctx, dataChanged) { + } + } +-// src/platform/flushSync.native.ts +-var flushSync = (fn) => { +- fn(); +-}; +- ++// src/platform/flushSync.native.ts ++var flushSync = (fn) => { ++ fn(); ++}; ++ +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { @@ -2738,7 +2839,7 @@ index 40e87cd..2b216b4 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2203,7 +2022,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -2203,7 +2024,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } function scrollTo(ctx, params) { @@ -2747,7 +2848,7 @@ index 40e87cd..2b216b4 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2225,6 +2044,7 @@ function scrollTo(ctx, params) { +@@ -2225,6 +2046,7 @@ function scrollTo(ctx, params) { if (!noScrollingTo) { if (isInitialScroll) { initialScrollCompletion.resetFlags(state); @@ -2755,7 +2856,7 @@ index 40e87cd..2b216b4 100644 } const averageSizeSnapshot = getAverageSizeSnapshot(state); state.scrollingTo = { -@@ -2235,6 +2055,15 @@ function scrollTo(ctx, params) { +@@ -2235,6 +2057,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); @@ -2771,7 +2872,7 @@ index 40e87cd..2b216b4 100644 } } state.scrollPending = targetOffset; -@@ -2242,7 +2071,7 @@ function scrollTo(ctx, params) { +@@ -2242,7 +2073,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -2780,7 +2881,7 @@ index 40e87cd..2b216b4 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2255,6 +2084,310 @@ function scrollTo(ctx, params) { +@@ -2255,6 +2086,310 @@ function scrollTo(ctx, params) { } } @@ -3091,7 +3192,7 @@ index 40e87cd..2b216b4 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4299,6 +4432,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4299,6 +4434,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -3099,7 +3200,7 @@ index 40e87cd..2b216b4 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4316,8 +4450,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4316,8 +4452,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -3120,7 +3221,42 @@ index 40e87cd..2b216b4 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -7631,6 +7775,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -5810,6 +5956,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize + return Platform.OS === "web" && props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; + } + function setHeaderSize(ctx, size) { ++ var _a3; + const { state } = ctx; + const previousHeaderSize = peek$(ctx, "headerSize") || 0; + const didChange = previousHeaderSize !== size; +@@ -5819,6 +5966,8 @@ function setHeaderSize(ctx, size) { + updateContentMetricsState(ctx); + if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { + requestAdjust(ctx, size - previousHeaderSize); ++ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) { ++ doMaintainScrollAtEnd(ctx); + } + } + state.didMeasureHeader = true; +@@ -6905,13 +7054,14 @@ function getRenderedItem(ctx, key) { + + // src/utils/normalizeMaintainScrollAtEnd.ts + function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) { +- var _a3, _b, _c, _d; ++ var _a3, _b, _c, _d, _e; + return { + animated: false, + onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true, + onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true, +- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true, +- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true ++ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true, ++ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true, ++ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true + }; + } + function normalizeMaintainScrollAtEnd(value) { +@@ -7631,6 +7781,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -3128,8 +3264,25 @@ index 40e87cd..2b216b4 100644 (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) +diff --git a/node_modules/@legendapp/list/react-native.web.d.ts b/node_modules/@legendapp/list/react-native.web.d.ts +index b6c7481..ace847e 100644 +--- a/node_modules/@legendapp/list/react-native.web.d.ts ++++ b/node_modules/@legendapp/list/react-native.web.d.ts +@@ -511,6 +511,12 @@ interface AlwaysRenderConfig { + interface MaintainScrollAtEndOnOptions { + dataChange?: boolean; + footerLayout?: boolean; ++ /** ++ * Keep the list pinned to the end when the header changes size. A header that measures larger ++ * than its estimate pushes every item down, which leaves a list that had already scrolled to ++ * its end short of it by the difference. ++ */ ++ headerLayout?: boolean; + itemLayout?: boolean; + layout?: boolean; + } diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js -index 914d2da..a91aa0a 100644 +index 914d2da..ccb19f4 100644 --- a/node_modules/@legendapp/list/react-native.web.js +++ b/node_modules/@legendapp/list/react-native.web.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -4240,15 +4393,21 @@ index 914d2da..a91aa0a 100644 // src/core/doMaintainScrollAtEnd.ts function doMaintainScrollAtEnd(ctx) { const state = ctx.state; -@@ -1451,6 +1242,7 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1451,9 +1242,12 @@ function doMaintainScrollAtEnd(ctx) { const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; state.maintainingScrollAtEnd = pendingState; + clearScrollTargetSettle(state); requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); - const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1809,6 +1601,27 @@ function prepareMVCP(ctx, dataChanged) { +- const didScrollSinceRequest = state.scroll !== scrollAtRequest; ++ const lastIssued = state.lastIssuedScrollOffset; ++ const isSettlingOntoIssuedScroll = lastIssued !== void 0 && Math.abs(state.scroll - lastIssued) <= EDGE_POSITION_EPSILON; ++ const didScrollSinceRequest = state.scroll !== scrollAtRequest && !isSettlingOntoIssuedScroll; + if (isStillWithinThreshold || !didScrollSinceRequest) { + state.maintainingScrollAtEnd = activeState; + const scroller = refScroller.current; +@@ -1809,6 +1603,27 @@ function prepareMVCP(ctx, dataChanged) { } } @@ -4276,7 +4435,7 @@ index 914d2da..a91aa0a 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2069,7 +1882,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -2069,7 +1884,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } function scrollTo(ctx, params) { @@ -4285,7 +4444,7 @@ index 914d2da..a91aa0a 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2091,6 +1904,7 @@ function scrollTo(ctx, params) { +@@ -2091,6 +1906,7 @@ function scrollTo(ctx, params) { if (!noScrollingTo) { if (isInitialScroll) { initialScrollCompletion.resetFlags(state); @@ -4293,7 +4452,7 @@ index 914d2da..a91aa0a 100644 } const averageSizeSnapshot = getAverageSizeSnapshot(state); state.scrollingTo = { -@@ -2101,6 +1915,15 @@ function scrollTo(ctx, params) { +@@ -2101,6 +1917,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); @@ -4309,7 +4468,7 @@ index 914d2da..a91aa0a 100644 } } state.scrollPending = targetOffset; -@@ -2108,7 +1931,7 @@ function scrollTo(ctx, params) { +@@ -2108,7 +1933,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -4318,7 +4477,7 @@ index 914d2da..a91aa0a 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2121,6 +1944,316 @@ function scrollTo(ctx, params) { +@@ -2121,6 +1946,316 @@ function scrollTo(ctx, params) { } } @@ -4635,7 +4794,7 @@ index 914d2da..a91aa0a 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4350,6 +4483,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4350,6 +4485,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -4643,7 +4802,7 @@ index 914d2da..a91aa0a 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4367,8 +4501,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4367,8 +4503,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -4664,7 +4823,7 @@ index 914d2da..a91aa0a 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -6006,6 +6150,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -6006,6 +6152,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; @@ -4673,7 +4832,7 @@ index 914d2da..a91aa0a 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6094,6 +6240,26 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,6 +6242,26 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -4700,7 +4859,7 @@ index 914d2da..a91aa0a 100644 const scrollToLocalOffset = React3.useCallback( (offset, animated) => { const scrollElement = scrollRef.current; -@@ -6101,29 +6267,32 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6101,29 +6269,34 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView if (!target || typeof target.scrollTo !== "function") { return; } @@ -4724,6 +4883,7 @@ index 914d2da..a91aa0a 100644 - options.left = clampedOffset; - } else { - options.top = clampedOffset; ++ ctx.state.lastIssuedScrollOffset = clampOffset(offset, getMaxScrollOffset()); + target.scrollTo(options); + return; } @@ -4734,15 +4894,16 @@ index 914d2da..a91aa0a 100644 + } else { + options.top = reachableOffset; + } ++ ctx.state.lastIssuedScrollOffset = reachableOffset; + target.scrollTo(options); + }); }, - [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] -+ [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable] ++ [ctx, getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable] ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6318,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6149,8 +6322,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -4752,7 +4913,42 @@ index 914d2da..a91aa0a 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -8288,6 +8456,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6499,6 +6671,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize + return props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; + } + function setHeaderSize(ctx, size) { ++ var _a3; + const { state } = ctx; + const previousHeaderSize = peek$(ctx, "headerSize") || 0; + const didChange = previousHeaderSize !== size; +@@ -6508,6 +6681,8 @@ function setHeaderSize(ctx, size) { + updateContentMetricsState(ctx); + if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { + requestAdjust(ctx, size - previousHeaderSize); ++ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) { ++ doMaintainScrollAtEnd(ctx); + } + } + state.didMeasureHeader = true; +@@ -7570,13 +7745,14 @@ function getRenderedItem(ctx, key) { + + // src/utils/normalizeMaintainScrollAtEnd.ts + function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) { +- var _a3, _b, _c, _d; ++ var _a3, _b, _c, _d, _e; + return { + animated: false, + onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true, + onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true, +- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true, +- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true ++ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true, ++ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true, ++ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true + }; + } + function normalizeMaintainScrollAtEnd(value) { +@@ -8288,6 +8464,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -4761,7 +4957,7 @@ index 914d2da..a91aa0a 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs -index 95465f2..cd8868b 100644 +index 95465f2..4fb1e41 100644 --- a/node_modules/@legendapp/list/react-native.web.mjs +++ b/node_modules/@legendapp/list/react-native.web.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -5872,15 +6068,21 @@ index 95465f2..cd8868b 100644 // src/core/doMaintainScrollAtEnd.ts function doMaintainScrollAtEnd(ctx) { const state = ctx.state; -@@ -1430,6 +1221,7 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1430,9 +1221,12 @@ function doMaintainScrollAtEnd(ctx) { const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; state.maintainingScrollAtEnd = pendingState; + clearScrollTargetSettle(state); requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); - const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1788,6 +1580,27 @@ function prepareMVCP(ctx, dataChanged) { +- const didScrollSinceRequest = state.scroll !== scrollAtRequest; ++ const lastIssued = state.lastIssuedScrollOffset; ++ const isSettlingOntoIssuedScroll = lastIssued !== void 0 && Math.abs(state.scroll - lastIssued) <= EDGE_POSITION_EPSILON; ++ const didScrollSinceRequest = state.scroll !== scrollAtRequest && !isSettlingOntoIssuedScroll; + if (isStillWithinThreshold || !didScrollSinceRequest) { + state.maintainingScrollAtEnd = activeState; + const scroller = refScroller.current; +@@ -1788,6 +1582,27 @@ function prepareMVCP(ctx, dataChanged) { } } @@ -5908,7 +6110,7 @@ index 95465f2..cd8868b 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2048,7 +1861,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -2048,7 +1863,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } function scrollTo(ctx, params) { @@ -5917,7 +6119,7 @@ index 95465f2..cd8868b 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2070,6 +1883,7 @@ function scrollTo(ctx, params) { +@@ -2070,6 +1885,7 @@ function scrollTo(ctx, params) { if (!noScrollingTo) { if (isInitialScroll) { initialScrollCompletion.resetFlags(state); @@ -5925,7 +6127,7 @@ index 95465f2..cd8868b 100644 } const averageSizeSnapshot = getAverageSizeSnapshot(state); state.scrollingTo = { -@@ -2080,6 +1894,15 @@ function scrollTo(ctx, params) { +@@ -2080,6 +1896,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); @@ -5941,7 +6143,7 @@ index 95465f2..cd8868b 100644 } } state.scrollPending = targetOffset; -@@ -2087,7 +1910,7 @@ function scrollTo(ctx, params) { +@@ -2087,7 +1912,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -5950,7 +6152,7 @@ index 95465f2..cd8868b 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2100,6 +1923,316 @@ function scrollTo(ctx, params) { +@@ -2100,6 +1925,316 @@ function scrollTo(ctx, params) { } } @@ -6267,7 +6469,7 @@ index 95465f2..cd8868b 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4329,6 +4462,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4329,6 +4464,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -6275,7 +6477,7 @@ index 95465f2..cd8868b 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4346,8 +4480,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4346,8 +4482,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -6296,7 +6498,7 @@ index 95465f2..cd8868b 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5985,6 +6129,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -5985,6 +6131,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; @@ -6305,7 +6507,7 @@ index 95465f2..cd8868b 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6073,6 +6219,26 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,6 +6221,26 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -6332,7 +6534,7 @@ index 95465f2..cd8868b 100644 const scrollToLocalOffset = useCallback( (offset, animated) => { const scrollElement = scrollRef.current; -@@ -6080,29 +6246,32 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6080,29 +6248,34 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ if (!target || typeof target.scrollTo !== "function") { return; } @@ -6356,6 +6558,7 @@ index 95465f2..cd8868b 100644 - options.left = clampedOffset; - } else { - options.top = clampedOffset; ++ ctx.state.lastIssuedScrollOffset = clampOffset(offset, getMaxScrollOffset()); + target.scrollTo(options); + return; } @@ -6366,15 +6569,16 @@ index 95465f2..cd8868b 100644 + } else { + options.top = reachableOffset; + } ++ ctx.state.lastIssuedScrollOffset = reachableOffset; + target.scrollTo(options); + }); }, - [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] -+ [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable] ++ [ctx, getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable] ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6297,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6128,8 +6301,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -6384,7 +6588,42 @@ index 95465f2..cd8868b 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -8267,6 +8435,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6478,6 +6650,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize + return props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; + } + function setHeaderSize(ctx, size) { ++ var _a3; + const { state } = ctx; + const previousHeaderSize = peek$(ctx, "headerSize") || 0; + const didChange = previousHeaderSize !== size; +@@ -6487,6 +6660,8 @@ function setHeaderSize(ctx, size) { + updateContentMetricsState(ctx); + if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { + requestAdjust(ctx, size - previousHeaderSize); ++ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) { ++ doMaintainScrollAtEnd(ctx); + } + } + state.didMeasureHeader = true; +@@ -7549,13 +7724,14 @@ function getRenderedItem(ctx, key) { + + // src/utils/normalizeMaintainScrollAtEnd.ts + function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) { +- var _a3, _b, _c, _d; ++ var _a3, _b, _c, _d, _e; + return { + animated: false, + onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true, + onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true, +- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true, +- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true ++ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true, ++ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true, ++ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true + }; + } + function normalizeMaintainScrollAtEnd(value) { +@@ -8267,6 +8443,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -6392,8 +6631,25 @@ index 95465f2..cd8868b 100644 (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) +diff --git a/node_modules/@legendapp/list/react.d.ts b/node_modules/@legendapp/list/react.d.ts +index b6c7481..ace847e 100644 +--- a/node_modules/@legendapp/list/react.d.ts ++++ b/node_modules/@legendapp/list/react.d.ts +@@ -511,6 +511,12 @@ interface AlwaysRenderConfig { + interface MaintainScrollAtEndOnOptions { + dataChange?: boolean; + footerLayout?: boolean; ++ /** ++ * Keep the list pinned to the end when the header changes size. A header that measures larger ++ * than its estimate pushes every item down, which leaves a list that had already scrolled to ++ * its end short of it by the difference. ++ */ ++ headerLayout?: boolean; + itemLayout?: boolean; + layout?: boolean; + } diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js -index 914d2da..a91aa0a 100644 +index 914d2da..ccb19f4 100644 --- a/node_modules/@legendapp/list/react.js +++ b/node_modules/@legendapp/list/react.js @@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -7504,15 +7760,21 @@ index 914d2da..a91aa0a 100644 // src/core/doMaintainScrollAtEnd.ts function doMaintainScrollAtEnd(ctx) { const state = ctx.state; -@@ -1451,6 +1242,7 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1451,9 +1242,12 @@ function doMaintainScrollAtEnd(ctx) { const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; state.maintainingScrollAtEnd = pendingState; + clearScrollTargetSettle(state); requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); - const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1809,6 +1601,27 @@ function prepareMVCP(ctx, dataChanged) { +- const didScrollSinceRequest = state.scroll !== scrollAtRequest; ++ const lastIssued = state.lastIssuedScrollOffset; ++ const isSettlingOntoIssuedScroll = lastIssued !== void 0 && Math.abs(state.scroll - lastIssued) <= EDGE_POSITION_EPSILON; ++ const didScrollSinceRequest = state.scroll !== scrollAtRequest && !isSettlingOntoIssuedScroll; + if (isStillWithinThreshold || !didScrollSinceRequest) { + state.maintainingScrollAtEnd = activeState; + const scroller = refScroller.current; +@@ -1809,6 +1603,27 @@ function prepareMVCP(ctx, dataChanged) { } } @@ -7540,7 +7802,7 @@ index 914d2da..a91aa0a 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2069,7 +1882,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -2069,7 +1884,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } function scrollTo(ctx, params) { @@ -7549,7 +7811,7 @@ index 914d2da..a91aa0a 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2091,6 +1904,7 @@ function scrollTo(ctx, params) { +@@ -2091,6 +1906,7 @@ function scrollTo(ctx, params) { if (!noScrollingTo) { if (isInitialScroll) { initialScrollCompletion.resetFlags(state); @@ -7557,7 +7819,7 @@ index 914d2da..a91aa0a 100644 } const averageSizeSnapshot = getAverageSizeSnapshot(state); state.scrollingTo = { -@@ -2101,6 +1915,15 @@ function scrollTo(ctx, params) { +@@ -2101,6 +1917,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); @@ -7573,7 +7835,7 @@ index 914d2da..a91aa0a 100644 } } state.scrollPending = targetOffset; -@@ -2108,7 +1931,7 @@ function scrollTo(ctx, params) { +@@ -2108,7 +1933,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -7582,7 +7844,7 @@ index 914d2da..a91aa0a 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2121,6 +1944,316 @@ function scrollTo(ctx, params) { +@@ -2121,6 +1946,316 @@ function scrollTo(ctx, params) { } } @@ -7899,7 +8161,7 @@ index 914d2da..a91aa0a 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4350,6 +4483,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4350,6 +4485,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -7907,7 +8169,7 @@ index 914d2da..a91aa0a 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4367,8 +4501,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4367,8 +4503,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -7928,7 +8190,7 @@ index 914d2da..a91aa0a 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -6006,6 +6150,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -6006,6 +6152,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; @@ -7937,7 +8199,7 @@ index 914d2da..a91aa0a 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6094,6 +6240,26 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6094,6 +6242,26 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -7964,7 +8226,7 @@ index 914d2da..a91aa0a 100644 const scrollToLocalOffset = React3.useCallback( (offset, animated) => { const scrollElement = scrollRef.current; -@@ -6101,29 +6267,32 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6101,29 +6269,34 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView if (!target || typeof target.scrollTo !== "function") { return; } @@ -7988,6 +8250,7 @@ index 914d2da..a91aa0a 100644 - options.left = clampedOffset; - } else { - options.top = clampedOffset; ++ ctx.state.lastIssuedScrollOffset = clampOffset(offset, getMaxScrollOffset()); + target.scrollTo(options); + return; } @@ -7998,15 +8261,16 @@ index 914d2da..a91aa0a 100644 + } else { + options.top = reachableOffset; + } ++ ctx.state.lastIssuedScrollOffset = reachableOffset; + target.scrollTo(options); + }); }, - [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] -+ [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable] ++ [ctx, getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable] ); React3.useImperativeHandle(ref, () => { const api = { -@@ -6149,8 +6318,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView +@@ -6149,8 +6322,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -8016,7 +8280,42 @@ index 914d2da..a91aa0a 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -8288,6 +8456,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6499,6 +6671,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize + return props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; + } + function setHeaderSize(ctx, size) { ++ var _a3; + const { state } = ctx; + const previousHeaderSize = peek$(ctx, "headerSize") || 0; + const didChange = previousHeaderSize !== size; +@@ -6508,6 +6681,8 @@ function setHeaderSize(ctx, size) { + updateContentMetricsState(ctx); + if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { + requestAdjust(ctx, size - previousHeaderSize); ++ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) { ++ doMaintainScrollAtEnd(ctx); + } + } + state.didMeasureHeader = true; +@@ -7570,13 +7745,14 @@ function getRenderedItem(ctx, key) { + + // src/utils/normalizeMaintainScrollAtEnd.ts + function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) { +- var _a3, _b, _c, _d; ++ var _a3, _b, _c, _d, _e; + return { + animated: false, + onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true, + onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true, +- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true, +- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true ++ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true, ++ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true, ++ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true + }; + } + function normalizeMaintainScrollAtEnd(value) { +@@ -8288,6 +8464,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -8025,7 +8324,7 @@ index 914d2da..a91aa0a 100644 }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs -index 95465f2..cd8868b 100644 +index 95465f2..4fb1e41 100644 --- a/node_modules/@legendapp/list/react.mjs +++ b/node_modules/@legendapp/list/react.mjs @@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; @@ -9136,15 +9435,21 @@ index 95465f2..cd8868b 100644 // src/core/doMaintainScrollAtEnd.ts function doMaintainScrollAtEnd(ctx) { const state = ctx.state; -@@ -1430,6 +1221,7 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1430,9 +1221,12 @@ function doMaintainScrollAtEnd(ctx) { const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; const scrollAtRequest = state.scroll; state.maintainingScrollAtEnd = pendingState; + clearScrollTargetSettle(state); requestAnimationFrame(() => { const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); - const didScrollSinceRequest = state.scroll !== scrollAtRequest; -@@ -1788,6 +1580,27 @@ function prepareMVCP(ctx, dataChanged) { +- const didScrollSinceRequest = state.scroll !== scrollAtRequest; ++ const lastIssued = state.lastIssuedScrollOffset; ++ const isSettlingOntoIssuedScroll = lastIssued !== void 0 && Math.abs(state.scroll - lastIssued) <= EDGE_POSITION_EPSILON; ++ const didScrollSinceRequest = state.scroll !== scrollAtRequest && !isSettlingOntoIssuedScroll; + if (isStillWithinThreshold || !didScrollSinceRequest) { + state.maintainingScrollAtEnd = activeState; + const scroller = refScroller.current; +@@ -1788,6 +1582,27 @@ function prepareMVCP(ctx, dataChanged) { } } @@ -9172,7 +9477,7 @@ index 95465f2..cd8868b 100644 // src/utils/getScrollVelocity.ts var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2048,7 +1861,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { +@@ -2048,7 +1863,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { } } function scrollTo(ctx, params) { @@ -9181,7 +9486,7 @@ index 95465f2..cd8868b 100644 const state = ctx.state; const { noScrollingTo, forceScroll, ...scrollTarget } = params; const { -@@ -2070,6 +1883,7 @@ function scrollTo(ctx, params) { +@@ -2070,6 +1885,7 @@ function scrollTo(ctx, params) { if (!noScrollingTo) { if (isInitialScroll) { initialScrollCompletion.resetFlags(state); @@ -9189,7 +9494,7 @@ index 95465f2..cd8868b 100644 } const averageSizeSnapshot = getAverageSizeSnapshot(state); state.scrollingTo = { -@@ -2080,6 +1894,15 @@ function scrollTo(ctx, params) { +@@ -2080,6 +1896,15 @@ function scrollTo(ctx, params) { }; if (!isInitialScroll) { pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); @@ -9205,7 +9510,7 @@ index 95465f2..cd8868b 100644 } } state.scrollPending = targetOffset; -@@ -2087,7 +1910,7 @@ function scrollTo(ctx, params) { +@@ -2087,7 +1912,7 @@ function scrollTo(ctx, params) { if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { if (animated) { if (state.scrollTargetPinnedRange) { @@ -9214,7 +9519,7 @@ index 95465f2..cd8868b 100644 } } else { updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2100,6 +1923,316 @@ function scrollTo(ctx, params) { +@@ -2100,6 +1925,316 @@ function scrollTo(ctx, params) { } } @@ -9531,7 +9836,7 @@ index 95465f2..cd8868b 100644 // src/core/scrollToIndex.ts function clampScrollIndex(index, dataLength) { if (dataLength <= 0) { -@@ -4329,6 +4462,7 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4329,6 +4464,7 @@ function calculateItemsInView(ctx, params = {}) { startIndex }); totalSize = getContentSize(ctx); @@ -9539,7 +9844,7 @@ index 95465f2..cd8868b 100644 if (minIndexSizeChanged !== void 0) { state.minIndexSizeChanged = void 0; } -@@ -4346,8 +4480,18 @@ function calculateItemsInView(ctx, params = {}) { +@@ -4346,8 +4482,18 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); @@ -9560,7 +9865,7 @@ index 95465f2..cd8868b 100644 updateScroll2(state.scroll); updateScrollRange(); } -@@ -5985,6 +6129,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll +@@ -5985,6 +6131,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll // src/components/ListComponentScrollView.tsx var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; var SCROLL_END_FALLBACK_MS = 200; @@ -9569,7 +9874,7 @@ index 95465f2..cd8868b 100644 var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; function ensureScrollbarHiddenStyle() { if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6073,6 +6219,26 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6073,6 +6221,26 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ } return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; }, [getMaxScrollOffset, horizontal, isWindowScroll]); @@ -9596,7 +9901,7 @@ index 95465f2..cd8868b 100644 const scrollToLocalOffset = useCallback( (offset, animated) => { const scrollElement = scrollRef.current; -@@ -6080,29 +6246,32 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6080,29 +6248,34 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ if (!target || typeof target.scrollTo !== "function") { return; } @@ -9620,6 +9925,7 @@ index 95465f2..cd8868b 100644 - options.left = clampedOffset; - } else { - options.top = clampedOffset; ++ ctx.state.lastIssuedScrollOffset = clampOffset(offset, getMaxScrollOffset()); + target.scrollTo(options); + return; } @@ -9630,15 +9936,16 @@ index 95465f2..cd8868b 100644 + } else { + options.top = reachableOffset; + } ++ ctx.state.lastIssuedScrollOffset = reachableOffset; + target.scrollTo(options); + }); }, - [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] -+ [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable] ++ [ctx, getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable] ); useImperativeHandle(ref, () => { const api = { -@@ -6128,8 +6297,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ +@@ -6128,8 +6301,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ }, scrollToEnd: (options = {}) => { const { animated = true } = options; @@ -9648,7 +9955,42 @@ index 95465f2..cd8868b 100644 }, scrollToOffset: (params) => { const { offset, animated = true } = params; -@@ -8267,6 +8435,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6478,6 +6650,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize + return props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; + } + function setHeaderSize(ctx, size) { ++ var _a3; + const { state } = ctx; + const previousHeaderSize = peek$(ctx, "headerSize") || 0; + const didChange = previousHeaderSize !== size; +@@ -6487,6 +6660,8 @@ function setHeaderSize(ctx, size) { + updateContentMetricsState(ctx); + if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { + requestAdjust(ctx, size - previousHeaderSize); ++ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) { ++ doMaintainScrollAtEnd(ctx); + } + } + state.didMeasureHeader = true; +@@ -7549,13 +7724,14 @@ function getRenderedItem(ctx, key) { + + // src/utils/normalizeMaintainScrollAtEnd.ts + function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) { +- var _a3, _b, _c, _d; ++ var _a3, _b, _c, _d, _e; + return { + animated: false, + onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true, + onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true, +- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true, +- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true ++ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true, ++ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true, ++ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true + }; + } + function normalizeMaintainScrollAtEnd(value) { +@@ -8267,6 +8443,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScrollBeginDrag: (event) => { var _a4, _b2; prepareReachedEdgeForNextUserScroll(ctx); @@ -9656,3 +9998,37 @@ index 95465f2..cd8868b 100644 (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); }, onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) +diff --git a/node_modules/@legendapp/list/reanimated.d.ts b/node_modules/@legendapp/list/reanimated.d.ts +index e504332..99337c1 100644 +--- a/node_modules/@legendapp/list/reanimated.d.ts ++++ b/node_modules/@legendapp/list/reanimated.d.ts +@@ -489,6 +489,12 @@ interface AlwaysRenderConfig { + interface MaintainScrollAtEndOnOptions { + dataChange?: boolean; + footerLayout?: boolean; ++ /** ++ * Keep the list pinned to the end when the header changes size. A header that measures larger ++ * than its estimate pushes every item down, which leaves a list that had already scrolled to ++ * its end short of it by the difference. ++ */ ++ headerLayout?: boolean; + itemLayout?: boolean; + layout?: boolean; + } +diff --git a/node_modules/@legendapp/list/section-list.d.ts b/node_modules/@legendapp/list/section-list.d.ts +index a790f7f..9a3c2ef 100644 +--- a/node_modules/@legendapp/list/section-list.d.ts ++++ b/node_modules/@legendapp/list/section-list.d.ts +@@ -545,6 +545,12 @@ interface AlwaysRenderConfig { + interface MaintainScrollAtEndOnOptions { + dataChange?: boolean; + footerLayout?: boolean; ++ /** ++ * Keep the list pinned to the end when the header changes size. A header that measures larger ++ * than its estimate pushes every item down, which leaves a list that had already scrolled to ++ * its end short of it by the difference. ++ */ ++ headerLayout?: boolean; + itemLayout?: boolean; + layout?: boolean; + } From 8aa43c519af86f1e4aa0c1eeecfcfddd6e8ddaa1 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 12 Aug 2026 23:44:23 -0400 Subject: [PATCH 29/38] test(e2e): check that opening a thread lands on its newest message The regression this covers was reported by hand and would have gone on being reported by hand: nothing in either suite read where a thread came to rest. Two things give the flow teeth. Image responses are delayed, so rows grow after the initial scroll rather than before it - on a warm disk cache the same sweep is green with the bug in place. And retries are off for this flow: with the library fix disabled the first attempt failed and the retry passed, so the suite's default single retry would have hidden it. It also counts the threads whose content exceeded their viewport and fails if that count is zero, rather than passing by measuring nothing. Big-team channel rows had no testID, so the conversations with enough history to grow were unaddressable; they carry one now, next to the small-team row's. --- shared/chat/inbox/row/big-team-channel.tsx | 9 +- .../electron/flows/chat-thread-bottom.test.ts | 95 +++++++++++++++++++ shared/tests/e2e/shared/test-ids.ts | 1 + 3 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 shared/tests/e2e/electron/flows/chat-thread-bottom.test.ts diff --git a/shared/chat/inbox/row/big-team-channel.tsx b/shared/chat/inbox/row/big-team-channel.tsx index 186e30ebb7ce..bf8ebcefe249 100644 --- a/shared/chat/inbox/row/big-team-channel.tsx +++ b/shared/chat/inbox/row/big-team-channel.tsx @@ -3,6 +3,7 @@ import type * as React from 'react' import * as Kb from '@/common-adapters' import * as RowSizes from './sizes' import * as T from '@/constants/types' +import * as TestIDs from '@/tests/e2e/shared/test-ids' import {useInboxRowBig} from '@/chat/inbox/rows-state' type Props = { conversationIDKey: string @@ -92,7 +93,13 @@ const BigTeamChannel = (props: Props) => { ) : null return ( - + => + page.evaluate((testID: string) => { + type Scroller = {clientHeight: number; scrollHeight: number; scrollTop: number} + const doc = ( + globalThis as unknown as { + document?: {querySelector: (selector: string) => {firstElementChild?: Scroller | null} | null} + } + ).document + const scroller = doc?.querySelector(`[data-testid="${testID}"]`)?.firstElementChild + if (!scroller) return null + return { + clientHeight: Math.round(scroller.clientHeight), + distanceFromEnd: Math.round(scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop), + scrollHeight: Math.round(scroller.scrollHeight), + } + }, T.CHAT_MESSAGE_LIST) + +// No retries. The growth this depends on lands a frame or two after the list does, so it is timing +// dependent by nature - and a retry that passes hides the regression the flow exists for. Verified: +// with the library fix disabled the first attempt failed and the retry passed. +test.describe.configure({retries: 0}) + +test('opens every conversation on its newest message', async ({page}) => { + test.setTimeout(120_000) + await navigateToChat(page) + + // Routing every request so the handler can pick the images out; everything else continues + // untouched. Removed at the end because the page is shared with the rest of the suite. + await page.route('**/*', async route => { + if (route.request().resourceType() === 'image') { + await new Promise(resolve => setTimeout(resolve, IMAGE_DELAY_MS)) + } + await route.continue() + }) + + const checked: string[] = [] + try { + // Team channels as well as one-to-ones: the threads with enough history to grow after landing + // are mostly team channels, and the flow that found this bug was clicking through them. + const rows = page.locator( + `[data-testid="${T.CHAT_INBOX_CHANNEL_ROW}"], [data-testid="${T.CHAT_INBOX_ROW}"]` + ) + const rowCount = Math.min(await rows.count(), 25) + for (let i = 0; i < rowCount && checked.length < 5; i++) { + const row = rows.nth(i) + const name = (await row.innerText().catch(() => '')).split('\n')[0] ?? `row ${i}` + await row.click({force: true, timeout: 10_000}).catch(() => {}) + await page.waitForSelector(`[data-testid="${T.CHAT_MESSAGE_LIST}"]`, {timeout: 10_000}) + // Long enough for the delayed images to commit and for anything following the end to react. + await page.waitForTimeout(IMAGE_DELAY_MS + 2_500) + + const metrics = await readListMetrics(page) + if (!metrics || metrics.scrollHeight - metrics.clientHeight < MIN_SCROLLABLE_OVERFLOW) continue + checked.push(name) + expect( + metrics.distanceFromEnd, + `${name}: thread settled ${metrics.distanceFromEnd}px above its newest message (content ${metrics.scrollHeight}, viewport ${metrics.clientHeight})` + ).toBeLessThanOrEqual(MAX_DISTANCE_FROM_END) + } + } finally { + await page.unroute('**/*') + } + + // Without this the loop above passes by never measuring a thread long enough to fail. + expect(checked.length, 'no conversation had more content than its viewport, so nothing was checked').toBeGreaterThan( + 0 + ) +}) diff --git a/shared/tests/e2e/shared/test-ids.ts b/shared/tests/e2e/shared/test-ids.ts index f5bb5e4bce0f..c01658038632 100644 --- a/shared/tests/e2e/shared/test-ids.ts +++ b/shared/tests/e2e/shared/test-ids.ts @@ -14,6 +14,7 @@ export const NAV_TAB_SETTINGS = 'nav-tab-settings' // Chat export const CHAT_INBOX_LIST = 'chat-inbox-list' export const CHAT_INBOX_ROW = 'chat-inbox-row' +export const CHAT_INBOX_CHANNEL_ROW = 'chat-inbox-channel-row' export const CHAT_MESSAGE_LIST = 'chat-message-list' export const CHAT_INPUT = 'chat-input' export const CHAT_SEND_BUTTON = 'chat-send-button' From b239d85835ed2065ade553793797a0bc3962b38d Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 18 Aug 2026 15:14:45 -0400 Subject: [PATCH 30/38] fix(chat): rebuild the legend-list patch against 3.3.7 Master moved @legendapp/list from 3.3.5 to 3.3.7 while this branch was out. Our fork branch was still sitting on the 3.3.5 tree, so the patch this branch carried no longer matched what gets installed. Fast-forwarded the fork to v3.3.7, reapplied the scroll-target settle work on top (it merged without conflict), rebuilt dist, and regenerated the patch. Upstream's own 3.3.6/3.3.7 scroll fixes are kept. --- ....3.5.patch => @legendapp+list+3.3.7.patch} | 13500 ++++++++-------- 1 file changed, 6981 insertions(+), 6519 deletions(-) rename shared/patches/{@legendapp+list+3.3.5.patch => @legendapp+list+3.3.7.patch} (55%) diff --git a/shared/patches/@legendapp+list+3.3.5.patch b/shared/patches/@legendapp+list+3.3.7.patch similarity index 55% rename from shared/patches/@legendapp+list+3.3.5.patch rename to shared/patches/@legendapp+list+3.3.7.patch index 3c66b008d707..0afd47f5781a 100644 --- a/shared/patches/@legendapp+list+3.3.5.patch +++ b/shared/patches/@legendapp+list+3.3.7.patch @@ -1,46 +1,8 @@ -diff --git a/node_modules/@legendapp/list/.DS_Store b/node_modules/@legendapp/list/.DS_Store -deleted file mode 100644 -index b86e710..0000000 -Binary files a/node_modules/@legendapp/list/.DS_Store and /dev/null differ -diff --git a/node_modules/@legendapp/list/animated.d.ts b/node_modules/@legendapp/list/animated.d.ts -index 14beb98..56eefce 100644 ---- a/node_modules/@legendapp/list/animated.d.ts -+++ b/node_modules/@legendapp/list/animated.d.ts -@@ -488,6 +488,12 @@ interface AlwaysRenderConfig { - interface MaintainScrollAtEndOnOptions { - dataChange?: boolean; - footerLayout?: boolean; -+ /** -+ * Keep the list pinned to the end when the header changes size. A header that measures larger -+ * than its estimate pushes every item down, which leaves a list that had already scrolled to -+ * its end short of it by the difference. -+ */ -+ headerLayout?: boolean; - itemLayout?: boolean; - layout?: boolean; - } -diff --git a/node_modules/@legendapp/list/react-native.d.ts b/node_modules/@legendapp/list/react-native.d.ts -index ce1fe00..9a4311c 100644 ---- a/node_modules/@legendapp/list/react-native.d.ts -+++ b/node_modules/@legendapp/list/react-native.d.ts -@@ -488,6 +488,12 @@ interface AlwaysRenderConfig { - interface MaintainScrollAtEndOnOptions { - dataChange?: boolean; - footerLayout?: boolean; -+ /** -+ * Keep the list pinned to the end when the header changes size. A header that measures larger -+ * than its estimate pushes every item down, which leaves a list that had already scrolled to -+ * its end short of it by the difference. -+ */ -+ headerLayout?: boolean; - itemLayout?: boolean; - layout?: boolean; - } diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js -index b3c5a30..9d7ad6d 100644 +index 6f41da3..0d7357b 100644 --- a/node_modules/@legendapp/list/react-native.js +++ b/node_modules/@legendapp/list/react-native.js -@@ -457,611 +457,6 @@ var EDGE_POSITION_EPSILON = 1; +@@ -458,171 +458,279 @@ var EDGE_POSITION_EPSILON = 1; var ENABLE_DEVMODE = IS_DEV && false; var ENABLE_DEBUG_VIEW = IS_DEV && false; @@ -50,22 +12,68 @@ index b3c5a30..9d7ad6d 100644 - scheduledWork.cancel("checkFinishedScrollRetryFrame"); - scheduledWork.cancel("checkFinishedScrollFallback"); - scheduledWork.cancel("platformScrollCompletion"); --} ++// src/core/getStartOffsetAdjustment.ts ++function getStartOffsetAdjustment(ctx) { ++ const { state } = ctx; ++ const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; ++ return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); + } -function settlePendingImperativeScroll(state) { - var _a3, _b; - const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; - state.pendingScrollResolve = void 0; - state.pendingScrollToEnd = void 0; - resolvePendingScroll == null ? void 0 : resolvePendingScroll(); --} ++ ++// src/utils/getId.ts ++function getId(state, index) { ++ const { data, keyExtractor } = state.props; ++ if (!data) { ++ return ""; ++ } ++ const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; ++ const id = ret; ++ state.idCache[index] = id; ++ return id; + } -function cancelImperativeScroll(state) { - cancelScrollCompletionChecks(state); - state.scheduledWork.cancel("imperativeScrollReady"); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; - settlePendingImperativeScroll(state); --} -- ++ ++// src/core/updateContentMetricsState.ts ++function getRawContentLength(ctx) { ++ var _a3, _b, _c; ++ const { state, values } = ctx; ++ return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); ++} ++function getAlignItemsAtEndPadding(ctx) { ++ const { state } = ctx; ++ const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; ++ return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; ++} ++function updateContentMetricsState(ctx) { ++ var _a3; ++ const { state } = ctx; ++ const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; ++ const nextPadding = getAlignItemsAtEndPadding(ctx); ++ if (previousPadding !== nextPadding) { ++ const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated"; ++ const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold")); ++ if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) { ++ state.scheduledWork.microtask(() => { ++ if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) { ++ set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx)); ++ } ++ }, "alignItemsAtEndPaddingFallback"); ++ } else if (!isMaintainExpected) { ++ set$(ctx, "alignItemsAtEndPadding", nextPadding); ++ } ++ } + } + -// src/core/deferredPublicOnScroll.ts -function withResolvedContentOffset(state, event, resolvedOffset) { - return { @@ -73,12 +81,42 @@ index b3c5a30..9d7ad6d 100644 - nativeEvent: { - ...event.nativeEvent, - contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -- } ++// src/core/addTotalSize.ts ++function addTotalSize(ctx, key, add, notifyTotalSize = true) { ++ const state = ctx.state; ++ const prevTotalSize = state.totalSize; ++ let totalSize = state.totalSize; ++ if (key === null) { ++ totalSize = add; ++ if (state.timeoutSetPaddingTop) { ++ clearTimeout(state.timeoutSetPaddingTop); ++ state.timeoutSetPaddingTop = void 0; + } - }; --} ++ } else { ++ totalSize += add; ++ } ++ if (prevTotalSize !== totalSize) { ++ if (!IsNewArchitecture && state.initialScroll && totalSize < prevTotalSize) { ++ state.pendingTotalSize = totalSize; ++ } else { ++ state.pendingTotalSize = void 0; ++ state.totalSize = totalSize; ++ if (notifyTotalSize) { ++ set$(ctx, "totalSize", totalSize); ++ } ++ updateContentMetricsState(ctx); ++ } ++ } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { ++ set$(ctx, "totalSize", totalSize); ++ } + } -function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { - var _a3, _b, _c, _d; -- const state = ctx.state; ++ ++// src/core/setSize.ts ++function setSize(ctx, itemKey, size, notifyTotalSize = true) { + const state = ctx.state; - const deferredEvent = state.deferredPublicOnScrollEvent; - state.deferredPublicOnScrollEvent = void 0; - if (deferredEvent) { @@ -90,18 +128,29 @@ index b3c5a30..9d7ad6d 100644 - (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 - ) - ); -- } --} -- ++ const { sizes } = state; ++ const previousSize = sizes.get(itemKey); ++ const diff = previousSize !== void 0 ? size - previousSize : size; ++ if (diff !== 0) { ++ addTotalSize(ctx, itemKey, diff, notifyTotalSize); + } ++ sizes.set(itemKey, size); + } + -// src/core/initialScrollSession.ts -var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -function hasInitialScrollSessionCompletion(completion) { - return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); --} ++// src/utils/helpers.ts ++function isFunction(obj) { ++ return typeof obj === "function"; + } -function clearInitialScrollSession(state) { - state.initialScrollSession = void 0; - return void 0; --} ++function isArray(obj) { ++ return Array.isArray(obj); + } -function createInitialScrollSession(options) { - const { bootstrap, completion, kind, previousDataLength } = options; - return kind === "offset" ? { @@ -114,7 +163,13 @@ index b3c5a30..9d7ad6d 100644 - kind, - previousDataLength - }; --} ++var warned = /* @__PURE__ */ new Set(); ++function warnDevOnce(id, text) { ++ if (IS_DEV && !warned.has(id)) { ++ warned.add(id); ++ console.warn(`[legend-list] ${text}`); ++ } + } -function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { - var _a4, _b2; - if (!state.initialScrollSession) { @@ -130,10 +185,37 @@ index b3c5a30..9d7ad6d 100644 - kind, - previousDataLength: state.initialScrollSession.previousDataLength - }); -- } ++function roundSize(size) { ++ return Math.floor(size * 8) / 8; ++} ++function isNullOrUndefined(value) { ++ return value === null || value === void 0; ++} ++function getPadding(s, type) { ++ var _a3, _b, _c; ++ const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; ++ return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; ++} ++function extractPadding(style, contentContainerStyle, type) { ++ return getPadding(style, type) + getPadding(contentContainerStyle, type); ++} ++function findContainerId(ctx, key) { ++ var _a3, _b; ++ const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); ++ if (directMatch !== void 0) { ++ return directMatch; + } - (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; - return state.initialScrollSession.completion; --} ++ const numContainers = peek$(ctx, "numContainers"); ++ for (let i = 0; i < numContainers; i++) { ++ const itemKey = peek$(ctx, `containerItemKey${i}`); ++ if (itemKey === key) { ++ return i; ++ } ++ } ++ return -1; + } -var initialScrollCompletion = { - didDispatchNativeScroll(state) { - var _a3, _b; @@ -152,11 +234,24 @@ index b3c5a30..9d7ad6d 100644 - resetFlags(state) { - if (!state.initialScrollSession) { - return; -- } ++ ++// src/utils/getItemSize.ts ++function getKnownOrFixedSize(ctx, key, index, data, resolved) { ++ var _a3, _b; ++ const state = ctx.state; ++ const { getFixedItemSize, getItemType } = state.props; ++ let size = key ? state.sizesKnown.get(key) : void 0; ++ if (size === void 0 && key && getFixedItemSize) { ++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; ++ const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); ++ if (fixedSize !== void 0) { ++ size = fixedSize + ctx.scrollAxisGap; ++ state.sizesKnown.set(key, size); + } - const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); - completion.didDispatchNativeScroll = void 0; - completion.didRetrySilentInitialScroll = void 0; -- } + } -}; -var initialScrollWatchdog = { - clear(state) { @@ -180,16 +275,29 @@ index b3c5a30..9d7ad6d 100644 - var _a3, _b; - if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { - return; -- } ++ return size; ++} ++function getKnownOrFixedItemSize(ctx, index) { ++ const key = getId(ctx.state, index); ++ return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); ++} ++function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { ++ for (let index = startIndex; index <= endIndex; index++) { ++ if (getKnownOrFixedItemSize(ctx, index) === void 0) { ++ return false; + } - const completion = ensureInitialScrollSessionCompletion(state); - completion.watchdog = watchdog ? { - startScroll: watchdog.startScroll, - targetOffset: watchdog.targetOffset - } : void 0; -- } + } -}; -function setInitialScrollSession(state, options = {}) { -- var _a3, _b, _c, _d; ++ return true; ++} ++function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { + var _a3, _b, _c, _d; - const existingSession = state.initialScrollSession; - const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; - const completion = existingSession == null ? void 0 : existingSession.completion; @@ -197,10 +305,26 @@ index b3c5a30..9d7ad6d 100644 - const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; - if (!kind) { - return clearInitialScrollSession(state); -- } ++ const state = ctx.state; ++ const { ++ sizes, ++ averageSizes, ++ props: { estimatedItemSize, getItemType }, ++ scrollingTo ++ } = state; ++ const sizeKnown = state.sizesKnown.get(key); ++ if (sizeKnown !== void 0) { ++ return sizeKnown; + } - if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { - return clearInitialScrollSession(state); -- } ++ let size; ++ const renderedSize = sizes.get(key); ++ if (preferCachedSize) { ++ if (renderedSize !== void 0) { ++ return renderedSize; ++ } + } - const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; - state.initialScrollSession = createInitialScrollSession({ - bootstrap, @@ -209,201 +333,102 @@ index b3c5a30..9d7ad6d 100644 - previousDataLength - }); - return state.initialScrollSession; --} -- --// src/utils/checkThreshold.ts --var HYSTERESIS_MULTIPLIER = 1.3; --function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { -- const absDistance = Math.abs(distance); -- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; --} --var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { -- const absDistance = Math.abs(distance); -- const within = atThreshold || threshold > 0 && absDistance <= threshold; -- const updateSnapshot = () => { -- setSnapshot({ -- atThreshold, -- contentSize: context.contentSize, -- dataLength: context.dataLength, -- scrollPosition: context.scrollPosition -- }); -- }; -- if (!wasReached) { -- if (!within) { -- return false; -- } -- onReached(distance); -- updateSnapshot(); -- return true; -- } -- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); -- if (reset) { -- setSnapshot(void 0); -- return false; -- } -- if (within) { -- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; -- if (changed) { -- updateSnapshot(); -- } -- } -- return true; --}; -- --// src/utils/edgeReachedGate.ts --function resetEdgeLatch(ctx, edge) { -- const state = ctx.state; -- if (edge === "start") { -- state.isStartReached = false; -- state.startReachedSnapshot = void 0; -- } else { -- state.isEndReached = false; -- state.endReachedSnapshot = void 0; -- } --} --function resetSharedEdgeGateIfOutsideHysteresis(ctx) { -- const state = ctx.state; -- if (!state.edgeReachedGate) { -- return; -- } -- const contentSize = getContentSize(ctx); -- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); -- const isContentLess = contentSize < state.scrollLength; -- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; -- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; -- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); -- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); -- if (isOutsideStart && isOutsideEnd) { -- state.edgeReachedGate = void 0; -- } --} --function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { -- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; --} --function markReachedEdge(ctx) { -- ctx.state.edgeReachedGate = "closed"; --} --function prepareReachedEdgeForNextUserScroll(ctx) { -- if (ctx.state.edgeReachedGate) { -- ctx.state.edgeReachedGate = "prepared"; -- } --} --function beginReachedEdgeUserScroll(ctx, scrollDelta) { -- const state = ctx.state; -- if (state.edgeReachedGate !== "prepared") { -- return void 0; -- } -- const allowedEdge = scrollDelta < 0 ? "start" : "end"; -- state.edgeReachedGate = "closed"; -- resetEdgeLatch(ctx, allowedEdge); -- return allowedEdge; --} -- --// src/utils/hasActiveInitialScroll.ts --function hasActiveInitialScroll(state) { -- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; --} -- --// src/utils/checkAtBottom.ts --function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -- var _a3; -- const state = ctx.state; -- if (!state) { -- return; -- } -- const { -- queuedInitialLayout, -- scrollLength, -- scroll, -- maintainingScrollAtEnd, -- props: { maintainScrollAtEndThreshold, onEndReachedThreshold } -- } = state; -- const contentSize = getContentSize(ctx); -- resetSharedEdgeGateIfOutsideHysteresis(ctx); -- if (contentSize > 0 && queuedInitialLayout) { -- const insetEnd = getContentInsetEnd(ctx); -- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; -- const isContentLess = contentSize < scrollLength; -- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); -- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); -- set$( -- ctx, -- "isWithinMaintainScrollAtEndThreshold", -- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength -- ); -- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; -- if (!shouldSkipThresholdChecks) { -- state.isEndReached = checkThreshold( -- distanceFromEnd, -- isContentLess, -- onEndReachedThreshold * scrollLength, -- state.isEndReached, -- state.endReachedSnapshot, -- { -- contentSize, -- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, -- scrollPosition: scroll -- }, -- (distance) => { -- var _a4, _b; -- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { -- markReachedEdge(ctx); -- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); -- } -- }, -- (snapshot) => { -- state.endReachedSnapshot = snapshot; -- } -- ); -- } -- } --} -- --// src/utils/checkAtTop.ts --function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -- const state = ctx == null ? void 0 : ctx.state; -- if (!state) { -- return; -- } -- const { -- isStartReached, -- props: { data, onStartReachedThreshold }, -- scroll, -- scrollLength, -- startReachedSnapshot, -- totalSize -- } = state; -- const dataLength = data.length; -- const threshold = onStartReachedThreshold * scrollLength; -- resetSharedEdgeGateIfOutsideHysteresis(ctx); -- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { -- state.isStartReached = false; -- state.startReachedSnapshot = void 0; -- } -- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); -- set$(ctx, "isNearStart", scroll <= threshold); -- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; -- if (!shouldSkipThresholdChecks) { -- state.isStartReached = checkThreshold( -- scroll, -- false, -- threshold, -- state.isStartReached, -- startReachedSnapshot, -- { -- contentSize: totalSize, -- dataLength, -- scrollPosition: scroll -- }, -- (distance) => { -- var _a3, _b; -- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { -- markReachedEdge(ctx); -- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); -- } -- }, -- (snapshot) => { -- state.startReachedSnapshot = snapshot; ++ size = getKnownOrFixedSize(ctx, key, index, data, resolved); ++ if (size !== void 0) { ++ setSize(ctx, key, size, notifyTotalSize); ++ return size; ++ } ++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; ++ if (useAverageSize && !scrollingTo) { ++ const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; ++ if (averageSizeForType !== void 0) { ++ size = roundSize(averageSizeForType); ++ } ++ } ++ if (size === void 0 && renderedSize !== void 0) { ++ return renderedSize; ++ } ++ if (size === void 0 && useAverageSize && scrollingTo) { ++ const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; ++ if (averageSizeForType !== void 0) { ++ size = roundSize(averageSizeForType); ++ } ++ } ++ if (size === void 0) { ++ size = estimatedItemSize + ctx.scrollAxisGap; ++ } ++ setSize(ctx, key, size, notifyTotalSize); ++ return size; ++} ++function getItemSizeAtIndex(ctx, index) { ++ if (index === void 0 || index < 0) { ++ return void 0; ++ } ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; ++ } ++ if (index !== void 0) { ++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); ++ if (startOffsetAdjustment) { ++ offset += startOffsetAdjustment; ++ } ++ } ++ if (viewPosition !== void 0 && index !== void 0) { ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return offset; ++ } ++ const isOutOfBounds = index < 0 || index >= dataLength; ++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; ++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); ++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); ++ const trailingInset = getContentInsetEnd(ctx); ++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); ++ if (!isOutOfBounds && index === state.props.data.length - 1) { ++ const footerSize = peek$(ctx, "footerSize") || 0; ++ offset += footerSize; ++ } ++ } ++ return offset; ++} ++ ++// src/core/clampScrollOffset.ts ++function clampScrollOffset(ctx, offset, scrollTarget) { ++ const state = ctx.state; ++ const contentSize = getContentSize(ctx); ++ let clampedOffset = offset; ++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android" || state.lastLayout)) { ++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); ++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; ++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; ++ const maxOffset = baseMaxOffset + extraEndOffset; ++ clampedOffset = Math.min(offset, maxOffset); ++ } ++ clampedOffset = Math.max(0, clampedOffset); ++ return clampedOffset; ++} ++ ++// src/core/calculateOffsetForIndex.ts ++function calculateOffsetForIndex(ctx, index) { ++ const state = ctx.state; ++ return index !== void 0 ? state.positions[index] || 0 : 0; + } + + // src/utils/checkThreshold.ts +@@ -824,748 +932,760 @@ function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { + }, + (snapshot) => { + state.startReachedSnapshot = snapshot; - } - ); - } @@ -443,8 +468,10 @@ index b3c5a30..9d7ad6d 100644 - setAdaptiveRender(ctx, "normal", "scroll"); - } else { - state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); -- } --} ++ } ++ ); + } + } -function setAdaptiveRender(ctx, mode, reason) { - var _a3, _b; - const previousMode = peek$(ctx, "adaptiveRender"); @@ -452,18 +479,35 @@ index b3c5a30..9d7ad6d 100644 - set$(ctx, "adaptiveRender", mode); - (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); - } --} ++ ++// src/utils/checkThresholds.ts ++function checkThresholds(ctx, allowedEdge) { ++ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; ++ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); ++ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + } -function resetAdaptiveRender(ctx) { -- var _a3, _b; ++ ++// src/core/recalculateSettledScroll.ts ++function recalculateSettledScroll(ctx) { + var _a3, _b; - clearAdaptiveRenderExitTimeout(ctx); - const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; - if (peek$(ctx, "adaptiveRender") !== mode) { - setAdaptiveRender(ctx, mode, "initial"); -- } --} ++ const state = ctx.state; ++ if ((_a3 = state.props) == null ? void 0 : _a3.data) { ++ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); + } ++ checkThresholds(ctx); + } -function updateAdaptiveRender(ctx, scrollVelocity, options) { - var _a3, _b, _c; -- const state = ctx.state; ++ ++// src/core/finishScrollTo.ts ++function finishScrollTo(ctx) { ++ var _a3, _b; + const state = ctx.state; - const adaptiveRender = state.props.adaptiveRender; - const currentMode = peek$(ctx, "adaptiveRender"); - if (peek$(ctx, "readyToRender")) { @@ -485,30 +529,98 @@ index b3c5a30..9d7ad6d 100644 - } - } else { - resetAdaptiveRender(ctx); -- } -- } --} -- --// src/utils/getEffectiveDrawDistance.ts --var INITIAL_DRAW_DISTANCE = 50; --function getEffectiveDrawDistance(ctx, mode) { -- var _a3; -- const drawDistance = ctx.state.props.drawDistance; -- const initialScroll = ctx.state.initialScroll; ++ if (state == null ? void 0 : state.scrollingTo) { ++ cancelScrollCompletionChecks(state); ++ const resolvePendingScroll = state.pendingScrollResolve; ++ state.pendingScrollResolve = void 0; ++ const scrollingTo = state.scrollingTo; ++ state.scrollHistory.length = 0; ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ if (state.pendingTotalSize !== void 0) { ++ addTotalSize(ctx, null, state.pendingTotalSize); + } ++ if (PlatformAdjustBreaksScroll) { ++ state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); ++ } ++ if (scrollingTo.isInitialScroll || state.initialScroll) { ++ const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; ++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; ++ finishInitialScroll(ctx, { ++ onFinished: () => { ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); ++ }, ++ preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, ++ recalculateItems: true, ++ schedulePreservedTargetClear: shouldPreserveResizeTarget, ++ syncObservedOffset: isOffsetSession, ++ waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame ++ }); ++ return; ++ } ++ recalculateSettledScroll(ctx); ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); + } + } + +-// src/utils/getEffectiveDrawDistance.ts +-var INITIAL_DRAW_DISTANCE = 50; +-function getEffectiveDrawDistance(ctx, mode) { +- var _a3; +- const drawDistance = ctx.state.props.drawDistance; +- const initialScroll = ctx.state.initialScroll; - const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; - const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; - return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; --} ++// src/core/initialScrollSession.ts ++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; ++function hasInitialScrollSessionCompletion(completion) { ++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); + } -function scheduleFullDrawDistancePrewarm(ctx) { - const { state } = ctx; - if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { - return; -- } ++function clearInitialScrollSession(state) { ++ state.initialScrollSession = void 0; ++ return void 0; ++} ++function createInitialScrollSession(options) { ++ const { bootstrap, completion, kind, previousDataLength } = options; ++ return kind === "offset" ? { ++ completion, ++ kind, ++ previousDataLength ++ } : { ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }; ++} ++function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { ++ var _a4, _b2; ++ if (!state.initialScrollSession) { ++ state.initialScrollSession = createInitialScrollSession({ ++ completion: {}, ++ kind, ++ previousDataLength: 0 ++ }); ++ } else if (state.initialScrollSession.kind !== kind) { ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, ++ completion: state.initialScrollSession.completion, ++ kind, ++ previousDataLength: state.initialScrollSession.previousDataLength ++ }); + } - state.scheduledWork.frame(() => { - var _a3; - return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); - }, "fullDrawDistancePrewarm"); --} ++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; ++ return state.initialScrollSession.completion; + } - -// src/utils/setInitialRenderState.ts -function resetInitialRenderState(ctx, { @@ -519,10 +631,61 @@ index b3c5a30..9d7ad6d 100644 - if (resetLayout) { - state.didContainersLayout = false; - state.queuedInitialLayout = false; -- } ++var initialScrollCompletion = { ++ didDispatchNativeScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); ++ }, ++ didRetrySilentInitialScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); ++ }, ++ markInitialScrollNativeDispatch(state) { ++ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; ++ }, ++ markSilentInitialScrollRetry(state) { ++ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; ++ }, ++ resetFlags(state) { ++ if (!state.initialScrollSession) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); ++ completion.didDispatchNativeScroll = void 0; ++ completion.didRetrySilentInitialScroll = void 0; + } - if (resetInitialScroll) { - state.didFinishInitialScroll = false; -- } ++}; ++var initialScrollWatchdog = { ++ clear(state) { ++ initialScrollWatchdog.set(state, void 0); ++ }, ++ didReachTarget(newScroll, watchdog) { ++ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); ++ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ get(state) { ++ var _a3, _b; ++ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; ++ }, ++ hasNonZeroTargetOffset(targetOffset) { ++ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ isAtZeroTargetOffset(targetOffset) { ++ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ set(state, watchdog) { ++ var _a3, _b; ++ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state); ++ completion.watchdog = watchdog ? { ++ startScroll: watchdog.startScroll, ++ targetOffset: watchdog.targetOffset ++ } : void 0; + } - set$(ctx, "readyToRender", false); - resetAdaptiveRender(ctx); -} @@ -537,25 +700,61 @@ index b3c5a30..9d7ad6d 100644 - } = state; - if (didLayout) { - state.didContainersLayout = true; -- } ++}; ++function setInitialScrollSession(state, options = {}) { ++ var _a3, _b, _c, _d; ++ const existingSession = state.initialScrollSession; ++ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; ++ const completion = existingSession == null ? void 0 : existingSession.completion; ++ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; ++ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; ++ if (!kind) { ++ return clearInitialScrollSession(state); + } - if (didInitialScroll) { - state.didFinishInitialScroll = true; -- } ++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { ++ return clearInitialScrollSession(state); + } - const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); - if (isReadyToRender && !peek$(ctx, "readyToRender")) { - set$(ctx, "readyToRender", true); - setAdaptiveRender(ctx, "normal", "ready"); - if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { - scheduleFullDrawDistancePrewarm(ctx); -- } ++ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }); ++ return state.initialScrollSession; ++} ++ ++// src/core/checkFinishedScroll.ts ++var INITIAL_SCROLL_MAX_FALLBACK_CHECKS = 20; ++var INITIAL_SCROLL_COMPLETION_TARGET_EPSILON = 1; ++var INITIAL_SCROLL_ZERO_TARGET_EPSILON = 1; ++var SILENT_INITIAL_SCROLL_RETRY_DELAY_MS = 16; ++var SILENT_INITIAL_SCROLL_TARGET_EPSILON = 1; ++function checkFinishedScroll(ctx, options) { ++ const scrollingTo = ctx.state.scrollingTo; ++ if (options == null ? void 0 : options.onlyIfAligned) { ++ if (!(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) || scrollingTo.animated) { ++ return; + } - if (!state.didLoad) { - state.didLoad = true; - if (onLoad) { - onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); - } -- } -- } --} ++ if (!getResolvedScrollCompletionState(ctx, scrollingTo).isAtResolvedTarget) { ++ return; + } + } ++ ctx.state.scheduledWork.frame(() => checkFinishedScrollFrame(ctx), "checkFinishedScrollFrame"); + } - -// src/core/finishInitialScroll.ts -var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -563,16 +762,24 @@ index b3c5a30..9d7ad6d 100644 - state.scroll = offset; - state.scrollPending = offset; - state.scrollPrev = offset; --} ++function hasScrollCompletionOwnership(state, options) { ++ const { clampedTargetOffset, scrollingTo } = options; ++ return !scrollingTo.isInitialScroll || state.hasScrolled || clampedTargetOffset <= INITIAL_SCROLL_COMPLETION_TARGET_EPSILON; + } -function clearPreservedInitialScrollTargetTimeout(state) { - state.scheduledWork.cancel("preservedInitialScroll"); --} ++function isSilentInitialDispatch(state, scrollingTo) { ++ return !!(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) && initialScrollCompletion.didDispatchNativeScroll(state) && !state.hasScrolled; + } -function clearPreservedInitialScrollTarget(state) { - clearPreservedInitialScrollTargetTimeout(state); - state.clearPreservedInitialScrollOnNextFinish = void 0; - state.initialScroll = void 0; - setInitialScrollSession(state); --} ++function getInitialScrollWatchdogTargetOffset(state) { ++ var _a3; ++ return (_a3 = initialScrollWatchdog.get(state)) == null ? void 0 : _a3.targetOffset; + } -function supersedeInitialScroll(ctx) { - var _a3, _b, _c; - const state = ctx.state; @@ -590,7 +797,10 @@ index b3c5a30..9d7ad6d 100644 - setInitialScrollSession(state, { bootstrap: null }); - finishInitialScroll(ctx); - } --} ++function isNativeInitialNonZeroTarget(state) { ++ const targetOffset = getInitialScrollWatchdogTargetOffset(state); ++ return !state.didFinishInitialScroll && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); + } -function finishInitialScroll(ctx, options) { - var _a3, _b, _c; - const state = ctx.state; @@ -601,7 +811,11 @@ index b3c5a30..9d7ad6d 100644 - if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { - syncInitialScrollOffset(state, observedOffset); - } -- } ++function shouldFinishInitialScrollWithoutNativeProgress(state, scrollingTo) { ++ var _a3, _b; ++ if (!scrollingTo.isInitialScroll || scrollingTo.animated || !state.didContainersLayout) { ++ return false; + } - const complete = () => { - var _a4, _b2, _c2, _d, _e; - const shouldReleaseDeferredPublicOnScroll = Platform.OS === "web" && ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; @@ -639,7 +853,9 @@ index b3c5a30..9d7ad6d 100644 - if (options == null ? void 0 : options.waitForCompletionFrame) { - requestAnimationFrame(complete); - return; -- } ++ if (((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap") { ++ return false; + } - complete(); -} - @@ -647,279 +863,435 @@ index b3c5a30..9d7ad6d 100644 -function calculateOffsetForIndex(ctx, index) { - const state = ctx.state; - return index !== void 0 ? state.positions[index] || 0 : 0; --} ++ const targetOffset = (_b = scrollingTo.targetOffset) != null ? _b : scrollingTo.offset; ++ if (initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset) && initialScrollCompletion.didDispatchNativeScroll(state) && !state.hasScrolled) { ++ return false; ++ } ++ if (initialScrollWatchdog.isAtZeroTargetOffset(targetOffset) || Math.abs(state.scroll - targetOffset) > 1 || Math.abs(state.scrollPending - targetOffset) > 1) { ++ return false; ++ } ++ return !!scrollingTo.waitForInitialScrollCompletionFrame || isNativeInitialNonZeroTarget(state); + } - - // src/core/getStartOffsetAdjustment.ts - function getStartOffsetAdjustment(ctx) { +-// src/core/getStartOffsetAdjustment.ts +-function getStartOffsetAdjustment(ctx) { ++function shouldFinishInitialZeroTargetScroll(ctx) { ++ var _a3; const { state } = ctx; -@@ -1265,58 +660,279 @@ function getItemSizeAtIndex(ctx, index) { - if (index === void 0 || index < 0) { - return void 0; - } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+ const targetId = getId(ctx.state, index); -+ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+} -+ -+// src/core/calculateOffsetWithOffsetPosition.ts -+function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; +- return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); ++ return !!((_a3 = state.scrollingTo) == null ? void 0 : _a3.isInitialScroll) && state.props.data.length > 0 && getContentSize(ctx) <= state.scrollLength && state.scrollPending <= INITIAL_SCROLL_ZERO_TARGET_EPSILON; + } +- +-// src/utils/getId.ts +-function getId(state, index) { +- const { data, keyExtractor } = state.props; +- if (!data) { +- return ""; +- } +- const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; +- const id = ret; +- state.idCache[index] = id; +- return id; ++function isEndAlignedLastItemTarget(ctx, scrollingTo) { ++ return scrollingTo.index === ctx.state.props.data.length - 1 && scrollingTo.viewPosition === 1; + } +- +-// src/core/updateContentMetricsState.ts +-function getRawContentLength(ctx) { +- var _a3, _b, _c; +- const { state, values } = ctx; +- return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); ++function getCurrentTargetOffset(ctx, scrollingTo) { + var _a3; -+ const state = ctx.state; -+ const { index, viewOffset, viewPosition } = params; -+ let offset = offsetParam; -+ if (viewOffset) { -+ offset -= viewOffset; -+ } -+ if (index !== void 0) { -+ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -+ if (startOffsetAdjustment) { -+ offset += startOffsetAdjustment; -+ } -+ } -+ if (viewPosition !== void 0 && index !== void 0) { -+ const dataLength = state.props.data.length; -+ if (dataLength === 0) { -+ return offset; -+ } -+ const isOutOfBounds = index < 0 || index >= dataLength; -+ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -+ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -+ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -+ const trailingInset = getContentInsetEnd(ctx); -+ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -+ if (!isOutOfBounds && index === state.props.data.length - 1) { -+ const footerSize = peek$(ctx, "footerSize") || 0; -+ offset += footerSize; -+ } -+ } -+ return offset; -+} -+ -+// src/core/clampScrollOffset.ts -+function clampScrollOffset(ctx, offset, scrollTarget) { -+ const state = ctx.state; -+ const contentSize = getContentSize(ctx); -+ let clampedOffset = offset; -+ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android" || state.lastLayout)) { -+ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); -+ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; -+ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; -+ const maxOffset = baseMaxOffset + extraEndOffset; -+ clampedOffset = Math.min(offset, maxOffset); -+ } -+ clampedOffset = Math.max(0, clampedOffset); -+ return clampedOffset; -+} -+ -+// src/core/calculateOffsetForIndex.ts -+function calculateOffsetForIndex(ctx, index) { -+ const state = ctx.state; -+ return index !== void 0 ? state.positions[index] || 0 : 0; -+} -+ -+// src/utils/checkThreshold.ts -+var HYSTERESIS_MULTIPLIER = 1.3; -+function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { -+ const absDistance = Math.abs(distance); -+ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; -+} -+var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { -+ const absDistance = Math.abs(distance); -+ const within = atThreshold || threshold > 0 && absDistance <= threshold; -+ const updateSnapshot = () => { -+ setSnapshot({ -+ atThreshold, -+ contentSize: context.contentSize, -+ dataLength: context.dataLength, -+ scrollPosition: context.scrollPosition -+ }); ++ const index = scrollingTo.index; ++ const shouldRecomputeEndTarget = isEndAlignedLastItemTarget(ctx, scrollingTo); ++ const requestedTargetOffset = shouldRecomputeEndTarget && index !== void 0 ? calculateOffsetWithOffsetPosition(ctx, calculateOffsetForIndex(ctx, index), scrollingTo) : (_a3 = scrollingTo.targetOffset) != null ? _a3 : clampScrollOffset(ctx, scrollingTo.offset - (scrollingTo.viewOffset || 0), scrollingTo); ++ return clampScrollOffset(ctx, requestedTargetOffset, scrollingTo); + } +-function getAlignItemsAtEndPadding(ctx) { ++function getResolvedScrollCompletionState(ctx, scrollingTo) { + const { state } = ctx; +- const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; +- return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; ++ const scroll = state.scrollPending; ++ const adjust = state.scrollAdjustHandler.getAdjust(); ++ const clampedTargetOffset = getCurrentTargetOffset(ctx, scrollingTo); ++ const maxOffset = clampScrollOffset(ctx, scroll, scrollingTo); ++ const diff1 = Math.abs(scroll - clampedTargetOffset); ++ const adjustedTargetOffset = clampedTargetOffset + adjust; ++ const diff2 = Math.abs(scroll - adjustedTargetOffset); ++ const canUseAdjustedCompletion = !scrollingTo.animated || Platform.OS === "ios"; ++ return { ++ clampedTargetOffset, ++ isAtResolvedTarget: Math.abs(scroll - maxOffset) < 1 && (diff1 < 1 || canUseAdjustedCompletion && diff2 < 1) + }; -+ if (!wasReached) { -+ if (!within) { -+ return false; -+ } -+ onReached(distance); -+ updateSnapshot(); -+ return true; -+ } -+ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); -+ if (reset) { -+ setSnapshot(void 0); -+ return false; -+ } -+ if (within) { -+ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; -+ if (changed) { -+ updateSnapshot(); -+ } + } +-function updateContentMetricsState(ctx) { +- var _a3; ++function checkFinishedScrollFrame(ctx) { ++ const scrollingTo = ctx.state.scrollingTo; ++ if (!scrollingTo) { ++ return; + } -+ return true; -+}; -+ -+// src/utils/edgeReachedGate.ts -+function resetEdgeLatch(ctx, edge) { -+ const state = ctx.state; -+ if (edge === "start") { -+ state.isStartReached = false; -+ state.startReachedSnapshot = void 0; -+ } else { -+ state.isEndReached = false; -+ state.endReachedSnapshot = void 0; -+ } -+} -+function resetSharedEdgeGateIfOutsideHysteresis(ctx) { -+ const state = ctx.state; -+ if (!state.edgeReachedGate) { -+ return; -+ } -+ const contentSize = getContentSize(ctx); -+ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); -+ const isContentLess = contentSize < state.scrollLength; -+ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; -+ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; -+ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); -+ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); -+ if (isOutsideStart && isOutsideEnd) { -+ state.edgeReachedGate = void 0; -+ } -+} -+function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { -+ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; -+} -+function markReachedEdge(ctx) { -+ ctx.state.edgeReachedGate = "closed"; -+} -+function prepareReachedEdgeForNextUserScroll(ctx) { -+ if (ctx.state.edgeReachedGate) { -+ ctx.state.edgeReachedGate = "prepared"; -+ } -+} -+function beginReachedEdgeUserScroll(ctx, scrollDelta) { -+ const state = ctx.state; -+ if (state.edgeReachedGate !== "prepared") { -+ return void 0; -+ } -+ const allowedEdge = scrollDelta < 0 ? "start" : "end"; -+ state.edgeReachedGate = "closed"; -+ resetEdgeLatch(ctx, allowedEdge); -+ return allowedEdge; -+} -+ -+// src/utils/hasActiveInitialScroll.ts -+function hasActiveInitialScroll(state) { -+ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; -+} -+ -+// src/utils/checkAtBottom.ts -+function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { + const { state } = ctx; +- const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; +- const nextPadding = getAlignItemsAtEndPadding(ctx); +- if (previousPadding !== nextPadding) { +- const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated"; +- const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold")); +- if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) { +- state.scheduledWork.microtask(() => { +- if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) { +- set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx)); +- } +- }, "alignItemsAtEndPaddingFallback"); +- } else if (!isMaintainExpected) { +- set$(ctx, "alignItemsAtEndPadding", nextPadding); +- } ++ const completionState = getResolvedScrollCompletionState(ctx, scrollingTo); ++ if (completionState.isAtResolvedTarget && hasScrollCompletionOwnership(state, { ++ clampedTargetOffset: completionState.clampedTargetOffset, ++ scrollingTo ++ })) { ++ finishScrollTo(ctx); + } + } +- +-// src/core/addTotalSize.ts +-function addTotalSize(ctx, key, add, notifyTotalSize = true) { ++function scrollToFallbackOffset(ctx, offset) { + var _a3; -+ const state = ctx.state; -+ if (!state) { ++ (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({ ++ animated: false, ++ x: ctx.state.props.horizontal ? offset : 0, ++ y: ctx.state.props.horizontal ? 0 : offset ++ }); ++} ++function checkFinishedScrollFallback(ctx) { + const state = ctx.state; +- const prevTotalSize = state.totalSize; +- let totalSize = state.totalSize; +- if (key === null) { +- totalSize = add; +- if (state.timeoutSetPaddingTop) { +- clearTimeout(state.timeoutSetPaddingTop); +- state.timeoutSetPaddingTop = void 0; +- } +- } else { +- totalSize += add; ++ if (state.scheduledWork.has("checkFinishedScrollFallback")) { + return; -+ } -+ const { -+ queuedInitialLayout, -+ scrollLength, -+ scroll, -+ maintainingScrollAtEnd, -+ props: { maintainScrollAtEndThreshold, onEndReachedThreshold } -+ } = state; -+ const contentSize = getContentSize(ctx); -+ resetSharedEdgeGateIfOutsideHysteresis(ctx); -+ if (contentSize > 0 && queuedInitialLayout) { -+ const insetEnd = getContentInsetEnd(ctx); -+ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; -+ const isContentLess = contentSize < scrollLength; -+ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); -+ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); -+ set$( -+ ctx, -+ "isWithinMaintainScrollAtEndThreshold", -+ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength -+ ); -+ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; -+ if (!shouldSkipThresholdChecks) { -+ state.isEndReached = checkThreshold( -+ distanceFromEnd, -+ isContentLess, -+ onEndReachedThreshold * scrollLength, -+ state.isEndReached, -+ state.endReachedSnapshot, -+ { -+ contentSize, -+ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, -+ scrollPosition: scroll -+ }, -+ (distance) => { -+ var _a4, _b; -+ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { -+ markReachedEdge(ctx); -+ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); -+ } -+ }, -+ (snapshot) => { -+ state.endReachedSnapshot = snapshot; -+ } + } +- if (prevTotalSize !== totalSize) { +- if (!IsNewArchitecture && state.initialScroll && totalSize < prevTotalSize) { +- state.pendingTotalSize = totalSize; +- } else { +- state.pendingTotalSize = void 0; +- state.totalSize = totalSize; +- if (notifyTotalSize) { +- set$(ctx, "totalSize", totalSize); ++ const scrollingTo = state.scrollingTo; ++ const shouldFinishInitialZeroTarget = shouldFinishInitialZeroTargetScroll(ctx); ++ const silentInitialDispatch = isSilentInitialDispatch(state, scrollingTo); ++ const canFinishInitialWithoutNativeProgress = scrollingTo !== void 0 ? shouldFinishInitialScrollWithoutNativeProgress(state, scrollingTo) : false; ++ const slowTimeout = (scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) && !shouldFinishInitialZeroTarget && !canFinishInitialWithoutNativeProgress || !state.didContainersLayout; ++ const initialDelay = shouldFinishInitialZeroTarget || canFinishInitialWithoutNativeProgress ? 0 : silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : slowTimeout ? 500 : 100; ++ let numChecks = 0; ++ const scheduleFallbackCheck = (delay) => { ++ state.scheduledWork.timeout(checkHasScrolled, delay, "checkFinishedScrollFallback"); ++ }; ++ const checkHasScrolled = () => { ++ var _a3, _b, _c, _d; ++ const isStillScrollingTo = state.scrollingTo; ++ if (isStillScrollingTo) { ++ numChecks++; ++ const isNativeInitialPending = isNativeInitialNonZeroTarget(state) && !state.hasScrolled; ++ const maxChecks = silentInitialDispatch ? 5 : isNativeInitialPending ? INITIAL_SCROLL_MAX_FALLBACK_CHECKS : 5; ++ const shouldFinishZeroTarget = shouldFinishInitialZeroTargetScroll(ctx); ++ const canFinishInitialScrollWithoutNativeProgress = shouldFinishInitialScrollWithoutNativeProgress( ++ state, ++ isStillScrollingTo + ); -+ } -+ } -+} -+ -+// src/utils/checkAtTop.ts -+function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -+ const state = ctx == null ? void 0 : ctx.state; -+ if (!state) { ++ const completionState = getResolvedScrollCompletionState(ctx, isStillScrollingTo); ++ const canFinishAfterSilentNativeDispatch = Platform.OS === "android" && silentInitialDispatch && completionState.isAtResolvedTarget && numChecks >= 1; ++ const shouldRetrySilentInitialNativeScroll = Platform.OS === "android" && canFinishAfterSilentNativeDispatch && !initialScrollCompletion.didRetrySilentInitialScroll(state); ++ const shouldFinishAfterObservedScroll = state.hasScrolled && (!isStillScrollingTo.isInitialScroll || completionState.isAtResolvedTarget); ++ const shouldRetryUnalignedInitialScroll = isStillScrollingTo.isInitialScroll && !completionState.isAtResolvedTarget && numChecks <= maxChecks; ++ const shouldRetryUnalignedEndScroll = Platform.OS === "ios" && !isStillScrollingTo.isInitialScroll && isEndAlignedLastItemTarget(ctx, isStillScrollingTo) && !completionState.isAtResolvedTarget && numChecks <= maxChecks; ++ if (shouldRetrySilentInitialNativeScroll) { ++ const targetOffset = (_b = (_a3 = getInitialScrollWatchdogTargetOffset(state)) != null ? _a3 : isStillScrollingTo.targetOffset) != null ? _b : 0; ++ const jiggleOffset = targetOffset >= SILENT_INITIAL_SCROLL_TARGET_EPSILON ? targetOffset - SILENT_INITIAL_SCROLL_TARGET_EPSILON : targetOffset + SILENT_INITIAL_SCROLL_TARGET_EPSILON; ++ initialScrollCompletion.markSilentInitialScrollRetry(state); ++ scrollToFallbackOffset(ctx, jiggleOffset); ++ state.scheduledWork.frame( ++ () => scrollToFallbackOffset(ctx, targetOffset), ++ "checkFinishedScrollRetryFrame" ++ ); ++ scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); ++ } else if (shouldRetryUnalignedEndScroll) { ++ scrollToFallbackOffset(ctx, completionState.clampedTargetOffset); ++ scheduleFallbackCheck(100); ++ } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) { ++ finishScrollTo(ctx); ++ } else if ((isNativeInitialPending || shouldRetryUnalignedInitialScroll) && numChecks <= maxChecks) { ++ const targetOffset = (_d = (_c = getInitialScrollWatchdogTargetOffset(state)) != null ? _c : isStillScrollingTo.targetOffset) != null ? _d : state.scrollPending; ++ scrollToFallbackOffset(ctx, targetOffset); ++ scheduleFallbackCheck(silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : 100); ++ } else { ++ scheduleFallbackCheck(silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : 100); + } +- updateContentMetricsState(ctx); + } +- } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { +- set$(ctx, "totalSize", totalSize); +- } ++ }; ++ scheduleFallbackCheck(initialDelay); + } + +-// src/core/setSize.ts +-function setSize(ctx, itemKey, size, notifyTotalSize = true) { ++// src/core/doScrollTo.native.ts ++function doScrollTo(ctx, params) { + const state = ctx.state; +- const { sizes } = state; +- const previousSize = sizes.get(itemKey); +- const diff = previousSize !== void 0 ? size - previousSize : size; +- if (diff !== 0) { +- addTotalSize(ctx, itemKey, diff, notifyTotalSize); ++ const { animated, horizontal, isInitialScroll, offset } = params; ++ const isAnimated = !!animated; ++ const { refScroller } = state; ++ const scroller = refScroller.current; ++ if (!scroller) { + return; -+ } -+ const { -+ isStartReached, -+ props: { data, onStartReachedThreshold }, -+ scroll, -+ scrollLength, -+ startReachedSnapshot, -+ totalSize -+ } = state; -+ const dataLength = data.length; -+ const threshold = onStartReachedThreshold * scrollLength; -+ resetSharedEdgeGateIfOutsideHysteresis(ctx); -+ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { -+ state.isStartReached = false; -+ state.startReachedSnapshot = void 0; -+ } -+ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); -+ set$(ctx, "isNearStart", scroll <= threshold); -+ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; -+ if (!shouldSkipThresholdChecks) { -+ state.isStartReached = checkThreshold( -+ scroll, -+ false, -+ threshold, -+ state.isStartReached, -+ startReachedSnapshot, -+ { -+ contentSize: totalSize, -+ dataLength, -+ scrollPosition: scroll -+ }, -+ (distance) => { -+ var _a3, _b; -+ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { -+ markReachedEdge(ctx); -+ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); + } +- sizes.set(itemKey, size); +-} +- +-// src/utils/helpers.ts +-function isFunction(obj) { +- return typeof obj === "function"; +-} +-function isArray(obj) { +- return Array.isArray(obj); +-} +-var warned = /* @__PURE__ */ new Set(); +-function warnDevOnce(id, text) { +- if (IS_DEV && !warned.has(id)) { +- warned.add(id); +- console.warn(`[legend-list] ${text}`); ++ const isHorizontal = !!horizontal; ++ const contentSize = isHorizontal ? getContentSize(ctx) : void 0; ++ const nativeOffset = toNativeHorizontalOffset(state, offset, contentSize); ++ scroller.scrollTo({ ++ animated: isAnimated, ++ x: isHorizontal ? nativeOffset : 0, ++ y: isHorizontal ? 0 : offset ++ }); ++ if (isInitialScroll) { ++ initialScrollCompletion.markInitialScrollNativeDispatch(state); + } +-} +-function roundSize(size) { +- return Math.floor(size * 8) / 8; +-} +-function isNullOrUndefined(value) { +- return value === null || value === void 0; +-} +-function getPadding(s, type) { +- var _a3, _b, _c; +- const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; +- return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; +-} +-function extractPadding(style, contentContainerStyle, type) { +- return getPadding(style, type) + getPadding(contentContainerStyle, type); +-} +-function findContainerId(ctx, key) { +- var _a3, _b; +- const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); +- if (directMatch !== void 0) { +- return directMatch; ++ if (isAnimated && Math.abs(state.scroll - offset) <= 1) { ++ checkFinishedScroll(ctx); + } +- const numContainers = peek$(ctx, "numContainers"); +- for (let i = 0; i < numContainers; i++) { +- const itemKey = peek$(ctx, `containerItemKey${i}`); +- if (itemKey === key) { +- return i; +- } ++ if (!isAnimated) { ++ state.scroll = offset; ++ checkFinishedScrollFallback(ctx); + } +- return -1; + } + +-// src/utils/getItemSize.ts +-function getKnownOrFixedSize(ctx, key, index, data, resolved) { +- var _a3, _b; ++// src/utils/requestAdjust.ts ++function requestAdjust(ctx, positionDiff, source) { + const state = ctx.state; +- const { getFixedItemSize, getItemType } = state.props; +- let size = key ? state.sizesKnown.get(key) : void 0; +- if (size === void 0 && key && getFixedItemSize) { +- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; +- const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); +- if (fixedSize !== void 0) { +- size = fixedSize + ctx.scrollAxisGap; +- state.sizesKnown.set(key, size); +- } +- } +- return size; +-} +-function getKnownOrFixedItemSize(ctx, index) { +- const key = getId(ctx.state, index); +- return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); +-} +-function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { +- for (let index = startIndex; index <= endIndex; index++) { +- if (getKnownOrFixedItemSize(ctx, index) === void 0) { +- return false; ++ if (Math.abs(positionDiff) > 0.1) { ++ const dataChanged = source === "data"; ++ const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff; ++ const doit = () => { ++ if (needsScrollWorkaround) { ++ doScrollTo(ctx, { horizontal: state.props.horizontal, offset: state.scroll }); ++ } else { ++ state.scrollAdjustHandler.requestAdjust(positionDiff); ++ if (state.adjustingFromInitialMount) { ++ state.adjustingFromInitialMount--; + } -+ }, -+ (snapshot) => { -+ state.startReachedSnapshot = snapshot; + } -+ ); -+ } ++ }; ++ state.scroll += positionDiff; ++ state.scrollForNextCalculateItemsInView = void 0; ++ const readyToRender = peek$(ctx, "readyToRender"); ++ if (readyToRender) { ++ doit(); ++ if (Platform.OS !== "web" && source !== "item-size") { ++ const threshold = state.scroll - positionDiff / 2; ++ if (!state.ignoreScrollFromMVCP) { ++ state.ignoreScrollFromMVCP = {}; ++ } ++ if (positionDiff > 0) { ++ state.ignoreScrollFromMVCP.lt = threshold; ++ } else { ++ state.ignoreScrollFromMVCP.gt = threshold; ++ } ++ const delay = needsScrollWorkaround ? 250 : 100; ++ state.scheduledWork.timeout( ++ () => { ++ var _a3; ++ state.ignoreScrollFromMVCP = void 0; ++ const shouldForceUpdate = state.ignoreScrollFromMVCPIgnored && state.scrollProcessingEnabled !== false; ++ if (shouldForceUpdate) { ++ state.ignoreScrollFromMVCPIgnored = false; ++ state.scrollPending = state.scroll; ++ (_a3 = state.reprocessCurrentScroll) == null ? void 0 : _a3.call(state); ++ } ++ }, ++ delay, ++ "ignoreScrollFromMVCP" ++ ); ++ } ++ } else { ++ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; ++ requestAnimationFrame(doit); + } + } +- return true; } - +-function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { +- var _a3, _b, _c, _d; ++ ++// src/core/scrollTargetSettle.ts ++var SETTLE_POSITION_EPSILON = 0.5; ++var SETTLE_QUIET_PASSES_TO_RELEASE = 2; ++var SETTLE_TTL_MS = 500; ++function getSettleTargetOffset(ctx, settle, index, position) { ++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; ++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); ++} ++function clearScrollTargetSettle(state) { ++ state.scrollTargetSettle = void 0; ++} ++function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; +- const { +- sizes, +- averageSizes, +- props: { estimatedItemSize, getItemType }, +- scrollingTo +- } = state; +- const sizeKnown = state.sizesKnown.get(key); +- if (sizeKnown !== void 0) { +- return sizeKnown; +- } +- let size; +- const renderedSize = sizes.get(key); +- if (preferCachedSize) { +- if (renderedSize !== void 0) { +- return renderedSize; +- } +- } +- size = getKnownOrFixedSize(ctx, key, index, data, resolved); +- if (size !== void 0) { +- setSize(ctx, key, size, notifyTotalSize); +- return size; +- } +- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; +- if (useAverageSize && !scrollingTo) { +- const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; +- if (averageSizeForType !== void 0) { +- size = roundSize(averageSizeForType); +- } +- } +- if (size === void 0 && renderedSize !== void 0) { +- return renderedSize; +- } +- if (size === void 0 && useAverageSize && scrollingTo) { +- const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; +- if (averageSizeForType !== void 0) { +- size = roundSize(averageSizeForType); +- } +- } +- if (size === void 0) { +- size = estimatedItemSize + ctx.scrollAxisGap; +- } +- setSize(ctx, key, size, notifyTotalSize); +- return size; +-} +-function getItemSizeAtIndex(ctx, index) { +- if (index === void 0 || index < 0) { +- return void 0; ++ const { index, viewOffset, viewPosition } = params; ++ const { data } = state.props; ++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; ++ if (index < 0 || index >= data.length || isEndAlignedLastItem) { ++ clearScrollTargetSettle(state); ++ return; + } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++ state.scrollTargetSettle = { ++ expiresAt: Date.now() + SETTLE_TTL_MS, ++ id: getId(state, index), ++ quietPasses: 0, ++ viewOffset, ++ viewPosition ++ }; + } +- -// src/core/calculateOffsetWithOffsetPosition.ts -function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; ++function settleScrollTarget(ctx) { + var _a3; + const state = ctx.state; - const { index, viewOffset, viewPosition } = params; - let offset = offsetParam; - if (viewOffset) { @@ -930,7 +1302,10 @@ index b3c5a30..9d7ad6d 100644 - if (startOffsetAdjustment) { - offset += startOffsetAdjustment; - } -- } ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ return false; + } - if (viewPosition !== void 0 && index !== void 0) { - const dataLength = state.props.data.length; - if (dataLength === 0) { @@ -945,21 +1320,74 @@ index b3c5a30..9d7ad6d 100644 - if (!isOutOfBounds && index === state.props.data.length - 1) { - const footerSize = peek$(ctx, "footerSize") || 0; - offset += footerSize; -- } -- } ++ const index = state.indexByKey.get(settle.id); ++ const position = index === void 0 ? void 0 : state.positions[index]; ++ if (index === void 0 || position === void 0) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ const now = Date.now(); ++ if (now > settle.expiresAt) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position); ++ const diff = targetOffset - state.scroll; ++ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ settle.quietPasses++; ++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { ++ clearScrollTargetSettle(state); + } ++ return false; + } - return offset; -+// src/utils/checkThresholds.ts -+function checkThresholds(ctx, allowedEdge) { -+ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; -+ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); ++ settle.quietPasses = 0; ++ settle.expiresAt = now + SETTLE_TTL_MS; ++ if (((_a3 = state.scrollingTo) == null ? void 0 : _a3.index) === index) { ++ state.scrollingTo.offset = position; ++ state.scrollingTo.targetOffset = targetOffset; ++ } ++ requestAdjust(ctx, diff); ++ return true; } -// src/core/clampScrollOffset.ts -function clampScrollOffset(ctx, offset, scrollTarget) { -+// src/core/recalculateSettledScroll.ts -+function recalculateSettledScroll(ctx) { ++// src/core/cancelImperativeScroll.ts ++function cancelScrollCompletionChecks({ scheduledWork }) { ++ scheduledWork.cancel("checkFinishedScrollFrame"); ++ scheduledWork.cancel("checkFinishedScrollRetryFrame"); ++ scheduledWork.cancel("checkFinishedScrollFallback"); ++ scheduledWork.cancel("platformScrollCompletion"); ++} ++function settlePendingImperativeScroll(state) { + var _a3, _b; ++ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; ++ state.pendingScrollResolve = void 0; ++ state.pendingScrollToEnd = void 0; ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); ++} ++function cancelImperativeScroll(state) { ++ cancelScrollCompletionChecks(state); ++ state.scheduledWork.cancel("imperativeScrollReady"); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ clearScrollTargetSettle(state); ++ settlePendingImperativeScroll(state); ++} ++ ++// src/core/deferredPublicOnScroll.ts ++function withResolvedContentOffset(state, event, resolvedOffset) { ++ return { ++ ...event, ++ nativeEvent: { ++ ...event.nativeEvent, ++ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } ++ } ++ }; ++} ++function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { ++ var _a3, _b, _c, _d; const state = ctx.state; - const contentSize = getContentSize(ctx); - let clampedOffset = offset; @@ -969,171 +1397,103 @@ index b3c5a30..9d7ad6d 100644 - const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; - const maxOffset = baseMaxOffset + extraEndOffset; - clampedOffset = Math.min(offset, maxOffset); -+ if ((_a3 = state.props) == null ? void 0 : _a3.data) { -+ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); ++ const deferredEvent = state.deferredPublicOnScrollEvent; ++ state.deferredPublicOnScrollEvent = void 0; ++ if (deferredEvent) { ++ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( ++ _c, ++ withResolvedContentOffset( ++ state, ++ deferredEvent, ++ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 ++ ) ++ ); } - clampedOffset = Math.max(0, clampedOffset); - return clampedOffset; -+ checkThresholds(ctx); } - // src/core/finishScrollTo.ts -@@ -1357,6 +973,124 @@ function finishScrollTo(ctx) { +-// src/core/finishScrollTo.ts +-function finishScrollTo(ctx) { +- var _a3, _b; ++// src/core/adaptiveRender.ts ++var DEFAULT_ADAPTIVE_RENDER_ENTER_VELOCITY = 3; ++var DEFAULT_ADAPTIVE_RENDER_EXIT_VELOCITY = 1; ++var DEFAULT_ADAPTIVE_RENDER_EXIT_DELAY = 250; ++var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; ++function clearAdaptiveRenderExitTimeout(ctx) { ++ ctx.state.scheduledWork.cancel("adaptiveRender"); ++} ++function scheduleAdaptiveRenderExit(ctx, exitDelay) { + const state = ctx.state; +- if (state == null ? void 0 : state.scrollingTo) { +- cancelScrollCompletionChecks(state); +- const resolvePendingScroll = state.pendingScrollResolve; +- state.pendingScrollResolve = void 0; +- const scrollingTo = state.scrollingTo; +- state.scrollHistory.length = 0; +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- if (state.pendingTotalSize !== void 0) { +- addTotalSize(ctx, null, state.pendingTotalSize); +- } +- if (PlatformAdjustBreaksScroll) { +- state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); +- } +- if (scrollingTo.isInitialScroll || state.initialScroll) { +- const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; +- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; +- finishInitialScroll(ctx, { +- onFinished: () => { +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +- }, +- preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, +- recalculateItems: true, +- schedulePreservedTargetClear: shouldPreserveResizeTarget, +- syncObservedOffset: isOffsetSession, +- waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame +- }); +- return; +- } +- recalculateSettledScroll(ctx); +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); ++ clearAdaptiveRenderExitTimeout(ctx); ++ if (exitDelay <= 0) { ++ setAdaptiveRender(ctx, "normal", "scroll"); ++ } else { ++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); } } - -+// src/core/initialScrollSession.ts -+var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -+function hasInitialScrollSessionCompletion(completion) { -+ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); -+} -+function clearInitialScrollSession(state) { -+ state.initialScrollSession = void 0; -+ return void 0; -+} -+function createInitialScrollSession(options) { -+ const { bootstrap, completion, kind, previousDataLength } = options; -+ return kind === "offset" ? { -+ completion, -+ kind, -+ previousDataLength -+ } : { -+ bootstrap, -+ completion, -+ kind, -+ previousDataLength -+ }; -+} -+function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { -+ var _a4, _b2; -+ if (!state.initialScrollSession) { -+ state.initialScrollSession = createInitialScrollSession({ -+ completion: {}, -+ kind, -+ previousDataLength: 0 -+ }); -+ } else if (state.initialScrollSession.kind !== kind) { -+ state.initialScrollSession = createInitialScrollSession({ -+ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, -+ completion: state.initialScrollSession.completion, -+ kind, -+ previousDataLength: state.initialScrollSession.previousDataLength -+ }); -+ } -+ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; -+ return state.initialScrollSession.completion; -+} -+var initialScrollCompletion = { -+ didDispatchNativeScroll(state) { -+ var _a3, _b; -+ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); -+ }, -+ didRetrySilentInitialScroll(state) { -+ var _a3, _b; -+ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); -+ }, -+ markInitialScrollNativeDispatch(state) { -+ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; -+ }, -+ markSilentInitialScrollRetry(state) { -+ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; -+ }, -+ resetFlags(state) { -+ if (!state.initialScrollSession) { -+ return; -+ } -+ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); -+ completion.didDispatchNativeScroll = void 0; -+ completion.didRetrySilentInitialScroll = void 0; -+ } -+}; -+var initialScrollWatchdog = { -+ clear(state) { -+ initialScrollWatchdog.set(state, void 0); -+ }, -+ didReachTarget(newScroll, watchdog) { -+ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); -+ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ get(state) { -+ var _a3, _b; -+ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; -+ }, -+ hasNonZeroTargetOffset(targetOffset) { -+ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ isAtZeroTargetOffset(targetOffset) { -+ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ set(state, watchdog) { -+ var _a3, _b; -+ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { -+ return; -+ } -+ const completion = ensureInitialScrollSessionCompletion(state); -+ completion.watchdog = watchdog ? { -+ startScroll: watchdog.startScroll, -+ targetOffset: watchdog.targetOffset -+ } : void 0; -+ } -+}; -+function setInitialScrollSession(state, options = {}) { -+ var _a3, _b, _c, _d; -+ const existingSession = state.initialScrollSession; -+ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; -+ const completion = existingSession == null ? void 0 : existingSession.completion; -+ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; -+ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; -+ if (!kind) { -+ return clearInitialScrollSession(state); -+ } -+ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { -+ return clearInitialScrollSession(state); -+ } -+ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; -+ state.initialScrollSession = createInitialScrollSession({ -+ bootstrap, -+ completion, -+ kind, -+ previousDataLength -+ }); -+ return state.initialScrollSession; -+} -+ - // src/core/checkFinishedScroll.ts - var INITIAL_SCROLL_MAX_FALLBACK_CHECKS = 20; - var INITIAL_SCROLL_COMPLETION_TARGET_EPSILON = 1; -@@ -1549,6 +1283,69 @@ function doScrollTo(ctx, params) { - } - } - -+// src/core/adaptiveRender.ts -+var DEFAULT_ADAPTIVE_RENDER_ENTER_VELOCITY = 3; -+var DEFAULT_ADAPTIVE_RENDER_EXIT_VELOCITY = 1; -+var DEFAULT_ADAPTIVE_RENDER_EXIT_DELAY = 250; -+var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; -+var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; -+var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; -+function clearAdaptiveRenderExitTimeout(ctx) { -+ ctx.state.scheduledWork.cancel("adaptiveRender"); -+} -+function scheduleAdaptiveRenderExit(ctx, exitDelay) { -+ const state = ctx.state; -+ clearAdaptiveRenderExitTimeout(ctx); -+ if (exitDelay <= 0) { -+ setAdaptiveRender(ctx, "normal", "scroll"); -+ } else { -+ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); -+ } -+} +- +-// src/core/checkFinishedScroll.ts +-var INITIAL_SCROLL_MAX_FALLBACK_CHECKS = 20; +-var INITIAL_SCROLL_COMPLETION_TARGET_EPSILON = 1; +-var INITIAL_SCROLL_ZERO_TARGET_EPSILON = 1; +-var SILENT_INITIAL_SCROLL_RETRY_DELAY_MS = 16; +-var SILENT_INITIAL_SCROLL_TARGET_EPSILON = 1; +-function checkFinishedScroll(ctx, options) { +- const scrollingTo = ctx.state.scrollingTo; +- if (options == null ? void 0 : options.onlyIfAligned) { +- if (!(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) || scrollingTo.animated) { +- return; +- } +- if (!getResolvedScrollCompletionState(ctx, scrollingTo).isAtResolvedTarget) { +- return; +- } +function setAdaptiveRender(ctx, mode, reason) { + var _a3, _b; + const previousMode = peek$(ctx, "adaptiveRender"); + if (previousMode !== mode) { + set$(ctx, "adaptiveRender", mode); + (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); -+ } -+} + } +- ctx.state.scheduledWork.frame(() => checkFinishedScrollFrame(ctx), "checkFinishedScrollFrame"); + } +-function hasScrollCompletionOwnership(state, options) { +- const { clampedTargetOffset, scrollingTo } = options; +- return !scrollingTo.isInitialScroll || state.hasScrolled || clampedTargetOffset <= INITIAL_SCROLL_COMPLETION_TARGET_EPSILON; +function resetAdaptiveRender(ctx) { + var _a3, _b; + clearAdaptiveRenderExitTimeout(ctx); @@ -1141,7 +1501,9 @@ index b3c5a30..9d7ad6d 100644 + if (peek$(ctx, "adaptiveRender") !== mode) { + setAdaptiveRender(ctx, mode, "initial"); + } -+} + } +-function isSilentInitialDispatch(state, scrollingTo) { +- return !!(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) && initialScrollCompletion.didDispatchNativeScroll(state) && !state.hasScrolled; +function updateAdaptiveRender(ctx, scrollVelocity, options) { + var _a3, _b, _c; + const state = ctx.state; @@ -1168,307 +1530,78 @@ index b3c5a30..9d7ad6d 100644 + resetAdaptiveRender(ctx); + } + } -+} -+ - // src/core/doMaintainScrollAtEnd.ts - function doMaintainScrollAtEnd(ctx) { - const state = ctx.state; -@@ -1575,9 +1372,12 @@ function doMaintainScrollAtEnd(ctx) { - const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; - const scrollAtRequest = state.scroll; - state.maintainingScrollAtEnd = pendingState; -+ clearScrollTargetSettle(state); - requestAnimationFrame(() => { - const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); -- const didScrollSinceRequest = state.scroll !== scrollAtRequest; -+ const lastIssued = state.lastIssuedScrollOffset; -+ const isSettlingOntoIssuedScroll = lastIssued !== void 0 && Math.abs(state.scroll - lastIssued) <= EDGE_POSITION_EPSILON; -+ const didScrollSinceRequest = state.scroll !== scrollAtRequest && !isSettlingOntoIssuedScroll; - if (isStillWithinThreshold || !didScrollSinceRequest) { - state.maintainingScrollAtEnd = activeState; - const scroller = refScroller.current; -@@ -1972,11 +1772,32 @@ function prepareMVCP(ctx, dataChanged) { - } } - --// src/platform/flushSync.native.ts --var flushSync = (fn) => { -- fn(); --}; -- -+// src/platform/flushSync.native.ts -+var flushSync = (fn) => { -+ fn(); -+}; +-function getInitialScrollWatchdogTargetOffset(state) { + +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { -+ var _a3; + var _a3; +- return (_a3 = initialScrollWatchdog.get(state)) == null ? void 0 : _a3.targetOffset; +-} +-function isNativeInitialNonZeroTarget(state) { +- const targetOffset = getInitialScrollWatchdogTargetOffset(state); +- return !state.didFinishInitialScroll && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); + const drawDistance = ctx.state.props.drawDistance; + const initialScroll = ctx.state.initialScroll; + const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; + const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; + return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; -+} + } +-function shouldFinishInitialScrollWithoutNativeProgress(state, scrollingTo) { +- var _a3, _b; +- if (!scrollingTo.isInitialScroll || scrollingTo.animated || !state.didContainersLayout) { +- return false; +- } +- if (((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap") { +- return false; +function scheduleFullDrawDistancePrewarm(ctx) { + const { state } = ctx; + if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { + return; -+ } + } +- const targetOffset = (_b = scrollingTo.targetOffset) != null ? _b : scrollingTo.offset; +- if (initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset) && initialScrollCompletion.didDispatchNativeScroll(state) && !state.hasScrolled) { +- return false; + state.scheduledWork.frame(() => { + var _a3; + return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); + }, "fullDrawDistancePrewarm"); +} + - // src/utils/getScrollVelocity.ts - var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; - var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2224,7 +2045,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { - } - } - function scrollTo(ctx, params) { -- var _a3, _b; -+ var _a3, _b, _c; - const state = ctx.state; - const { noScrollingTo, forceScroll, ...scrollTarget } = params; - const { -@@ -2246,6 +2067,7 @@ function scrollTo(ctx, params) { - if (!noScrollingTo) { - if (isInitialScroll) { - initialScrollCompletion.resetFlags(state); -+ clearScrollTargetSettle(state); - } - const averageSizeSnapshot = getAverageSizeSnapshot(state); - state.scrollingTo = { -@@ -2256,6 +2078,15 @@ function scrollTo(ctx, params) { - }; - if (!isInitialScroll) { - pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); -+ if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { -+ beginScrollTargetSettle(ctx, { -+ index: scrollTarget.index, -+ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, -+ viewPosition: scrollTarget.viewPosition -+ }); -+ } else { -+ clearScrollTargetSettle(state); -+ } - } ++// src/utils/setInitialRenderState.ts ++function resetInitialRenderState(ctx, { ++ resetLayout, ++ resetInitialScroll ++}) { ++ const { state } = ctx; ++ if (resetLayout) { ++ state.didContainersLayout = false; ++ state.queuedInitialLayout = false; } - state.scrollPending = targetOffset; -@@ -2263,7 +2094,7 @@ function scrollTo(ctx, params) { - if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { - if (animated) { - if (state.scrollTargetPinnedRange) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); -+ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); - } - } else { - updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2276,6 +2107,310 @@ function scrollTo(ctx, params) { +- if (initialScrollWatchdog.isAtZeroTargetOffset(targetOffset) || Math.abs(state.scroll - targetOffset) > 1 || Math.abs(state.scrollPending - targetOffset) > 1) { +- return false; ++ if (resetInitialScroll) { ++ state.didFinishInitialScroll = false; } +- return !!scrollingTo.waitForInitialScrollCompletionFrame || isNativeInitialNonZeroTarget(state); ++ set$(ctx, "readyToRender", false); ++ resetAdaptiveRender(ctx); } - -+// src/core/scrollTargetSettle.ts -+var SETTLE_POSITION_EPSILON = 1; -+var SETTLE_QUIET_PASSES_TO_RELEASE = 2; -+var SETTLE_MAX_MS = 1e3; -+var SETTLE_MAX_CORRECTIONS = 8; -+function clearScrollTargetSettle(state) { -+ state.scrollTargetSettle = void 0; -+ state.scheduledWork.cancel("scrollTargetSettle"); -+ state.scheduledWork.cancel("scrollTargetSettleDeadline"); -+} -+function beginScrollTargetSettle(ctx, params) { -+ const state = ctx.state; -+ const { index, viewOffset, viewPosition } = params; -+ const { alignItemsAtEnd, data, maintainScrollAtEnd } = state.props; -+ const hasEndAnchoring = !!maintainScrollAtEnd || !!alignItemsAtEnd; -+ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; -+ if (index < 0 || index >= data.length || isEndAlignedLastItem && hasEndAnchoring) { -+ clearScrollTargetSettle(state); -+ return; -+ } -+ clearScrollTargetSettle(state); -+ const now = Date.now(); -+ state.scrollTargetSettle = { -+ corrections: 0, -+ deadline: now + SETTLE_MAX_MS, -+ id: getId(state, index), -+ measuredIndex: void 0, -+ quietPasses: 0, -+ viewOffset, -+ viewPosition -+ }; -+ state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline"); -+} -+function getSettleTargetOffset(ctx, settle, index, position) { -+ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; -+ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); -+} -+function applyScrollTargetCorrection(ctx, id) { -+ var _a3; -+ const state = ctx.state; -+ const settle = state.scrollTargetSettle; -+ if (!settle || settle.id !== id) { -+ return; -+ } -+ const index = state.indexByKey.get(id); -+ const position = index === void 0 ? void 0 : state.positions[index]; -+ if (index === void 0 || position === void 0 || Date.now() > settle.deadline) { -+ clearScrollTargetSettle(state); -+ return; -+ } -+ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { -+ clearScrollTargetSettle(state); -+ return; -+ } -+ settle.corrections++; -+ settle.measuredIndex = void 0; -+ scrollTo(ctx, { -+ animated: false, -+ index, -+ itemSize: getItemSizeAtIndex(ctx, index), -+ // Correcting where a scroll already landed is not a new imperative scroll: claiming the -+ // session would suppress the list's own handling of subsequent scroll events and re-arm -+ // this settle from inside its own correction. -+ noScrollingTo: true, -+ offset: position, -+ viewOffset: settle.viewOffset, -+ viewPosition: settle.viewPosition -+ }); -+ const scrollingTo = state.scrollingTo; -+ if (scrollingTo) { -+ scrollingTo.targetOffset = state.scrollPending; -+ scrollingTo.offset = position; -+ } -+ (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); -+} -+function settleScrollTarget(ctx, options) { -+ var _a3; -+ const state = ctx.state; -+ const settle = state.scrollTargetSettle; -+ if (!settle) { -+ return false; -+ } -+ const measured = options == null ? void 0 : options.minIndexSizeChanged; -+ if (measured !== void 0) { -+ settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); -+ } -+ if (options == null ? void 0 : options.isCompensating) { -+ if (Date.now() > settle.deadline) { -+ clearScrollTargetSettle(state); -+ return false; -+ } -+ settle.quietPasses = 0; -+ return false; -+ } -+ const index = state.indexByKey.get(settle.id); -+ const position = index === void 0 ? void 0 : state.positions[index]; -+ if (index === void 0 || position === void 0) { -+ clearScrollTargetSettle(state); -+ return false; -+ } -+ if (Date.now() > settle.deadline) { -+ clearScrollTargetSettle(state); -+ return false; -+ } -+ if (settle.measuredIndex === void 0 || settle.measuredIndex > index) { -+ return false; -+ } -+ const targetOffset = getSettleTargetOffset(ctx, settle, index, position); -+ if (Math.abs(targetOffset - state.scroll) <= SETTLE_POSITION_EPSILON) { -+ settle.measuredIndex = void 0; -+ settle.quietPasses++; -+ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { -+ clearScrollTargetSettle(state); -+ } -+ return false; -+ } -+ settle.quietPasses = 0; -+ state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); -+ return true; -+} -+ -+// src/core/cancelImperativeScroll.ts -+function cancelScrollCompletionChecks({ scheduledWork }) { -+ scheduledWork.cancel("checkFinishedScrollFrame"); -+ scheduledWork.cancel("checkFinishedScrollRetryFrame"); -+ scheduledWork.cancel("checkFinishedScrollFallback"); -+ scheduledWork.cancel("platformScrollCompletion"); -+} -+function settlePendingImperativeScroll(state) { -+ var _a3, _b; -+ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; -+ state.pendingScrollResolve = void 0; -+ state.pendingScrollToEnd = void 0; -+ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); -+} -+function cancelImperativeScroll(state) { -+ cancelScrollCompletionChecks(state); -+ state.scheduledWork.cancel("imperativeScrollReady"); -+ state.scrollingTo = void 0; -+ state.scrollTargetPinnedRange = void 0; -+ clearScrollTargetSettle(state); -+ settlePendingImperativeScroll(state); -+} -+ -+// src/core/deferredPublicOnScroll.ts -+function withResolvedContentOffset(state, event, resolvedOffset) { -+ return { -+ ...event, -+ nativeEvent: { -+ ...event.nativeEvent, -+ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -+ } -+ }; -+} -+function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { -+ var _a3, _b, _c, _d; -+ const state = ctx.state; -+ const deferredEvent = state.deferredPublicOnScrollEvent; -+ state.deferredPublicOnScrollEvent = void 0; -+ if (deferredEvent) { -+ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( -+ _c, -+ withResolvedContentOffset( -+ state, -+ deferredEvent, -+ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 -+ ) -+ ); -+ } -+} -+ -+// src/utils/setInitialRenderState.ts -+function resetInitialRenderState(ctx, { -+ resetLayout, -+ resetInitialScroll -+}) { -+ const { state } = ctx; -+ if (resetLayout) { -+ state.didContainersLayout = false; -+ state.queuedInitialLayout = false; -+ } -+ if (resetInitialScroll) { -+ state.didFinishInitialScroll = false; -+ } -+ set$(ctx, "readyToRender", false); -+ resetAdaptiveRender(ctx); -+} -+function setInitialRenderState(ctx, { -+ didLayout, -+ didInitialScroll -+}) { -+ const { state } = ctx; -+ const { -+ loadStartTime, -+ props: { onLoad } -+ } = state; -+ if (didLayout) { -+ state.didContainersLayout = true; +-function shouldFinishInitialZeroTargetScroll(ctx) { +- var _a3; ++function setInitialRenderState(ctx, { ++ didLayout, ++ didInitialScroll ++}) { + const { state } = ctx; +- return !!((_a3 = state.scrollingTo) == null ? void 0 : _a3.isInitialScroll) && state.props.data.length > 0 && getContentSize(ctx) <= state.scrollLength && state.scrollPending <= INITIAL_SCROLL_ZERO_TARGET_EPSILON; ++ const { ++ loadStartTime, ++ props: { onLoad } ++ } = state; ++ if (didLayout) { ++ state.didContainersLayout = true; + } + if (didInitialScroll) { + state.didFinishInitialScroll = true; @@ -1487,7 +1620,9 @@ index b3c5a30..9d7ad6d 100644 + } + } + } -+} + } +-function isEndAlignedLastItemTarget(ctx, scrollingTo) { +- return scrollingTo.index === ctx.state.props.data.length - 1 && scrollingTo.viewPosition === 1; + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -1495,16 +1630,48 @@ index b3c5a30..9d7ad6d 100644 + state.scroll = offset; + state.scrollPending = offset; + state.scrollPrev = offset; -+} + } +-function getCurrentTargetOffset(ctx, scrollingTo) { +- var _a3; +- const index = scrollingTo.index; +- const shouldRecomputeEndTarget = isEndAlignedLastItemTarget(ctx, scrollingTo); +- const requestedTargetOffset = shouldRecomputeEndTarget && index !== void 0 ? calculateOffsetWithOffsetPosition(ctx, calculateOffsetForIndex(ctx, index), scrollingTo) : (_a3 = scrollingTo.targetOffset) != null ? _a3 : clampScrollOffset(ctx, scrollingTo.offset - (scrollingTo.viewOffset || 0), scrollingTo); +- return clampScrollOffset(ctx, requestedTargetOffset, scrollingTo); +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); -+} + } +-function getResolvedScrollCompletionState(ctx, scrollingTo) { +- const { state } = ctx; +- const scroll = state.scrollPending; +- const adjust = state.scrollAdjustHandler.getAdjust(); +- const clampedTargetOffset = getCurrentTargetOffset(ctx, scrollingTo); +- const maxOffset = clampScrollOffset(ctx, scroll, scrollingTo); +- const diff1 = Math.abs(scroll - clampedTargetOffset); +- const adjustedTargetOffset = clampedTargetOffset + adjust; +- const diff2 = Math.abs(scroll - adjustedTargetOffset); +- const canUseAdjustedCompletion = !scrollingTo.animated || Platform.OS === "ios"; +- return { +- clampedTargetOffset, +- isAtResolvedTarget: Math.abs(scroll - maxOffset) < 1 && (diff1 < 1 || canUseAdjustedCompletion && diff2 < 1) +- }; +function clearPreservedInitialScrollTarget(state) { + clearPreservedInitialScrollTargetTimeout(state); + state.clearPreservedInitialScrollOnNextFinish = void 0; + state.initialScroll = void 0; + setInitialScrollSession(state); -+} + } +-function checkFinishedScrollFrame(ctx) { +- const scrollingTo = ctx.state.scrollingTo; +- if (!scrollingTo) { +- return; +- } +- const { state } = ctx; +- const completionState = getResolvedScrollCompletionState(ctx, scrollingTo); +- if (completionState.isAtResolvedTarget && hasScrollCompletionOwnership(state, { +- clampedTargetOffset: completionState.clampedTargetOffset, +- scrollingTo +- })) { +- finishScrollTo(ctx); +function supersedeInitialScroll(ctx) { + var _a3, _b, _c; + const state = ctx.state; @@ -1521,11 +1688,22 @@ index b3c5a30..9d7ad6d 100644 + initialScrollCompletion.resetFlags(state); + setInitialScrollSession(state, { bootstrap: null }); + finishInitialScroll(ctx); -+ } -+} + } + } +-function scrollToFallbackOffset(ctx, offset) { +- var _a3; +- (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({ +- animated: false, +- x: ctx.state.props.horizontal ? offset : 0, +- y: ctx.state.props.horizontal ? 0 : offset +- }); +-} +-function checkFinishedScrollFallback(ctx) { +function finishInitialScroll(ctx, options) { + var _a3, _b, _c; -+ const state = ctx.state; + const state = ctx.state; +- if (state.scheduledWork.has("checkFinishedScrollFallback")) { +- return; + if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { + syncInitialScrollOffset(state, options.resolvedOffset); + } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { @@ -1533,7 +1711,43 @@ index b3c5a30..9d7ad6d 100644 + if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { + syncInitialScrollOffset(state, observedOffset); + } -+ } + } +- const scrollingTo = state.scrollingTo; +- const shouldFinishInitialZeroTarget = shouldFinishInitialZeroTargetScroll(ctx); +- const silentInitialDispatch = isSilentInitialDispatch(state, scrollingTo); +- const canFinishInitialWithoutNativeProgress = scrollingTo !== void 0 ? shouldFinishInitialScrollWithoutNativeProgress(state, scrollingTo) : false; +- const slowTimeout = (scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) && !shouldFinishInitialZeroTarget && !canFinishInitialWithoutNativeProgress || !state.didContainersLayout; +- const initialDelay = shouldFinishInitialZeroTarget || canFinishInitialWithoutNativeProgress ? 0 : silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : slowTimeout ? 500 : 100; +- let numChecks = 0; +- const scheduleFallbackCheck = (delay) => { +- state.scheduledWork.timeout(checkHasScrolled, delay, "checkFinishedScrollFallback"); +- }; +- const checkHasScrolled = () => { +- var _a3, _b, _c, _d; +- const isStillScrollingTo = state.scrollingTo; +- if (isStillScrollingTo) { +- numChecks++; +- const isNativeInitialPending = isNativeInitialNonZeroTarget(state) && !state.hasScrolled; +- const maxChecks = silentInitialDispatch ? 5 : isNativeInitialPending ? INITIAL_SCROLL_MAX_FALLBACK_CHECKS : 5; +- const shouldFinishZeroTarget = shouldFinishInitialZeroTargetScroll(ctx); +- const canFinishInitialScrollWithoutNativeProgress = shouldFinishInitialScrollWithoutNativeProgress( +- state, +- isStillScrollingTo +- ); +- const completionState = getResolvedScrollCompletionState(ctx, isStillScrollingTo); +- const canFinishAfterSilentNativeDispatch = Platform.OS === "android" && silentInitialDispatch && completionState.isAtResolvedTarget && numChecks >= 1; +- const shouldRetrySilentInitialNativeScroll = Platform.OS === "android" && canFinishAfterSilentNativeDispatch && !initialScrollCompletion.didRetrySilentInitialScroll(state); +- const shouldFinishAfterObservedScroll = state.hasScrolled && (!isStillScrollingTo.isInitialScroll || completionState.isAtResolvedTarget); +- const shouldRetryUnalignedInitialScroll = isStillScrollingTo.isInitialScroll && !completionState.isAtResolvedTarget && numChecks <= maxChecks; +- const shouldRetryUnalignedEndScroll = Platform.OS === "ios" && !isStillScrollingTo.isInitialScroll && isEndAlignedLastItemTarget(ctx, isStillScrollingTo) && !completionState.isAtResolvedTarget && numChecks <= maxChecks; +- if (shouldRetrySilentInitialNativeScroll) { +- const targetOffset = (_b = (_a3 = getInitialScrollWatchdogTargetOffset(state)) != null ? _a3 : isStillScrollingTo.targetOffset) != null ? _b : 0; +- const jiggleOffset = targetOffset >= SILENT_INITIAL_SCROLL_TARGET_EPSILON ? targetOffset - SILENT_INITIAL_SCROLL_TARGET_EPSILON : targetOffset + SILENT_INITIAL_SCROLL_TARGET_EPSILON; +- initialScrollCompletion.markSilentInitialScrollRetry(state); +- scrollToFallbackOffset(ctx, jiggleOffset); +- state.scheduledWork.frame( +- () => scrollToFallbackOffset(ctx, targetOffset), +- "checkFinishedScrollRetryFrame" + const complete = () => { + var _a4, _b2, _c2, _d, _e; + const shouldReleaseDeferredPublicOnScroll = Platform.OS === "web" && ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; @@ -1554,107 +1768,179 @@ index b3c5a30..9d7ad6d 100644 + }, + PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, + "preservedInitialScroll" -+ ); -+ } + ); +- scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); +- } else if (shouldRetryUnalignedEndScroll) { +- scrollToFallbackOffset(ctx, completionState.clampedTargetOffset); +- scheduleFallbackCheck(100); +- } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) { +- finishScrollTo(ctx); +- } else if ((isNativeInitialPending || shouldRetryUnalignedInitialScroll) && numChecks <= maxChecks) { +- const targetOffset = (_d = (_c = getInitialScrollWatchdogTargetOffset(state)) != null ? _c : isStillScrollingTo.targetOffset) != null ? _d : state.scrollPending; +- scrollToFallbackOffset(ctx, targetOffset); +- scheduleFallbackCheck(silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : 100); +- } else { +- scheduleFallbackCheck(silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : 100); + } + } else { + clearPreservedInitialScrollTarget(state); + } + if (options == null ? void 0 : options.recalculateItems) { + recalculateSettledScroll(ctx); -+ } + } + setInitialRenderState(ctx, { didInitialScroll: true }); + if (shouldReleaseDeferredPublicOnScroll) { + releaseDeferredPublicOnScroll(ctx, finalScrollOffset); + } + (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); -+ }; + }; +- scheduleFallbackCheck(initialDelay); +-} +- +-// src/core/doScrollTo.native.ts +-function doScrollTo(ctx, params) { +- const state = ctx.state; +- const { animated, horizontal, isInitialScroll, offset } = params; +- const isAnimated = !!animated; +- const { refScroller } = state; +- const scroller = refScroller.current; +- if (!scroller) { + if (options == null ? void 0 : options.waitForCompletionFrame) { + requestAnimationFrame(complete); -+ return; -+ } + return; + } +- const isHorizontal = !!horizontal; +- const contentSize = isHorizontal ? getContentSize(ctx) : void 0; +- const nativeOffset = toNativeHorizontalOffset(state, offset, contentSize); +- scroller.scrollTo({ +- animated: isAnimated, +- x: isHorizontal ? nativeOffset : 0, +- y: isHorizontal ? 0 : offset +- }); +- if (isInitialScroll) { +- initialScrollCompletion.markInitialScrollNativeDispatch(state); +- } +- if (isAnimated && Math.abs(state.scroll - offset) <= 1) { +- checkFinishedScroll(ctx); +- } +- if (!isAnimated) { +- state.scroll = offset; +- checkFinishedScrollFallback(ctx); +- } + complete(); -+} -+ - // src/core/scrollToIndex.ts - function clampScrollIndex(index, dataLength) { - if (dataLength <= 0) { -@@ -4320,6 +4455,7 @@ function calculateItemsInView(ctx, params = {}) { - startIndex - }); - totalSize = getContentSize(ctx); -+ const minIndexSizeChangedThisPass = minIndexSizeChanged; - if (minIndexSizeChanged !== void 0) { - state.minIndexSizeChanged = void 0; + } + + // src/core/scrollRequestTracker.ts +@@ -1612,60 +1732,6 @@ function getScrollRequestTracker(ctx) { + return ctx.scrollRequestTracker; + } + +-// src/utils/requestAdjust.ts +-function requestAdjust(ctx, positionDiff, source) { +- const state = ctx.state; +- if (Math.abs(positionDiff) > 0.1) { +- const dataChanged = source === "data"; +- const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff; +- const doit = () => { +- if (needsScrollWorkaround) { +- doScrollTo(ctx, { horizontal: state.props.horizontal, offset: state.scroll }); +- } else { +- state.scrollAdjustHandler.requestAdjust(positionDiff); +- if (state.adjustingFromInitialMount) { +- state.adjustingFromInitialMount--; +- } +- } +- }; +- state.scroll += positionDiff; +- state.scrollForNextCalculateItemsInView = void 0; +- const readyToRender = peek$(ctx, "readyToRender"); +- if (readyToRender) { +- doit(); +- if (Platform.OS !== "web" && source !== "item-size") { +- const threshold = state.scroll - positionDiff / 2; +- if (!state.ignoreScrollFromMVCP) { +- state.ignoreScrollFromMVCP = {}; +- } +- if (positionDiff > 0) { +- state.ignoreScrollFromMVCP.lt = threshold; +- } else { +- state.ignoreScrollFromMVCP.gt = threshold; +- } +- const delay = needsScrollWorkaround ? 250 : 100; +- state.scheduledWork.timeout( +- () => { +- var _a3; +- state.ignoreScrollFromMVCP = void 0; +- const shouldForceUpdate = state.ignoreScrollFromMVCPIgnored && state.scrollProcessingEnabled !== false; +- if (shouldForceUpdate) { +- state.ignoreScrollFromMVCPIgnored = false; +- state.scrollPending = state.scroll; +- (_a3 = state.reprocessCurrentScroll) == null ? void 0 : _a3.call(state); +- } +- }, +- delay, +- "ignoreScrollFromMVCP" +- ); +- } +- } else { +- state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; +- requestAnimationFrame(doit); +- } +- } +-} +- + // src/core/doMaintainScrollAtEnd.ts + function finishMaintainScrollAtEnd(ctx) { + var _a3; +@@ -2302,7 +2368,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + } + } + function scrollTo(ctx, params) { +- var _a3, _b; ++ var _a3, _b, _c; + const state = ctx.state; + const { noScrollingTo, forceScroll, ...scrollTarget } = params; + const { +@@ -2334,6 +2400,15 @@ function scrollTo(ctx, params) { + }; + if (!isInitialScroll) { + pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); ++ if (scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { ++ beginScrollTargetSettle(ctx, { ++ index: scrollTarget.index, ++ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, ++ viewPosition: scrollTarget.viewPosition ++ }); ++ } else { ++ clearScrollTargetSettle(state); ++ } } -@@ -4337,8 +4473,18 @@ function calculateItemsInView(ctx, params = {}) { + } + state.scrollPending = targetOffset; +@@ -2341,7 +2416,7 @@ function scrollTo(ctx, params) { + if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { + if (animated) { + if (state.scrollTargetPinnedRange) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); + } + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); +@@ -4415,7 +4490,8 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); - const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); -- if (didMVCPAdjustScroll) { -+ const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; -+ const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); -+ const mvcp = state.props.maintainVisibleContentPosition; -+ const isUnanchoredDataChange = dataChanged && !mvcp.data; -+ const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; -+ if (!suppressInitialScrollSideEffects) { -+ settleScrollTarget(ctx, { -+ isCompensating, -+ minIndexSizeChanged: isUnanchoredDataChange ? 0 : minIndexSizeChangedThisPass -+ }); -+ } -+ if (didMVCPAdjust) { ++ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx); ++ const didMVCPAdjustScroll = didSettleScrollTarget || !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); + if (didMVCPAdjustScroll) { updateScroll2(state.scroll); updateScrollRange(); - } -@@ -5831,6 +5977,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize - return Platform.OS === "web" && props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; - } - function setHeaderSize(ctx, size) { -+ var _a3; - const { state } = ctx; - const previousHeaderSize = peek$(ctx, "headerSize") || 0; - const didChange = previousHeaderSize !== size; -@@ -5840,6 +5987,8 @@ function setHeaderSize(ctx, size) { - updateContentMetricsState(ctx); - if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { - requestAdjust(ctx, size - previousHeaderSize); -+ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) { -+ doMaintainScrollAtEnd(ctx); - } - } - state.didMeasureHeader = true; -@@ -6926,13 +7075,14 @@ function getRenderedItem(ctx, key) { - - // src/utils/normalizeMaintainScrollAtEnd.ts - function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) { -- var _a3, _b, _c, _d; -+ var _a3, _b, _c, _d, _e; - return { - animated: false, - onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true, - onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true, -- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true, -- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true -+ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true, -+ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true, -+ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true - }; - } - function normalizeMaintainScrollAtEnd(value) { -@@ -7652,6 +7802,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded - onScrollBeginDrag: (event) => { - var _a4, _b2; - prepareReachedEdgeForNextUserScroll(ctx); -+ clearScrollTargetSettle(state); - (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); - }, - onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs -index 40e87cd..8e6f6d0 100644 +index 4ad913c..73aea1f 100644 --- a/node_modules/@legendapp/list/react-native.mjs +++ b/node_modules/@legendapp/list/react-native.mjs -@@ -436,611 +436,6 @@ var EDGE_POSITION_EPSILON = 1; +@@ -437,171 +437,279 @@ var EDGE_POSITION_EPSILON = 1; var ENABLE_DEVMODE = IS_DEV && false; var ENABLE_DEBUG_VIEW = IS_DEV && false; @@ -1664,35 +1950,111 @@ index 40e87cd..8e6f6d0 100644 - scheduledWork.cancel("checkFinishedScrollRetryFrame"); - scheduledWork.cancel("checkFinishedScrollFallback"); - scheduledWork.cancel("platformScrollCompletion"); --} ++// src/core/getStartOffsetAdjustment.ts ++function getStartOffsetAdjustment(ctx) { ++ const { state } = ctx; ++ const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; ++ return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); + } -function settlePendingImperativeScroll(state) { - var _a3, _b; - const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; - state.pendingScrollResolve = void 0; - state.pendingScrollToEnd = void 0; - resolvePendingScroll == null ? void 0 : resolvePendingScroll(); --} ++ ++// src/utils/getId.ts ++function getId(state, index) { ++ const { data, keyExtractor } = state.props; ++ if (!data) { ++ return ""; ++ } ++ const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; ++ const id = ret; ++ state.idCache[index] = id; ++ return id; + } -function cancelImperativeScroll(state) { - cancelScrollCompletionChecks(state); - state.scheduledWork.cancel("imperativeScrollReady"); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; - settlePendingImperativeScroll(state); --} -- --// src/core/deferredPublicOnScroll.ts --function withResolvedContentOffset(state, event, resolvedOffset) { -- return { -- ...event, -- nativeEvent: { ++ ++// src/core/updateContentMetricsState.ts ++function getRawContentLength(ctx) { ++ var _a3, _b, _c; ++ const { state, values } = ctx; ++ return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); ++} ++function getAlignItemsAtEndPadding(ctx) { ++ const { state } = ctx; ++ const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; ++ return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; ++} ++function updateContentMetricsState(ctx) { ++ var _a3; ++ const { state } = ctx; ++ const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; ++ const nextPadding = getAlignItemsAtEndPadding(ctx); ++ if (previousPadding !== nextPadding) { ++ const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated"; ++ const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold")); ++ if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) { ++ state.scheduledWork.microtask(() => { ++ if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) { ++ set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx)); ++ } ++ }, "alignItemsAtEndPaddingFallback"); ++ } else if (!isMaintainExpected) { ++ set$(ctx, "alignItemsAtEndPadding", nextPadding); ++ } ++ } + } + +-// src/core/deferredPublicOnScroll.ts +-function withResolvedContentOffset(state, event, resolvedOffset) { +- return { +- ...event, +- nativeEvent: { - ...event.nativeEvent, - contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -- } ++// src/core/addTotalSize.ts ++function addTotalSize(ctx, key, add, notifyTotalSize = true) { ++ const state = ctx.state; ++ const prevTotalSize = state.totalSize; ++ let totalSize = state.totalSize; ++ if (key === null) { ++ totalSize = add; ++ if (state.timeoutSetPaddingTop) { ++ clearTimeout(state.timeoutSetPaddingTop); ++ state.timeoutSetPaddingTop = void 0; + } - }; --} ++ } else { ++ totalSize += add; ++ } ++ if (prevTotalSize !== totalSize) { ++ if (!IsNewArchitecture && state.initialScroll && totalSize < prevTotalSize) { ++ state.pendingTotalSize = totalSize; ++ } else { ++ state.pendingTotalSize = void 0; ++ state.totalSize = totalSize; ++ if (notifyTotalSize) { ++ set$(ctx, "totalSize", totalSize); ++ } ++ updateContentMetricsState(ctx); ++ } ++ } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { ++ set$(ctx, "totalSize", totalSize); ++ } + } -function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { - var _a3, _b, _c, _d; -- const state = ctx.state; ++ ++// src/core/setSize.ts ++function setSize(ctx, itemKey, size, notifyTotalSize = true) { + const state = ctx.state; - const deferredEvent = state.deferredPublicOnScrollEvent; - state.deferredPublicOnScrollEvent = void 0; - if (deferredEvent) { @@ -1704,18 +2066,29 @@ index 40e87cd..8e6f6d0 100644 - (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 - ) - ); -- } --} -- ++ const { sizes } = state; ++ const previousSize = sizes.get(itemKey); ++ const diff = previousSize !== void 0 ? size - previousSize : size; ++ if (diff !== 0) { ++ addTotalSize(ctx, itemKey, diff, notifyTotalSize); + } ++ sizes.set(itemKey, size); + } + -// src/core/initialScrollSession.ts -var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -function hasInitialScrollSessionCompletion(completion) { - return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); --} ++// src/utils/helpers.ts ++function isFunction(obj) { ++ return typeof obj === "function"; + } -function clearInitialScrollSession(state) { - state.initialScrollSession = void 0; - return void 0; --} ++function isArray(obj) { ++ return Array.isArray(obj); + } -function createInitialScrollSession(options) { - const { bootstrap, completion, kind, previousDataLength } = options; - return kind === "offset" ? { @@ -1728,7 +2101,13 @@ index 40e87cd..8e6f6d0 100644 - kind, - previousDataLength - }; --} ++var warned = /* @__PURE__ */ new Set(); ++function warnDevOnce(id, text) { ++ if (IS_DEV && !warned.has(id)) { ++ warned.add(id); ++ console.warn(`[legend-list] ${text}`); ++ } + } -function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { - var _a4, _b2; - if (!state.initialScrollSession) { @@ -1744,10 +2123,37 @@ index 40e87cd..8e6f6d0 100644 - kind, - previousDataLength: state.initialScrollSession.previousDataLength - }); -- } ++function roundSize(size) { ++ return Math.floor(size * 8) / 8; ++} ++function isNullOrUndefined(value) { ++ return value === null || value === void 0; ++} ++function getPadding(s, type) { ++ var _a3, _b, _c; ++ const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; ++ return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; ++} ++function extractPadding(style, contentContainerStyle, type) { ++ return getPadding(style, type) + getPadding(contentContainerStyle, type); ++} ++function findContainerId(ctx, key) { ++ var _a3, _b; ++ const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); ++ if (directMatch !== void 0) { ++ return directMatch; + } - (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; - return state.initialScrollSession.completion; --} ++ const numContainers = peek$(ctx, "numContainers"); ++ for (let i = 0; i < numContainers; i++) { ++ const itemKey = peek$(ctx, `containerItemKey${i}`); ++ if (itemKey === key) { ++ return i; ++ } ++ } ++ return -1; + } -var initialScrollCompletion = { - didDispatchNativeScroll(state) { - var _a3, _b; @@ -1766,11 +2172,24 @@ index 40e87cd..8e6f6d0 100644 - resetFlags(state) { - if (!state.initialScrollSession) { - return; -- } ++ ++// src/utils/getItemSize.ts ++function getKnownOrFixedSize(ctx, key, index, data, resolved) { ++ var _a3, _b; ++ const state = ctx.state; ++ const { getFixedItemSize, getItemType } = state.props; ++ let size = key ? state.sizesKnown.get(key) : void 0; ++ if (size === void 0 && key && getFixedItemSize) { ++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; ++ const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); ++ if (fixedSize !== void 0) { ++ size = fixedSize + ctx.scrollAxisGap; ++ state.sizesKnown.set(key, size); + } - const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); - completion.didDispatchNativeScroll = void 0; - completion.didRetrySilentInitialScroll = void 0; -- } + } -}; -var initialScrollWatchdog = { - clear(state) { @@ -1794,16 +2213,29 @@ index 40e87cd..8e6f6d0 100644 - var _a3, _b; - if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { - return; -- } ++ return size; ++} ++function getKnownOrFixedItemSize(ctx, index) { ++ const key = getId(ctx.state, index); ++ return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); ++} ++function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { ++ for (let index = startIndex; index <= endIndex; index++) { ++ if (getKnownOrFixedItemSize(ctx, index) === void 0) { ++ return false; + } - const completion = ensureInitialScrollSessionCompletion(state); - completion.watchdog = watchdog ? { - startScroll: watchdog.startScroll, - targetOffset: watchdog.targetOffset - } : void 0; -- } + } -}; -function setInitialScrollSession(state, options = {}) { -- var _a3, _b, _c, _d; ++ return true; ++} ++function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { + var _a3, _b, _c, _d; - const existingSession = state.initialScrollSession; - const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; - const completion = existingSession == null ? void 0 : existingSession.completion; @@ -1811,10 +2243,26 @@ index 40e87cd..8e6f6d0 100644 - const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; - if (!kind) { - return clearInitialScrollSession(state); -- } ++ const state = ctx.state; ++ const { ++ sizes, ++ averageSizes, ++ props: { estimatedItemSize, getItemType }, ++ scrollingTo ++ } = state; ++ const sizeKnown = state.sizesKnown.get(key); ++ if (sizeKnown !== void 0) { ++ return sizeKnown; + } - if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { - return clearInitialScrollSession(state); -- } ++ let size; ++ const renderedSize = sizes.get(key); ++ if (preferCachedSize) { ++ if (renderedSize !== void 0) { ++ return renderedSize; ++ } + } - const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; - state.initialScrollSession = createInitialScrollSession({ - bootstrap, @@ -1823,201 +2271,102 @@ index 40e87cd..8e6f6d0 100644 - previousDataLength - }); - return state.initialScrollSession; --} -- --// src/utils/checkThreshold.ts --var HYSTERESIS_MULTIPLIER = 1.3; --function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { -- const absDistance = Math.abs(distance); -- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; --} --var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { -- const absDistance = Math.abs(distance); -- const within = atThreshold || threshold > 0 && absDistance <= threshold; -- const updateSnapshot = () => { -- setSnapshot({ -- atThreshold, -- contentSize: context.contentSize, -- dataLength: context.dataLength, -- scrollPosition: context.scrollPosition -- }); -- }; -- if (!wasReached) { -- if (!within) { -- return false; -- } -- onReached(distance); -- updateSnapshot(); -- return true; -- } -- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); -- if (reset) { -- setSnapshot(void 0); -- return false; -- } -- if (within) { -- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; -- if (changed) { -- updateSnapshot(); -- } -- } -- return true; --}; -- --// src/utils/edgeReachedGate.ts --function resetEdgeLatch(ctx, edge) { -- const state = ctx.state; -- if (edge === "start") { -- state.isStartReached = false; -- state.startReachedSnapshot = void 0; -- } else { -- state.isEndReached = false; -- state.endReachedSnapshot = void 0; -- } --} --function resetSharedEdgeGateIfOutsideHysteresis(ctx) { -- const state = ctx.state; -- if (!state.edgeReachedGate) { -- return; -- } -- const contentSize = getContentSize(ctx); -- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); -- const isContentLess = contentSize < state.scrollLength; -- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; -- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; -- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); -- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); -- if (isOutsideStart && isOutsideEnd) { -- state.edgeReachedGate = void 0; -- } --} --function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { -- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; --} --function markReachedEdge(ctx) { -- ctx.state.edgeReachedGate = "closed"; --} --function prepareReachedEdgeForNextUserScroll(ctx) { -- if (ctx.state.edgeReachedGate) { -- ctx.state.edgeReachedGate = "prepared"; -- } --} --function beginReachedEdgeUserScroll(ctx, scrollDelta) { -- const state = ctx.state; -- if (state.edgeReachedGate !== "prepared") { -- return void 0; -- } -- const allowedEdge = scrollDelta < 0 ? "start" : "end"; -- state.edgeReachedGate = "closed"; -- resetEdgeLatch(ctx, allowedEdge); -- return allowedEdge; --} -- --// src/utils/hasActiveInitialScroll.ts --function hasActiveInitialScroll(state) { -- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; --} -- --// src/utils/checkAtBottom.ts --function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -- var _a3; -- const state = ctx.state; -- if (!state) { -- return; -- } -- const { -- queuedInitialLayout, -- scrollLength, -- scroll, -- maintainingScrollAtEnd, -- props: { maintainScrollAtEndThreshold, onEndReachedThreshold } -- } = state; -- const contentSize = getContentSize(ctx); -- resetSharedEdgeGateIfOutsideHysteresis(ctx); -- if (contentSize > 0 && queuedInitialLayout) { -- const insetEnd = getContentInsetEnd(ctx); -- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; -- const isContentLess = contentSize < scrollLength; -- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); -- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); -- set$( -- ctx, -- "isWithinMaintainScrollAtEndThreshold", -- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength -- ); -- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; -- if (!shouldSkipThresholdChecks) { -- state.isEndReached = checkThreshold( -- distanceFromEnd, -- isContentLess, -- onEndReachedThreshold * scrollLength, -- state.isEndReached, -- state.endReachedSnapshot, -- { -- contentSize, -- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, -- scrollPosition: scroll -- }, -- (distance) => { -- var _a4, _b; -- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { -- markReachedEdge(ctx); -- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); -- } -- }, -- (snapshot) => { -- state.endReachedSnapshot = snapshot; -- } -- ); -- } -- } --} -- --// src/utils/checkAtTop.ts --function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -- const state = ctx == null ? void 0 : ctx.state; -- if (!state) { -- return; -- } -- const { -- isStartReached, -- props: { data, onStartReachedThreshold }, -- scroll, -- scrollLength, -- startReachedSnapshot, -- totalSize -- } = state; -- const dataLength = data.length; -- const threshold = onStartReachedThreshold * scrollLength; -- resetSharedEdgeGateIfOutsideHysteresis(ctx); -- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { -- state.isStartReached = false; -- state.startReachedSnapshot = void 0; -- } -- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); -- set$(ctx, "isNearStart", scroll <= threshold); -- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; -- if (!shouldSkipThresholdChecks) { -- state.isStartReached = checkThreshold( -- scroll, -- false, -- threshold, -- state.isStartReached, -- startReachedSnapshot, -- { -- contentSize: totalSize, -- dataLength, -- scrollPosition: scroll -- }, -- (distance) => { -- var _a3, _b; -- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { -- markReachedEdge(ctx); -- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); -- } -- }, -- (snapshot) => { -- state.startReachedSnapshot = snapshot; ++ size = getKnownOrFixedSize(ctx, key, index, data, resolved); ++ if (size !== void 0) { ++ setSize(ctx, key, size, notifyTotalSize); ++ return size; ++ } ++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; ++ if (useAverageSize && !scrollingTo) { ++ const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; ++ if (averageSizeForType !== void 0) { ++ size = roundSize(averageSizeForType); ++ } ++ } ++ if (size === void 0 && renderedSize !== void 0) { ++ return renderedSize; ++ } ++ if (size === void 0 && useAverageSize && scrollingTo) { ++ const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; ++ if (averageSizeForType !== void 0) { ++ size = roundSize(averageSizeForType); ++ } ++ } ++ if (size === void 0) { ++ size = estimatedItemSize + ctx.scrollAxisGap; ++ } ++ setSize(ctx, key, size, notifyTotalSize); ++ return size; ++} ++function getItemSizeAtIndex(ctx, index) { ++ if (index === void 0 || index < 0) { ++ return void 0; ++ } ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; ++ } ++ if (index !== void 0) { ++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); ++ if (startOffsetAdjustment) { ++ offset += startOffsetAdjustment; ++ } ++ } ++ if (viewPosition !== void 0 && index !== void 0) { ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return offset; ++ } ++ const isOutOfBounds = index < 0 || index >= dataLength; ++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; ++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); ++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); ++ const trailingInset = getContentInsetEnd(ctx); ++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); ++ if (!isOutOfBounds && index === state.props.data.length - 1) { ++ const footerSize = peek$(ctx, "footerSize") || 0; ++ offset += footerSize; ++ } ++ } ++ return offset; ++} ++ ++// src/core/clampScrollOffset.ts ++function clampScrollOffset(ctx, offset, scrollTarget) { ++ const state = ctx.state; ++ const contentSize = getContentSize(ctx); ++ let clampedOffset = offset; ++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android" || state.lastLayout)) { ++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); ++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; ++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; ++ const maxOffset = baseMaxOffset + extraEndOffset; ++ clampedOffset = Math.min(offset, maxOffset); ++ } ++ clampedOffset = Math.max(0, clampedOffset); ++ return clampedOffset; ++} ++ ++// src/core/calculateOffsetForIndex.ts ++function calculateOffsetForIndex(ctx, index) { ++ const state = ctx.state; ++ return index !== void 0 ? state.positions[index] || 0 : 0; + } + + // src/utils/checkThreshold.ts +@@ -803,748 +911,760 @@ function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { + }, + (snapshot) => { + state.startReachedSnapshot = snapshot; - } - ); - } @@ -2057,8 +2406,10 @@ index 40e87cd..8e6f6d0 100644 - setAdaptiveRender(ctx, "normal", "scroll"); - } else { - state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); -- } --} ++ } ++ ); + } + } -function setAdaptiveRender(ctx, mode, reason) { - var _a3, _b; - const previousMode = peek$(ctx, "adaptiveRender"); @@ -2066,18 +2417,35 @@ index 40e87cd..8e6f6d0 100644 - set$(ctx, "adaptiveRender", mode); - (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); - } --} ++ ++// src/utils/checkThresholds.ts ++function checkThresholds(ctx, allowedEdge) { ++ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; ++ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); ++ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + } -function resetAdaptiveRender(ctx) { -- var _a3, _b; ++ ++// src/core/recalculateSettledScroll.ts ++function recalculateSettledScroll(ctx) { + var _a3, _b; - clearAdaptiveRenderExitTimeout(ctx); - const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; - if (peek$(ctx, "adaptiveRender") !== mode) { - setAdaptiveRender(ctx, mode, "initial"); -- } --} ++ const state = ctx.state; ++ if ((_a3 = state.props) == null ? void 0 : _a3.data) { ++ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); + } ++ checkThresholds(ctx); + } -function updateAdaptiveRender(ctx, scrollVelocity, options) { - var _a3, _b, _c; -- const state = ctx.state; ++ ++// src/core/finishScrollTo.ts ++function finishScrollTo(ctx) { ++ var _a3, _b; + const state = ctx.state; - const adaptiveRender = state.props.adaptiveRender; - const currentMode = peek$(ctx, "adaptiveRender"); - if (peek$(ctx, "readyToRender")) { @@ -2099,10 +2467,40 @@ index 40e87cd..8e6f6d0 100644 - } - } else { - resetAdaptiveRender(ctx); -- } -- } --} -- ++ if (state == null ? void 0 : state.scrollingTo) { ++ cancelScrollCompletionChecks(state); ++ const resolvePendingScroll = state.pendingScrollResolve; ++ state.pendingScrollResolve = void 0; ++ const scrollingTo = state.scrollingTo; ++ state.scrollHistory.length = 0; ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ if (state.pendingTotalSize !== void 0) { ++ addTotalSize(ctx, null, state.pendingTotalSize); + } ++ if (PlatformAdjustBreaksScroll) { ++ state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); ++ } ++ if (scrollingTo.isInitialScroll || state.initialScroll) { ++ const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; ++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; ++ finishInitialScroll(ctx, { ++ onFinished: () => { ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); ++ }, ++ preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, ++ recalculateItems: true, ++ schedulePreservedTargetClear: shouldPreserveResizeTarget, ++ syncObservedOffset: isOffsetSession, ++ waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame ++ }); ++ return; ++ } ++ recalculateSettledScroll(ctx); ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); + } + } + -// src/utils/getEffectiveDrawDistance.ts -var INITIAL_DRAW_DISTANCE = 50; -function getEffectiveDrawDistance(ctx, mode) { @@ -2112,17 +2510,55 @@ index 40e87cd..8e6f6d0 100644 - const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; - const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; - return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; --} ++// src/core/initialScrollSession.ts ++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; ++function hasInitialScrollSessionCompletion(completion) { ++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); + } -function scheduleFullDrawDistancePrewarm(ctx) { - const { state } = ctx; - if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { - return; -- } ++function clearInitialScrollSession(state) { ++ state.initialScrollSession = void 0; ++ return void 0; ++} ++function createInitialScrollSession(options) { ++ const { bootstrap, completion, kind, previousDataLength } = options; ++ return kind === "offset" ? { ++ completion, ++ kind, ++ previousDataLength ++ } : { ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }; ++} ++function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { ++ var _a4, _b2; ++ if (!state.initialScrollSession) { ++ state.initialScrollSession = createInitialScrollSession({ ++ completion: {}, ++ kind, ++ previousDataLength: 0 ++ }); ++ } else if (state.initialScrollSession.kind !== kind) { ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, ++ completion: state.initialScrollSession.completion, ++ kind, ++ previousDataLength: state.initialScrollSession.previousDataLength ++ }); + } - state.scheduledWork.frame(() => { - var _a3; - return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); - }, "fullDrawDistancePrewarm"); --} ++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; ++ return state.initialScrollSession.completion; + } - -// src/utils/setInitialRenderState.ts -function resetInitialRenderState(ctx, { @@ -2133,10 +2569,61 @@ index 40e87cd..8e6f6d0 100644 - if (resetLayout) { - state.didContainersLayout = false; - state.queuedInitialLayout = false; -- } ++var initialScrollCompletion = { ++ didDispatchNativeScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); ++ }, ++ didRetrySilentInitialScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); ++ }, ++ markInitialScrollNativeDispatch(state) { ++ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; ++ }, ++ markSilentInitialScrollRetry(state) { ++ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; ++ }, ++ resetFlags(state) { ++ if (!state.initialScrollSession) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); ++ completion.didDispatchNativeScroll = void 0; ++ completion.didRetrySilentInitialScroll = void 0; + } - if (resetInitialScroll) { - state.didFinishInitialScroll = false; -- } ++}; ++var initialScrollWatchdog = { ++ clear(state) { ++ initialScrollWatchdog.set(state, void 0); ++ }, ++ didReachTarget(newScroll, watchdog) { ++ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); ++ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ get(state) { ++ var _a3, _b; ++ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; ++ }, ++ hasNonZeroTargetOffset(targetOffset) { ++ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ isAtZeroTargetOffset(targetOffset) { ++ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ set(state, watchdog) { ++ var _a3, _b; ++ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { ++ return; ++ } ++ const completion = ensureInitialScrollSessionCompletion(state); ++ completion.watchdog = watchdog ? { ++ startScroll: watchdog.startScroll, ++ targetOffset: watchdog.targetOffset ++ } : void 0; + } - set$(ctx, "readyToRender", false); - resetAdaptiveRender(ctx); -} @@ -2151,25 +2638,61 @@ index 40e87cd..8e6f6d0 100644 - } = state; - if (didLayout) { - state.didContainersLayout = true; -- } ++}; ++function setInitialScrollSession(state, options = {}) { ++ var _a3, _b, _c, _d; ++ const existingSession = state.initialScrollSession; ++ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; ++ const completion = existingSession == null ? void 0 : existingSession.completion; ++ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; ++ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; ++ if (!kind) { ++ return clearInitialScrollSession(state); + } - if (didInitialScroll) { - state.didFinishInitialScroll = true; -- } ++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { ++ return clearInitialScrollSession(state); + } - const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); - if (isReadyToRender && !peek$(ctx, "readyToRender")) { - set$(ctx, "readyToRender", true); - setAdaptiveRender(ctx, "normal", "ready"); - if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { - scheduleFullDrawDistancePrewarm(ctx); -- } ++ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }); ++ return state.initialScrollSession; ++} ++ ++// src/core/checkFinishedScroll.ts ++var INITIAL_SCROLL_MAX_FALLBACK_CHECKS = 20; ++var INITIAL_SCROLL_COMPLETION_TARGET_EPSILON = 1; ++var INITIAL_SCROLL_ZERO_TARGET_EPSILON = 1; ++var SILENT_INITIAL_SCROLL_RETRY_DELAY_MS = 16; ++var SILENT_INITIAL_SCROLL_TARGET_EPSILON = 1; ++function checkFinishedScroll(ctx, options) { ++ const scrollingTo = ctx.state.scrollingTo; ++ if (options == null ? void 0 : options.onlyIfAligned) { ++ if (!(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) || scrollingTo.animated) { ++ return; + } - if (!state.didLoad) { - state.didLoad = true; - if (onLoad) { - onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); - } -- } -- } --} ++ if (!getResolvedScrollCompletionState(ctx, scrollingTo).isAtResolvedTarget) { ++ return; + } + } ++ ctx.state.scheduledWork.frame(() => checkFinishedScrollFrame(ctx), "checkFinishedScrollFrame"); + } - -// src/core/finishInitialScroll.ts -var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -2177,16 +2700,24 @@ index 40e87cd..8e6f6d0 100644 - state.scroll = offset; - state.scrollPending = offset; - state.scrollPrev = offset; --} ++function hasScrollCompletionOwnership(state, options) { ++ const { clampedTargetOffset, scrollingTo } = options; ++ return !scrollingTo.isInitialScroll || state.hasScrolled || clampedTargetOffset <= INITIAL_SCROLL_COMPLETION_TARGET_EPSILON; + } -function clearPreservedInitialScrollTargetTimeout(state) { - state.scheduledWork.cancel("preservedInitialScroll"); --} ++function isSilentInitialDispatch(state, scrollingTo) { ++ return !!(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) && initialScrollCompletion.didDispatchNativeScroll(state) && !state.hasScrolled; + } -function clearPreservedInitialScrollTarget(state) { - clearPreservedInitialScrollTargetTimeout(state); - state.clearPreservedInitialScrollOnNextFinish = void 0; - state.initialScroll = void 0; - setInitialScrollSession(state); --} ++function getInitialScrollWatchdogTargetOffset(state) { ++ var _a3; ++ return (_a3 = initialScrollWatchdog.get(state)) == null ? void 0 : _a3.targetOffset; + } -function supersedeInitialScroll(ctx) { - var _a3, _b, _c; - const state = ctx.state; @@ -2204,7 +2735,10 @@ index 40e87cd..8e6f6d0 100644 - setInitialScrollSession(state, { bootstrap: null }); - finishInitialScroll(ctx); - } --} ++function isNativeInitialNonZeroTarget(state) { ++ const targetOffset = getInitialScrollWatchdogTargetOffset(state); ++ return !state.didFinishInitialScroll && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); + } -function finishInitialScroll(ctx, options) { - var _a3, _b, _c; - const state = ctx.state; @@ -2215,7 +2749,11 @@ index 40e87cd..8e6f6d0 100644 - if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { - syncInitialScrollOffset(state, observedOffset); - } -- } ++function shouldFinishInitialScrollWithoutNativeProgress(state, scrollingTo) { ++ var _a3, _b; ++ if (!scrollingTo.isInitialScroll || scrollingTo.animated || !state.didContainersLayout) { ++ return false; + } - const complete = () => { - var _a4, _b2, _c2, _d, _e; - const shouldReleaseDeferredPublicOnScroll = Platform.OS === "web" && ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; @@ -2253,7 +2791,9 @@ index 40e87cd..8e6f6d0 100644 - if (options == null ? void 0 : options.waitForCompletionFrame) { - requestAnimationFrame(complete); - return; -- } ++ if (((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap") { ++ return false; + } - complete(); -} - @@ -2261,279 +2801,435 @@ index 40e87cd..8e6f6d0 100644 -function calculateOffsetForIndex(ctx, index) { - const state = ctx.state; - return index !== void 0 ? state.positions[index] || 0 : 0; --} ++ const targetOffset = (_b = scrollingTo.targetOffset) != null ? _b : scrollingTo.offset; ++ if (initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset) && initialScrollCompletion.didDispatchNativeScroll(state) && !state.hasScrolled) { ++ return false; ++ } ++ if (initialScrollWatchdog.isAtZeroTargetOffset(targetOffset) || Math.abs(state.scroll - targetOffset) > 1 || Math.abs(state.scrollPending - targetOffset) > 1) { ++ return false; ++ } ++ return !!scrollingTo.waitForInitialScrollCompletionFrame || isNativeInitialNonZeroTarget(state); + } - - // src/core/getStartOffsetAdjustment.ts - function getStartOffsetAdjustment(ctx) { +-// src/core/getStartOffsetAdjustment.ts +-function getStartOffsetAdjustment(ctx) { ++function shouldFinishInitialZeroTargetScroll(ctx) { ++ var _a3; const { state } = ctx; -@@ -1244,58 +639,279 @@ function getItemSizeAtIndex(ctx, index) { - if (index === void 0 || index < 0) { - return void 0; - } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+ const targetId = getId(ctx.state, index); -+ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+} -+ -+// src/core/calculateOffsetWithOffsetPosition.ts -+function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; +- return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); ++ return !!((_a3 = state.scrollingTo) == null ? void 0 : _a3.isInitialScroll) && state.props.data.length > 0 && getContentSize(ctx) <= state.scrollLength && state.scrollPending <= INITIAL_SCROLL_ZERO_TARGET_EPSILON; + } +- +-// src/utils/getId.ts +-function getId(state, index) { +- const { data, keyExtractor } = state.props; +- if (!data) { +- return ""; +- } +- const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; +- const id = ret; +- state.idCache[index] = id; +- return id; ++function isEndAlignedLastItemTarget(ctx, scrollingTo) { ++ return scrollingTo.index === ctx.state.props.data.length - 1 && scrollingTo.viewPosition === 1; + } +- +-// src/core/updateContentMetricsState.ts +-function getRawContentLength(ctx) { +- var _a3, _b, _c; +- const { state, values } = ctx; +- return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); ++function getCurrentTargetOffset(ctx, scrollingTo) { + var _a3; -+ const state = ctx.state; -+ const { index, viewOffset, viewPosition } = params; -+ let offset = offsetParam; -+ if (viewOffset) { -+ offset -= viewOffset; -+ } -+ if (index !== void 0) { -+ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -+ if (startOffsetAdjustment) { -+ offset += startOffsetAdjustment; -+ } -+ } -+ if (viewPosition !== void 0 && index !== void 0) { -+ const dataLength = state.props.data.length; -+ if (dataLength === 0) { -+ return offset; -+ } -+ const isOutOfBounds = index < 0 || index >= dataLength; -+ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -+ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -+ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -+ const trailingInset = getContentInsetEnd(ctx); -+ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -+ if (!isOutOfBounds && index === state.props.data.length - 1) { -+ const footerSize = peek$(ctx, "footerSize") || 0; -+ offset += footerSize; -+ } -+ } -+ return offset; -+} -+ -+// src/core/clampScrollOffset.ts -+function clampScrollOffset(ctx, offset, scrollTarget) { -+ const state = ctx.state; -+ const contentSize = getContentSize(ctx); -+ let clampedOffset = offset; -+ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android" || state.lastLayout)) { -+ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); -+ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; -+ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; -+ const maxOffset = baseMaxOffset + extraEndOffset; -+ clampedOffset = Math.min(offset, maxOffset); -+ } -+ clampedOffset = Math.max(0, clampedOffset); -+ return clampedOffset; -+} -+ -+// src/core/calculateOffsetForIndex.ts -+function calculateOffsetForIndex(ctx, index) { -+ const state = ctx.state; -+ return index !== void 0 ? state.positions[index] || 0 : 0; -+} -+ -+// src/utils/checkThreshold.ts -+var HYSTERESIS_MULTIPLIER = 1.3; -+function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { -+ const absDistance = Math.abs(distance); -+ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; -+} -+var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { -+ const absDistance = Math.abs(distance); -+ const within = atThreshold || threshold > 0 && absDistance <= threshold; -+ const updateSnapshot = () => { -+ setSnapshot({ -+ atThreshold, -+ contentSize: context.contentSize, -+ dataLength: context.dataLength, -+ scrollPosition: context.scrollPosition -+ }); ++ const index = scrollingTo.index; ++ const shouldRecomputeEndTarget = isEndAlignedLastItemTarget(ctx, scrollingTo); ++ const requestedTargetOffset = shouldRecomputeEndTarget && index !== void 0 ? calculateOffsetWithOffsetPosition(ctx, calculateOffsetForIndex(ctx, index), scrollingTo) : (_a3 = scrollingTo.targetOffset) != null ? _a3 : clampScrollOffset(ctx, scrollingTo.offset - (scrollingTo.viewOffset || 0), scrollingTo); ++ return clampScrollOffset(ctx, requestedTargetOffset, scrollingTo); + } +-function getAlignItemsAtEndPadding(ctx) { ++function getResolvedScrollCompletionState(ctx, scrollingTo) { + const { state } = ctx; +- const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; +- return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; ++ const scroll = state.scrollPending; ++ const adjust = state.scrollAdjustHandler.getAdjust(); ++ const clampedTargetOffset = getCurrentTargetOffset(ctx, scrollingTo); ++ const maxOffset = clampScrollOffset(ctx, scroll, scrollingTo); ++ const diff1 = Math.abs(scroll - clampedTargetOffset); ++ const adjustedTargetOffset = clampedTargetOffset + adjust; ++ const diff2 = Math.abs(scroll - adjustedTargetOffset); ++ const canUseAdjustedCompletion = !scrollingTo.animated || Platform.OS === "ios"; ++ return { ++ clampedTargetOffset, ++ isAtResolvedTarget: Math.abs(scroll - maxOffset) < 1 && (diff1 < 1 || canUseAdjustedCompletion && diff2 < 1) + }; -+ if (!wasReached) { -+ if (!within) { -+ return false; -+ } -+ onReached(distance); -+ updateSnapshot(); -+ return true; -+ } -+ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); -+ if (reset) { -+ setSnapshot(void 0); -+ return false; -+ } -+ if (within) { -+ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; -+ if (changed) { -+ updateSnapshot(); -+ } -+ } -+ return true; -+}; -+ -+// src/utils/edgeReachedGate.ts -+function resetEdgeLatch(ctx, edge) { -+ const state = ctx.state; -+ if (edge === "start") { -+ state.isStartReached = false; -+ state.startReachedSnapshot = void 0; -+ } else { -+ state.isEndReached = false; -+ state.endReachedSnapshot = void 0; -+ } -+} -+function resetSharedEdgeGateIfOutsideHysteresis(ctx) { -+ const state = ctx.state; -+ if (!state.edgeReachedGate) { + } +-function updateContentMetricsState(ctx) { +- var _a3; ++function checkFinishedScrollFrame(ctx) { ++ const scrollingTo = ctx.state.scrollingTo; ++ if (!scrollingTo) { + return; + } -+ const contentSize = getContentSize(ctx); -+ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); -+ const isContentLess = contentSize < state.scrollLength; -+ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; -+ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; -+ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); -+ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); -+ if (isOutsideStart && isOutsideEnd) { -+ state.edgeReachedGate = void 0; -+ } + const { state } = ctx; +- const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; +- const nextPadding = getAlignItemsAtEndPadding(ctx); +- if (previousPadding !== nextPadding) { +- const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated"; +- const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold")); +- if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) { +- state.scheduledWork.microtask(() => { +- if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) { +- set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx)); +- } +- }, "alignItemsAtEndPaddingFallback"); +- } else if (!isMaintainExpected) { +- set$(ctx, "alignItemsAtEndPadding", nextPadding); +- } ++ const completionState = getResolvedScrollCompletionState(ctx, scrollingTo); ++ if (completionState.isAtResolvedTarget && hasScrollCompletionOwnership(state, { ++ clampedTargetOffset: completionState.clampedTargetOffset, ++ scrollingTo ++ })) { ++ finishScrollTo(ctx); + } + } +- +-// src/core/addTotalSize.ts +-function addTotalSize(ctx, key, add, notifyTotalSize = true) { ++function scrollToFallbackOffset(ctx, offset) { ++ var _a3; ++ (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({ ++ animated: false, ++ x: ctx.state.props.horizontal ? offset : 0, ++ y: ctx.state.props.horizontal ? 0 : offset ++ }); +} -+function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { -+ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; -+} -+function markReachedEdge(ctx) { -+ ctx.state.edgeReachedGate = "closed"; -+} -+function prepareReachedEdgeForNextUserScroll(ctx) { -+ if (ctx.state.edgeReachedGate) { -+ ctx.state.edgeReachedGate = "prepared"; -+ } -+} -+function beginReachedEdgeUserScroll(ctx, scrollDelta) { -+ const state = ctx.state; -+ if (state.edgeReachedGate !== "prepared") { -+ return void 0; -+ } -+ const allowedEdge = scrollDelta < 0 ? "start" : "end"; -+ state.edgeReachedGate = "closed"; -+ resetEdgeLatch(ctx, allowedEdge); -+ return allowedEdge; -+} -+ -+// src/utils/hasActiveInitialScroll.ts -+function hasActiveInitialScroll(state) { -+ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; -+} -+ -+// src/utils/checkAtBottom.ts -+function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -+ var _a3; -+ const state = ctx.state; -+ if (!state) { ++function checkFinishedScrollFallback(ctx) { + const state = ctx.state; +- const prevTotalSize = state.totalSize; +- let totalSize = state.totalSize; +- if (key === null) { +- totalSize = add; +- if (state.timeoutSetPaddingTop) { +- clearTimeout(state.timeoutSetPaddingTop); +- state.timeoutSetPaddingTop = void 0; +- } +- } else { +- totalSize += add; ++ if (state.scheduledWork.has("checkFinishedScrollFallback")) { + return; -+ } -+ const { -+ queuedInitialLayout, -+ scrollLength, -+ scroll, -+ maintainingScrollAtEnd, -+ props: { maintainScrollAtEndThreshold, onEndReachedThreshold } -+ } = state; -+ const contentSize = getContentSize(ctx); -+ resetSharedEdgeGateIfOutsideHysteresis(ctx); -+ if (contentSize > 0 && queuedInitialLayout) { -+ const insetEnd = getContentInsetEnd(ctx); -+ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; -+ const isContentLess = contentSize < scrollLength; -+ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); -+ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); -+ set$( -+ ctx, -+ "isWithinMaintainScrollAtEndThreshold", -+ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength -+ ); -+ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; -+ if (!shouldSkipThresholdChecks) { -+ state.isEndReached = checkThreshold( -+ distanceFromEnd, -+ isContentLess, -+ onEndReachedThreshold * scrollLength, -+ state.isEndReached, -+ state.endReachedSnapshot, -+ { -+ contentSize, -+ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, -+ scrollPosition: scroll -+ }, -+ (distance) => { -+ var _a4, _b; -+ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { -+ markReachedEdge(ctx); -+ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); -+ } -+ }, -+ (snapshot) => { -+ state.endReachedSnapshot = snapshot; -+ } + } +- if (prevTotalSize !== totalSize) { +- if (!IsNewArchitecture && state.initialScroll && totalSize < prevTotalSize) { +- state.pendingTotalSize = totalSize; +- } else { +- state.pendingTotalSize = void 0; +- state.totalSize = totalSize; +- if (notifyTotalSize) { +- set$(ctx, "totalSize", totalSize); ++ const scrollingTo = state.scrollingTo; ++ const shouldFinishInitialZeroTarget = shouldFinishInitialZeroTargetScroll(ctx); ++ const silentInitialDispatch = isSilentInitialDispatch(state, scrollingTo); ++ const canFinishInitialWithoutNativeProgress = scrollingTo !== void 0 ? shouldFinishInitialScrollWithoutNativeProgress(state, scrollingTo) : false; ++ const slowTimeout = (scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) && !shouldFinishInitialZeroTarget && !canFinishInitialWithoutNativeProgress || !state.didContainersLayout; ++ const initialDelay = shouldFinishInitialZeroTarget || canFinishInitialWithoutNativeProgress ? 0 : silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : slowTimeout ? 500 : 100; ++ let numChecks = 0; ++ const scheduleFallbackCheck = (delay) => { ++ state.scheduledWork.timeout(checkHasScrolled, delay, "checkFinishedScrollFallback"); ++ }; ++ const checkHasScrolled = () => { ++ var _a3, _b, _c, _d; ++ const isStillScrollingTo = state.scrollingTo; ++ if (isStillScrollingTo) { ++ numChecks++; ++ const isNativeInitialPending = isNativeInitialNonZeroTarget(state) && !state.hasScrolled; ++ const maxChecks = silentInitialDispatch ? 5 : isNativeInitialPending ? INITIAL_SCROLL_MAX_FALLBACK_CHECKS : 5; ++ const shouldFinishZeroTarget = shouldFinishInitialZeroTargetScroll(ctx); ++ const canFinishInitialScrollWithoutNativeProgress = shouldFinishInitialScrollWithoutNativeProgress( ++ state, ++ isStillScrollingTo + ); -+ } -+ } -+} -+ -+// src/utils/checkAtTop.ts -+function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -+ const state = ctx == null ? void 0 : ctx.state; -+ if (!state) { ++ const completionState = getResolvedScrollCompletionState(ctx, isStillScrollingTo); ++ const canFinishAfterSilentNativeDispatch = Platform.OS === "android" && silentInitialDispatch && completionState.isAtResolvedTarget && numChecks >= 1; ++ const shouldRetrySilentInitialNativeScroll = Platform.OS === "android" && canFinishAfterSilentNativeDispatch && !initialScrollCompletion.didRetrySilentInitialScroll(state); ++ const shouldFinishAfterObservedScroll = state.hasScrolled && (!isStillScrollingTo.isInitialScroll || completionState.isAtResolvedTarget); ++ const shouldRetryUnalignedInitialScroll = isStillScrollingTo.isInitialScroll && !completionState.isAtResolvedTarget && numChecks <= maxChecks; ++ const shouldRetryUnalignedEndScroll = Platform.OS === "ios" && !isStillScrollingTo.isInitialScroll && isEndAlignedLastItemTarget(ctx, isStillScrollingTo) && !completionState.isAtResolvedTarget && numChecks <= maxChecks; ++ if (shouldRetrySilentInitialNativeScroll) { ++ const targetOffset = (_b = (_a3 = getInitialScrollWatchdogTargetOffset(state)) != null ? _a3 : isStillScrollingTo.targetOffset) != null ? _b : 0; ++ const jiggleOffset = targetOffset >= SILENT_INITIAL_SCROLL_TARGET_EPSILON ? targetOffset - SILENT_INITIAL_SCROLL_TARGET_EPSILON : targetOffset + SILENT_INITIAL_SCROLL_TARGET_EPSILON; ++ initialScrollCompletion.markSilentInitialScrollRetry(state); ++ scrollToFallbackOffset(ctx, jiggleOffset); ++ state.scheduledWork.frame( ++ () => scrollToFallbackOffset(ctx, targetOffset), ++ "checkFinishedScrollRetryFrame" ++ ); ++ scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); ++ } else if (shouldRetryUnalignedEndScroll) { ++ scrollToFallbackOffset(ctx, completionState.clampedTargetOffset); ++ scheduleFallbackCheck(100); ++ } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) { ++ finishScrollTo(ctx); ++ } else if ((isNativeInitialPending || shouldRetryUnalignedInitialScroll) && numChecks <= maxChecks) { ++ const targetOffset = (_d = (_c = getInitialScrollWatchdogTargetOffset(state)) != null ? _c : isStillScrollingTo.targetOffset) != null ? _d : state.scrollPending; ++ scrollToFallbackOffset(ctx, targetOffset); ++ scheduleFallbackCheck(silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : 100); ++ } else { ++ scheduleFallbackCheck(silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : 100); + } +- updateContentMetricsState(ctx); + } +- } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { +- set$(ctx, "totalSize", totalSize); +- } ++ }; ++ scheduleFallbackCheck(initialDelay); + } + +-// src/core/setSize.ts +-function setSize(ctx, itemKey, size, notifyTotalSize = true) { ++// src/core/doScrollTo.native.ts ++function doScrollTo(ctx, params) { + const state = ctx.state; +- const { sizes } = state; +- const previousSize = sizes.get(itemKey); +- const diff = previousSize !== void 0 ? size - previousSize : size; +- if (diff !== 0) { +- addTotalSize(ctx, itemKey, diff, notifyTotalSize); ++ const { animated, horizontal, isInitialScroll, offset } = params; ++ const isAnimated = !!animated; ++ const { refScroller } = state; ++ const scroller = refScroller.current; ++ if (!scroller) { + return; -+ } -+ const { -+ isStartReached, -+ props: { data, onStartReachedThreshold }, -+ scroll, -+ scrollLength, -+ startReachedSnapshot, -+ totalSize -+ } = state; -+ const dataLength = data.length; -+ const threshold = onStartReachedThreshold * scrollLength; -+ resetSharedEdgeGateIfOutsideHysteresis(ctx); -+ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { -+ state.isStartReached = false; -+ state.startReachedSnapshot = void 0; -+ } -+ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); -+ set$(ctx, "isNearStart", scroll <= threshold); -+ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; -+ if (!shouldSkipThresholdChecks) { -+ state.isStartReached = checkThreshold( -+ scroll, -+ false, -+ threshold, -+ state.isStartReached, -+ startReachedSnapshot, -+ { -+ contentSize: totalSize, -+ dataLength, -+ scrollPosition: scroll -+ }, -+ (distance) => { -+ var _a3, _b; -+ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { -+ markReachedEdge(ctx); -+ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); + } +- sizes.set(itemKey, size); +-} +- +-// src/utils/helpers.ts +-function isFunction(obj) { +- return typeof obj === "function"; +-} +-function isArray(obj) { +- return Array.isArray(obj); +-} +-var warned = /* @__PURE__ */ new Set(); +-function warnDevOnce(id, text) { +- if (IS_DEV && !warned.has(id)) { +- warned.add(id); +- console.warn(`[legend-list] ${text}`); ++ const isHorizontal = !!horizontal; ++ const contentSize = isHorizontal ? getContentSize(ctx) : void 0; ++ const nativeOffset = toNativeHorizontalOffset(state, offset, contentSize); ++ scroller.scrollTo({ ++ animated: isAnimated, ++ x: isHorizontal ? nativeOffset : 0, ++ y: isHorizontal ? 0 : offset ++ }); ++ if (isInitialScroll) { ++ initialScrollCompletion.markInitialScrollNativeDispatch(state); + } +-} +-function roundSize(size) { +- return Math.floor(size * 8) / 8; +-} +-function isNullOrUndefined(value) { +- return value === null || value === void 0; +-} +-function getPadding(s, type) { +- var _a3, _b, _c; +- const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; +- return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; +-} +-function extractPadding(style, contentContainerStyle, type) { +- return getPadding(style, type) + getPadding(contentContainerStyle, type); +-} +-function findContainerId(ctx, key) { +- var _a3, _b; +- const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); +- if (directMatch !== void 0) { +- return directMatch; ++ if (isAnimated && Math.abs(state.scroll - offset) <= 1) { ++ checkFinishedScroll(ctx); + } +- const numContainers = peek$(ctx, "numContainers"); +- for (let i = 0; i < numContainers; i++) { +- const itemKey = peek$(ctx, `containerItemKey${i}`); +- if (itemKey === key) { +- return i; +- } ++ if (!isAnimated) { ++ state.scroll = offset; ++ checkFinishedScrollFallback(ctx); + } +- return -1; + } + +-// src/utils/getItemSize.ts +-function getKnownOrFixedSize(ctx, key, index, data, resolved) { +- var _a3, _b; ++// src/utils/requestAdjust.ts ++function requestAdjust(ctx, positionDiff, source) { + const state = ctx.state; +- const { getFixedItemSize, getItemType } = state.props; +- let size = key ? state.sizesKnown.get(key) : void 0; +- if (size === void 0 && key && getFixedItemSize) { +- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; +- const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); +- if (fixedSize !== void 0) { +- size = fixedSize + ctx.scrollAxisGap; +- state.sizesKnown.set(key, size); +- } +- } +- return size; +-} +-function getKnownOrFixedItemSize(ctx, index) { +- const key = getId(ctx.state, index); +- return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); +-} +-function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { +- for (let index = startIndex; index <= endIndex; index++) { +- if (getKnownOrFixedItemSize(ctx, index) === void 0) { +- return false; ++ if (Math.abs(positionDiff) > 0.1) { ++ const dataChanged = source === "data"; ++ const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff; ++ const doit = () => { ++ if (needsScrollWorkaround) { ++ doScrollTo(ctx, { horizontal: state.props.horizontal, offset: state.scroll }); ++ } else { ++ state.scrollAdjustHandler.requestAdjust(positionDiff); ++ if (state.adjustingFromInitialMount) { ++ state.adjustingFromInitialMount--; + } -+ }, -+ (snapshot) => { -+ state.startReachedSnapshot = snapshot; + } -+ ); -+ } ++ }; ++ state.scroll += positionDiff; ++ state.scrollForNextCalculateItemsInView = void 0; ++ const readyToRender = peek$(ctx, "readyToRender"); ++ if (readyToRender) { ++ doit(); ++ if (Platform.OS !== "web" && source !== "item-size") { ++ const threshold = state.scroll - positionDiff / 2; ++ if (!state.ignoreScrollFromMVCP) { ++ state.ignoreScrollFromMVCP = {}; ++ } ++ if (positionDiff > 0) { ++ state.ignoreScrollFromMVCP.lt = threshold; ++ } else { ++ state.ignoreScrollFromMVCP.gt = threshold; ++ } ++ const delay = needsScrollWorkaround ? 250 : 100; ++ state.scheduledWork.timeout( ++ () => { ++ var _a3; ++ state.ignoreScrollFromMVCP = void 0; ++ const shouldForceUpdate = state.ignoreScrollFromMVCPIgnored && state.scrollProcessingEnabled !== false; ++ if (shouldForceUpdate) { ++ state.ignoreScrollFromMVCPIgnored = false; ++ state.scrollPending = state.scroll; ++ (_a3 = state.reprocessCurrentScroll) == null ? void 0 : _a3.call(state); ++ } ++ }, ++ delay, ++ "ignoreScrollFromMVCP" ++ ); ++ } ++ } else { ++ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; ++ requestAnimationFrame(doit); + } + } +- return true; } - +-function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { +- var _a3, _b, _c, _d; ++ ++// src/core/scrollTargetSettle.ts ++var SETTLE_POSITION_EPSILON = 0.5; ++var SETTLE_QUIET_PASSES_TO_RELEASE = 2; ++var SETTLE_TTL_MS = 500; ++function getSettleTargetOffset(ctx, settle, index, position) { ++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; ++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); ++} ++function clearScrollTargetSettle(state) { ++ state.scrollTargetSettle = void 0; ++} ++function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; +- const { +- sizes, +- averageSizes, +- props: { estimatedItemSize, getItemType }, +- scrollingTo +- } = state; +- const sizeKnown = state.sizesKnown.get(key); +- if (sizeKnown !== void 0) { +- return sizeKnown; +- } +- let size; +- const renderedSize = sizes.get(key); +- if (preferCachedSize) { +- if (renderedSize !== void 0) { +- return renderedSize; +- } +- } +- size = getKnownOrFixedSize(ctx, key, index, data, resolved); +- if (size !== void 0) { +- setSize(ctx, key, size, notifyTotalSize); +- return size; +- } +- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; +- if (useAverageSize && !scrollingTo) { +- const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; +- if (averageSizeForType !== void 0) { +- size = roundSize(averageSizeForType); +- } +- } +- if (size === void 0 && renderedSize !== void 0) { +- return renderedSize; +- } +- if (size === void 0 && useAverageSize && scrollingTo) { +- const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; +- if (averageSizeForType !== void 0) { +- size = roundSize(averageSizeForType); +- } +- } +- if (size === void 0) { +- size = estimatedItemSize + ctx.scrollAxisGap; +- } +- setSize(ctx, key, size, notifyTotalSize); +- return size; +-} +-function getItemSizeAtIndex(ctx, index) { +- if (index === void 0 || index < 0) { +- return void 0; ++ const { index, viewOffset, viewPosition } = params; ++ const { data } = state.props; ++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; ++ if (index < 0 || index >= data.length || isEndAlignedLastItem) { ++ clearScrollTargetSettle(state); ++ return; + } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++ state.scrollTargetSettle = { ++ expiresAt: Date.now() + SETTLE_TTL_MS, ++ id: getId(state, index), ++ quietPasses: 0, ++ viewOffset, ++ viewPosition ++ }; + } +- -// src/core/calculateOffsetWithOffsetPosition.ts -function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; ++function settleScrollTarget(ctx) { + var _a3; + const state = ctx.state; - const { index, viewOffset, viewPosition } = params; - let offset = offsetParam; - if (viewOffset) { @@ -2544,7 +3240,10 @@ index 40e87cd..8e6f6d0 100644 - if (startOffsetAdjustment) { - offset += startOffsetAdjustment; - } -- } ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ return false; + } - if (viewPosition !== void 0 && index !== void 0) { - const dataLength = state.props.data.length; - if (dataLength === 0) { @@ -2559,21 +3258,74 @@ index 40e87cd..8e6f6d0 100644 - if (!isOutOfBounds && index === state.props.data.length - 1) { - const footerSize = peek$(ctx, "footerSize") || 0; - offset += footerSize; -- } -- } ++ const index = state.indexByKey.get(settle.id); ++ const position = index === void 0 ? void 0 : state.positions[index]; ++ if (index === void 0 || position === void 0) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ const now = Date.now(); ++ if (now > settle.expiresAt) { ++ clearScrollTargetSettle(state); ++ return false; ++ } ++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position); ++ const diff = targetOffset - state.scroll; ++ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ settle.quietPasses++; ++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { ++ clearScrollTargetSettle(state); + } ++ return false; + } - return offset; -+// src/utils/checkThresholds.ts -+function checkThresholds(ctx, allowedEdge) { -+ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; -+ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); ++ settle.quietPasses = 0; ++ settle.expiresAt = now + SETTLE_TTL_MS; ++ if (((_a3 = state.scrollingTo) == null ? void 0 : _a3.index) === index) { ++ state.scrollingTo.offset = position; ++ state.scrollingTo.targetOffset = targetOffset; ++ } ++ requestAdjust(ctx, diff); ++ return true; } -// src/core/clampScrollOffset.ts -function clampScrollOffset(ctx, offset, scrollTarget) { -+// src/core/recalculateSettledScroll.ts -+function recalculateSettledScroll(ctx) { ++// src/core/cancelImperativeScroll.ts ++function cancelScrollCompletionChecks({ scheduledWork }) { ++ scheduledWork.cancel("checkFinishedScrollFrame"); ++ scheduledWork.cancel("checkFinishedScrollRetryFrame"); ++ scheduledWork.cancel("checkFinishedScrollFallback"); ++ scheduledWork.cancel("platformScrollCompletion"); ++} ++function settlePendingImperativeScroll(state) { + var _a3, _b; ++ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; ++ state.pendingScrollResolve = void 0; ++ state.pendingScrollToEnd = void 0; ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); ++} ++function cancelImperativeScroll(state) { ++ cancelScrollCompletionChecks(state); ++ state.scheduledWork.cancel("imperativeScrollReady"); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ clearScrollTargetSettle(state); ++ settlePendingImperativeScroll(state); ++} ++ ++// src/core/deferredPublicOnScroll.ts ++function withResolvedContentOffset(state, event, resolvedOffset) { ++ return { ++ ...event, ++ nativeEvent: { ++ ...event.nativeEvent, ++ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } ++ } ++ }; ++} ++function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { ++ var _a3, _b, _c, _d; const state = ctx.state; - const contentSize = getContentSize(ctx); - let clampedOffset = offset; @@ -2583,171 +3335,103 @@ index 40e87cd..8e6f6d0 100644 - const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; - const maxOffset = baseMaxOffset + extraEndOffset; - clampedOffset = Math.min(offset, maxOffset); -+ if ((_a3 = state.props) == null ? void 0 : _a3.data) { -+ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); ++ const deferredEvent = state.deferredPublicOnScrollEvent; ++ state.deferredPublicOnScrollEvent = void 0; ++ if (deferredEvent) { ++ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( ++ _c, ++ withResolvedContentOffset( ++ state, ++ deferredEvent, ++ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 ++ ) ++ ); } - clampedOffset = Math.max(0, clampedOffset); - return clampedOffset; -+ checkThresholds(ctx); } - // src/core/finishScrollTo.ts -@@ -1336,6 +952,124 @@ function finishScrollTo(ctx) { +-// src/core/finishScrollTo.ts +-function finishScrollTo(ctx) { +- var _a3, _b; ++// src/core/adaptiveRender.ts ++var DEFAULT_ADAPTIVE_RENDER_ENTER_VELOCITY = 3; ++var DEFAULT_ADAPTIVE_RENDER_EXIT_VELOCITY = 1; ++var DEFAULT_ADAPTIVE_RENDER_EXIT_DELAY = 250; ++var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; ++function clearAdaptiveRenderExitTimeout(ctx) { ++ ctx.state.scheduledWork.cancel("adaptiveRender"); ++} ++function scheduleAdaptiveRenderExit(ctx, exitDelay) { + const state = ctx.state; +- if (state == null ? void 0 : state.scrollingTo) { +- cancelScrollCompletionChecks(state); +- const resolvePendingScroll = state.pendingScrollResolve; +- state.pendingScrollResolve = void 0; +- const scrollingTo = state.scrollingTo; +- state.scrollHistory.length = 0; +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- if (state.pendingTotalSize !== void 0) { +- addTotalSize(ctx, null, state.pendingTotalSize); +- } +- if (PlatformAdjustBreaksScroll) { +- state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); +- } +- if (scrollingTo.isInitialScroll || state.initialScroll) { +- const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; +- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; +- finishInitialScroll(ctx, { +- onFinished: () => { +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +- }, +- preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, +- recalculateItems: true, +- schedulePreservedTargetClear: shouldPreserveResizeTarget, +- syncObservedOffset: isOffsetSession, +- waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame +- }); +- return; +- } +- recalculateSettledScroll(ctx); +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); ++ clearAdaptiveRenderExitTimeout(ctx); ++ if (exitDelay <= 0) { ++ setAdaptiveRender(ctx, "normal", "scroll"); ++ } else { ++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); } } - -+// src/core/initialScrollSession.ts -+var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -+function hasInitialScrollSessionCompletion(completion) { -+ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); -+} -+function clearInitialScrollSession(state) { -+ state.initialScrollSession = void 0; -+ return void 0; -+} -+function createInitialScrollSession(options) { -+ const { bootstrap, completion, kind, previousDataLength } = options; -+ return kind === "offset" ? { -+ completion, -+ kind, -+ previousDataLength -+ } : { -+ bootstrap, -+ completion, -+ kind, -+ previousDataLength -+ }; -+} -+function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { -+ var _a4, _b2; -+ if (!state.initialScrollSession) { -+ state.initialScrollSession = createInitialScrollSession({ -+ completion: {}, -+ kind, -+ previousDataLength: 0 -+ }); -+ } else if (state.initialScrollSession.kind !== kind) { -+ state.initialScrollSession = createInitialScrollSession({ -+ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, -+ completion: state.initialScrollSession.completion, -+ kind, -+ previousDataLength: state.initialScrollSession.previousDataLength -+ }); -+ } -+ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; -+ return state.initialScrollSession.completion; -+} -+var initialScrollCompletion = { -+ didDispatchNativeScroll(state) { -+ var _a3, _b; -+ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); -+ }, -+ didRetrySilentInitialScroll(state) { -+ var _a3, _b; -+ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); -+ }, -+ markInitialScrollNativeDispatch(state) { -+ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; -+ }, -+ markSilentInitialScrollRetry(state) { -+ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; -+ }, -+ resetFlags(state) { -+ if (!state.initialScrollSession) { -+ return; -+ } -+ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); -+ completion.didDispatchNativeScroll = void 0; -+ completion.didRetrySilentInitialScroll = void 0; -+ } -+}; -+var initialScrollWatchdog = { -+ clear(state) { -+ initialScrollWatchdog.set(state, void 0); -+ }, -+ didReachTarget(newScroll, watchdog) { -+ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); -+ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ get(state) { -+ var _a3, _b; -+ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; -+ }, -+ hasNonZeroTargetOffset(targetOffset) { -+ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ isAtZeroTargetOffset(targetOffset) { -+ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ set(state, watchdog) { -+ var _a3, _b; -+ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { -+ return; -+ } -+ const completion = ensureInitialScrollSessionCompletion(state); -+ completion.watchdog = watchdog ? { -+ startScroll: watchdog.startScroll, -+ targetOffset: watchdog.targetOffset -+ } : void 0; -+ } -+}; -+function setInitialScrollSession(state, options = {}) { -+ var _a3, _b, _c, _d; -+ const existingSession = state.initialScrollSession; -+ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; -+ const completion = existingSession == null ? void 0 : existingSession.completion; -+ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; -+ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; -+ if (!kind) { -+ return clearInitialScrollSession(state); -+ } -+ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { -+ return clearInitialScrollSession(state); -+ } -+ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; -+ state.initialScrollSession = createInitialScrollSession({ -+ bootstrap, -+ completion, -+ kind, -+ previousDataLength -+ }); -+ return state.initialScrollSession; -+} -+ - // src/core/checkFinishedScroll.ts - var INITIAL_SCROLL_MAX_FALLBACK_CHECKS = 20; - var INITIAL_SCROLL_COMPLETION_TARGET_EPSILON = 1; -@@ -1528,6 +1262,69 @@ function doScrollTo(ctx, params) { - } - } - -+// src/core/adaptiveRender.ts -+var DEFAULT_ADAPTIVE_RENDER_ENTER_VELOCITY = 3; -+var DEFAULT_ADAPTIVE_RENDER_EXIT_VELOCITY = 1; -+var DEFAULT_ADAPTIVE_RENDER_EXIT_DELAY = 250; -+var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; -+var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; -+var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; -+function clearAdaptiveRenderExitTimeout(ctx) { -+ ctx.state.scheduledWork.cancel("adaptiveRender"); -+} -+function scheduleAdaptiveRenderExit(ctx, exitDelay) { -+ const state = ctx.state; -+ clearAdaptiveRenderExitTimeout(ctx); -+ if (exitDelay <= 0) { -+ setAdaptiveRender(ctx, "normal", "scroll"); -+ } else { -+ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); -+ } -+} +- +-// src/core/checkFinishedScroll.ts +-var INITIAL_SCROLL_MAX_FALLBACK_CHECKS = 20; +-var INITIAL_SCROLL_COMPLETION_TARGET_EPSILON = 1; +-var INITIAL_SCROLL_ZERO_TARGET_EPSILON = 1; +-var SILENT_INITIAL_SCROLL_RETRY_DELAY_MS = 16; +-var SILENT_INITIAL_SCROLL_TARGET_EPSILON = 1; +-function checkFinishedScroll(ctx, options) { +- const scrollingTo = ctx.state.scrollingTo; +- if (options == null ? void 0 : options.onlyIfAligned) { +- if (!(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) || scrollingTo.animated) { +- return; +- } +- if (!getResolvedScrollCompletionState(ctx, scrollingTo).isAtResolvedTarget) { +- return; +- } +function setAdaptiveRender(ctx, mode, reason) { + var _a3, _b; + const previousMode = peek$(ctx, "adaptiveRender"); + if (previousMode !== mode) { + set$(ctx, "adaptiveRender", mode); + (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); -+ } -+} + } +- ctx.state.scheduledWork.frame(() => checkFinishedScrollFrame(ctx), "checkFinishedScrollFrame"); + } +-function hasScrollCompletionOwnership(state, options) { +- const { clampedTargetOffset, scrollingTo } = options; +- return !scrollingTo.isInitialScroll || state.hasScrolled || clampedTargetOffset <= INITIAL_SCROLL_COMPLETION_TARGET_EPSILON; +function resetAdaptiveRender(ctx) { + var _a3, _b; + clearAdaptiveRenderExitTimeout(ctx); @@ -2755,7 +3439,9 @@ index 40e87cd..8e6f6d0 100644 + if (peek$(ctx, "adaptiveRender") !== mode) { + setAdaptiveRender(ctx, mode, "initial"); + } -+} + } +-function isSilentInitialDispatch(state, scrollingTo) { +- return !!(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) && initialScrollCompletion.didDispatchNativeScroll(state) && !state.hasScrolled; +function updateAdaptiveRender(ctx, scrollVelocity, options) { + var _a3, _b, _c; + const state = ctx.state; @@ -2782,310 +3468,81 @@ index 40e87cd..8e6f6d0 100644 + resetAdaptiveRender(ctx); + } + } -+} -+ - // src/core/doMaintainScrollAtEnd.ts - function doMaintainScrollAtEnd(ctx) { - const state = ctx.state; -@@ -1554,9 +1351,12 @@ function doMaintainScrollAtEnd(ctx) { - const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; - const scrollAtRequest = state.scroll; - state.maintainingScrollAtEnd = pendingState; -+ clearScrollTargetSettle(state); - requestAnimationFrame(() => { - const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); -- const didScrollSinceRequest = state.scroll !== scrollAtRequest; -+ const lastIssued = state.lastIssuedScrollOffset; -+ const isSettlingOntoIssuedScroll = lastIssued !== void 0 && Math.abs(state.scroll - lastIssued) <= EDGE_POSITION_EPSILON; -+ const didScrollSinceRequest = state.scroll !== scrollAtRequest && !isSettlingOntoIssuedScroll; - if (isStillWithinThreshold || !didScrollSinceRequest) { - state.maintainingScrollAtEnd = activeState; - const scroller = refScroller.current; -@@ -1951,11 +1751,32 @@ function prepareMVCP(ctx, dataChanged) { - } } - --// src/platform/flushSync.native.ts --var flushSync = (fn) => { -- fn(); --}; -- -+// src/platform/flushSync.native.ts -+var flushSync = (fn) => { -+ fn(); -+}; +-function getInitialScrollWatchdogTargetOffset(state) { + +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { -+ var _a3; + var _a3; +- return (_a3 = initialScrollWatchdog.get(state)) == null ? void 0 : _a3.targetOffset; +-} +-function isNativeInitialNonZeroTarget(state) { +- const targetOffset = getInitialScrollWatchdogTargetOffset(state); +- return !state.didFinishInitialScroll && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); + const drawDistance = ctx.state.props.drawDistance; + const initialScroll = ctx.state.initialScroll; + const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; + const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; + return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; -+} + } +-function shouldFinishInitialScrollWithoutNativeProgress(state, scrollingTo) { +- var _a3, _b; +- if (!scrollingTo.isInitialScroll || scrollingTo.animated || !state.didContainersLayout) { +- return false; +- } +- if (((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap") { +- return false; +function scheduleFullDrawDistancePrewarm(ctx) { + const { state } = ctx; + if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { + return; -+ } + } +- const targetOffset = (_b = scrollingTo.targetOffset) != null ? _b : scrollingTo.offset; +- if (initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset) && initialScrollCompletion.didDispatchNativeScroll(state) && !state.hasScrolled) { +- return false; + state.scheduledWork.frame(() => { + var _a3; + return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); + }, "fullDrawDistancePrewarm"); +} + - // src/utils/getScrollVelocity.ts - var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; - var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2203,7 +2024,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { - } - } - function scrollTo(ctx, params) { -- var _a3, _b; -+ var _a3, _b, _c; - const state = ctx.state; - const { noScrollingTo, forceScroll, ...scrollTarget } = params; - const { -@@ -2225,6 +2046,7 @@ function scrollTo(ctx, params) { - if (!noScrollingTo) { - if (isInitialScroll) { - initialScrollCompletion.resetFlags(state); -+ clearScrollTargetSettle(state); - } - const averageSizeSnapshot = getAverageSizeSnapshot(state); - state.scrollingTo = { -@@ -2235,6 +2057,15 @@ function scrollTo(ctx, params) { - }; - if (!isInitialScroll) { - pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); -+ if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { -+ beginScrollTargetSettle(ctx, { -+ index: scrollTarget.index, -+ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, -+ viewPosition: scrollTarget.viewPosition -+ }); -+ } else { -+ clearScrollTargetSettle(state); -+ } - } ++// src/utils/setInitialRenderState.ts ++function resetInitialRenderState(ctx, { ++ resetLayout, ++ resetInitialScroll ++}) { ++ const { state } = ctx; ++ if (resetLayout) { ++ state.didContainersLayout = false; ++ state.queuedInitialLayout = false; } - state.scrollPending = targetOffset; -@@ -2242,7 +2073,7 @@ function scrollTo(ctx, params) { - if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { - if (animated) { - if (state.scrollTargetPinnedRange) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); -+ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); - } - } else { - updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2255,6 +2086,310 @@ function scrollTo(ctx, params) { +- if (initialScrollWatchdog.isAtZeroTargetOffset(targetOffset) || Math.abs(state.scroll - targetOffset) > 1 || Math.abs(state.scrollPending - targetOffset) > 1) { +- return false; ++ if (resetInitialScroll) { ++ state.didFinishInitialScroll = false; } +- return !!scrollingTo.waitForInitialScrollCompletionFrame || isNativeInitialNonZeroTarget(state); ++ set$(ctx, "readyToRender", false); ++ resetAdaptiveRender(ctx); } - -+// src/core/scrollTargetSettle.ts -+var SETTLE_POSITION_EPSILON = 1; -+var SETTLE_QUIET_PASSES_TO_RELEASE = 2; -+var SETTLE_MAX_MS = 1e3; -+var SETTLE_MAX_CORRECTIONS = 8; -+function clearScrollTargetSettle(state) { -+ state.scrollTargetSettle = void 0; -+ state.scheduledWork.cancel("scrollTargetSettle"); -+ state.scheduledWork.cancel("scrollTargetSettleDeadline"); -+} -+function beginScrollTargetSettle(ctx, params) { -+ const state = ctx.state; -+ const { index, viewOffset, viewPosition } = params; -+ const { alignItemsAtEnd, data, maintainScrollAtEnd } = state.props; -+ const hasEndAnchoring = !!maintainScrollAtEnd || !!alignItemsAtEnd; -+ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; -+ if (index < 0 || index >= data.length || isEndAlignedLastItem && hasEndAnchoring) { -+ clearScrollTargetSettle(state); -+ return; -+ } -+ clearScrollTargetSettle(state); -+ const now = Date.now(); -+ state.scrollTargetSettle = { -+ corrections: 0, -+ deadline: now + SETTLE_MAX_MS, -+ id: getId(state, index), -+ measuredIndex: void 0, -+ quietPasses: 0, -+ viewOffset, -+ viewPosition -+ }; -+ state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline"); -+} -+function getSettleTargetOffset(ctx, settle, index, position) { -+ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; -+ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); -+} -+function applyScrollTargetCorrection(ctx, id) { -+ var _a3; -+ const state = ctx.state; -+ const settle = state.scrollTargetSettle; -+ if (!settle || settle.id !== id) { -+ return; -+ } -+ const index = state.indexByKey.get(id); -+ const position = index === void 0 ? void 0 : state.positions[index]; -+ if (index === void 0 || position === void 0 || Date.now() > settle.deadline) { -+ clearScrollTargetSettle(state); -+ return; -+ } -+ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { -+ clearScrollTargetSettle(state); -+ return; +-function shouldFinishInitialZeroTargetScroll(ctx) { +- var _a3; ++function setInitialRenderState(ctx, { ++ didLayout, ++ didInitialScroll ++}) { + const { state } = ctx; +- return !!((_a3 = state.scrollingTo) == null ? void 0 : _a3.isInitialScroll) && state.props.data.length > 0 && getContentSize(ctx) <= state.scrollLength && state.scrollPending <= INITIAL_SCROLL_ZERO_TARGET_EPSILON; ++ const { ++ loadStartTime, ++ props: { onLoad } ++ } = state; ++ if (didLayout) { ++ state.didContainersLayout = true; + } -+ settle.corrections++; -+ settle.measuredIndex = void 0; -+ scrollTo(ctx, { -+ animated: false, -+ index, -+ itemSize: getItemSizeAtIndex(ctx, index), -+ // Correcting where a scroll already landed is not a new imperative scroll: claiming the -+ // session would suppress the list's own handling of subsequent scroll events and re-arm -+ // this settle from inside its own correction. -+ noScrollingTo: true, -+ offset: position, -+ viewOffset: settle.viewOffset, -+ viewPosition: settle.viewPosition -+ }); -+ const scrollingTo = state.scrollingTo; -+ if (scrollingTo) { -+ scrollingTo.targetOffset = state.scrollPending; -+ scrollingTo.offset = position; -+ } -+ (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); -+} -+function settleScrollTarget(ctx, options) { -+ var _a3; -+ const state = ctx.state; -+ const settle = state.scrollTargetSettle; -+ if (!settle) { -+ return false; -+ } -+ const measured = options == null ? void 0 : options.minIndexSizeChanged; -+ if (measured !== void 0) { -+ settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); -+ } -+ if (options == null ? void 0 : options.isCompensating) { -+ if (Date.now() > settle.deadline) { -+ clearScrollTargetSettle(state); -+ return false; -+ } -+ settle.quietPasses = 0; -+ return false; -+ } -+ const index = state.indexByKey.get(settle.id); -+ const position = index === void 0 ? void 0 : state.positions[index]; -+ if (index === void 0 || position === void 0) { -+ clearScrollTargetSettle(state); -+ return false; -+ } -+ if (Date.now() > settle.deadline) { -+ clearScrollTargetSettle(state); -+ return false; -+ } -+ if (settle.measuredIndex === void 0 || settle.measuredIndex > index) { -+ return false; -+ } -+ const targetOffset = getSettleTargetOffset(ctx, settle, index, position); -+ if (Math.abs(targetOffset - state.scroll) <= SETTLE_POSITION_EPSILON) { -+ settle.measuredIndex = void 0; -+ settle.quietPasses++; -+ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { -+ clearScrollTargetSettle(state); -+ } -+ return false; -+ } -+ settle.quietPasses = 0; -+ state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); -+ return true; -+} -+ -+// src/core/cancelImperativeScroll.ts -+function cancelScrollCompletionChecks({ scheduledWork }) { -+ scheduledWork.cancel("checkFinishedScrollFrame"); -+ scheduledWork.cancel("checkFinishedScrollRetryFrame"); -+ scheduledWork.cancel("checkFinishedScrollFallback"); -+ scheduledWork.cancel("platformScrollCompletion"); -+} -+function settlePendingImperativeScroll(state) { -+ var _a3, _b; -+ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; -+ state.pendingScrollResolve = void 0; -+ state.pendingScrollToEnd = void 0; -+ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); -+} -+function cancelImperativeScroll(state) { -+ cancelScrollCompletionChecks(state); -+ state.scheduledWork.cancel("imperativeScrollReady"); -+ state.scrollingTo = void 0; -+ state.scrollTargetPinnedRange = void 0; -+ clearScrollTargetSettle(state); -+ settlePendingImperativeScroll(state); -+} -+ -+// src/core/deferredPublicOnScroll.ts -+function withResolvedContentOffset(state, event, resolvedOffset) { -+ return { -+ ...event, -+ nativeEvent: { -+ ...event.nativeEvent, -+ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -+ } -+ }; -+} -+function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { -+ var _a3, _b, _c, _d; -+ const state = ctx.state; -+ const deferredEvent = state.deferredPublicOnScrollEvent; -+ state.deferredPublicOnScrollEvent = void 0; -+ if (deferredEvent) { -+ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( -+ _c, -+ withResolvedContentOffset( -+ state, -+ deferredEvent, -+ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 -+ ) -+ ); -+ } -+} -+ -+// src/utils/setInitialRenderState.ts -+function resetInitialRenderState(ctx, { -+ resetLayout, -+ resetInitialScroll -+}) { -+ const { state } = ctx; -+ if (resetLayout) { -+ state.didContainersLayout = false; -+ state.queuedInitialLayout = false; -+ } -+ if (resetInitialScroll) { -+ state.didFinishInitialScroll = false; -+ } -+ set$(ctx, "readyToRender", false); -+ resetAdaptiveRender(ctx); -+} -+function setInitialRenderState(ctx, { -+ didLayout, -+ didInitialScroll -+}) { -+ const { state } = ctx; -+ const { -+ loadStartTime, -+ props: { onLoad } -+ } = state; -+ if (didLayout) { -+ state.didContainersLayout = true; -+ } -+ if (didInitialScroll) { -+ state.didFinishInitialScroll = true; ++ if (didInitialScroll) { ++ state.didFinishInitialScroll = true; + } + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); + if (isReadyToRender && !peek$(ctx, "readyToRender")) { @@ -3101,7 +3558,9 @@ index 40e87cd..8e6f6d0 100644 + } + } + } -+} + } +-function isEndAlignedLastItemTarget(ctx, scrollingTo) { +- return scrollingTo.index === ctx.state.props.data.length - 1 && scrollingTo.viewPosition === 1; + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -3109,16 +3568,48 @@ index 40e87cd..8e6f6d0 100644 + state.scroll = offset; + state.scrollPending = offset; + state.scrollPrev = offset; -+} + } +-function getCurrentTargetOffset(ctx, scrollingTo) { +- var _a3; +- const index = scrollingTo.index; +- const shouldRecomputeEndTarget = isEndAlignedLastItemTarget(ctx, scrollingTo); +- const requestedTargetOffset = shouldRecomputeEndTarget && index !== void 0 ? calculateOffsetWithOffsetPosition(ctx, calculateOffsetForIndex(ctx, index), scrollingTo) : (_a3 = scrollingTo.targetOffset) != null ? _a3 : clampScrollOffset(ctx, scrollingTo.offset - (scrollingTo.viewOffset || 0), scrollingTo); +- return clampScrollOffset(ctx, requestedTargetOffset, scrollingTo); +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); -+} + } +-function getResolvedScrollCompletionState(ctx, scrollingTo) { +- const { state } = ctx; +- const scroll = state.scrollPending; +- const adjust = state.scrollAdjustHandler.getAdjust(); +- const clampedTargetOffset = getCurrentTargetOffset(ctx, scrollingTo); +- const maxOffset = clampScrollOffset(ctx, scroll, scrollingTo); +- const diff1 = Math.abs(scroll - clampedTargetOffset); +- const adjustedTargetOffset = clampedTargetOffset + adjust; +- const diff2 = Math.abs(scroll - adjustedTargetOffset); +- const canUseAdjustedCompletion = !scrollingTo.animated || Platform.OS === "ios"; +- return { +- clampedTargetOffset, +- isAtResolvedTarget: Math.abs(scroll - maxOffset) < 1 && (diff1 < 1 || canUseAdjustedCompletion && diff2 < 1) +- }; +function clearPreservedInitialScrollTarget(state) { + clearPreservedInitialScrollTargetTimeout(state); + state.clearPreservedInitialScrollOnNextFinish = void 0; + state.initialScroll = void 0; + setInitialScrollSession(state); -+} + } +-function checkFinishedScrollFrame(ctx) { +- const scrollingTo = ctx.state.scrollingTo; +- if (!scrollingTo) { +- return; +- } +- const { state } = ctx; +- const completionState = getResolvedScrollCompletionState(ctx, scrollingTo); +- if (completionState.isAtResolvedTarget && hasScrollCompletionOwnership(state, { +- clampedTargetOffset: completionState.clampedTargetOffset, +- scrollingTo +- })) { +- finishScrollTo(ctx); +function supersedeInitialScroll(ctx) { + var _a3, _b, _c; + const state = ctx.state; @@ -3135,11 +3626,22 @@ index 40e87cd..8e6f6d0 100644 + initialScrollCompletion.resetFlags(state); + setInitialScrollSession(state, { bootstrap: null }); + finishInitialScroll(ctx); -+ } -+} + } + } +-function scrollToFallbackOffset(ctx, offset) { +- var _a3; +- (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({ +- animated: false, +- x: ctx.state.props.horizontal ? offset : 0, +- y: ctx.state.props.horizontal ? 0 : offset +- }); +-} +-function checkFinishedScrollFallback(ctx) { +function finishInitialScroll(ctx, options) { + var _a3, _b, _c; -+ const state = ctx.state; + const state = ctx.state; +- if (state.scheduledWork.has("checkFinishedScrollFallback")) { +- return; + if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { + syncInitialScrollOffset(state, options.resolvedOffset); + } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { @@ -3147,7 +3649,43 @@ index 40e87cd..8e6f6d0 100644 + if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { + syncInitialScrollOffset(state, observedOffset); + } -+ } + } +- const scrollingTo = state.scrollingTo; +- const shouldFinishInitialZeroTarget = shouldFinishInitialZeroTargetScroll(ctx); +- const silentInitialDispatch = isSilentInitialDispatch(state, scrollingTo); +- const canFinishInitialWithoutNativeProgress = scrollingTo !== void 0 ? shouldFinishInitialScrollWithoutNativeProgress(state, scrollingTo) : false; +- const slowTimeout = (scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) && !shouldFinishInitialZeroTarget && !canFinishInitialWithoutNativeProgress || !state.didContainersLayout; +- const initialDelay = shouldFinishInitialZeroTarget || canFinishInitialWithoutNativeProgress ? 0 : silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : slowTimeout ? 500 : 100; +- let numChecks = 0; +- const scheduleFallbackCheck = (delay) => { +- state.scheduledWork.timeout(checkHasScrolled, delay, "checkFinishedScrollFallback"); +- }; +- const checkHasScrolled = () => { +- var _a3, _b, _c, _d; +- const isStillScrollingTo = state.scrollingTo; +- if (isStillScrollingTo) { +- numChecks++; +- const isNativeInitialPending = isNativeInitialNonZeroTarget(state) && !state.hasScrolled; +- const maxChecks = silentInitialDispatch ? 5 : isNativeInitialPending ? INITIAL_SCROLL_MAX_FALLBACK_CHECKS : 5; +- const shouldFinishZeroTarget = shouldFinishInitialZeroTargetScroll(ctx); +- const canFinishInitialScrollWithoutNativeProgress = shouldFinishInitialScrollWithoutNativeProgress( +- state, +- isStillScrollingTo +- ); +- const completionState = getResolvedScrollCompletionState(ctx, isStillScrollingTo); +- const canFinishAfterSilentNativeDispatch = Platform.OS === "android" && silentInitialDispatch && completionState.isAtResolvedTarget && numChecks >= 1; +- const shouldRetrySilentInitialNativeScroll = Platform.OS === "android" && canFinishAfterSilentNativeDispatch && !initialScrollCompletion.didRetrySilentInitialScroll(state); +- const shouldFinishAfterObservedScroll = state.hasScrolled && (!isStillScrollingTo.isInitialScroll || completionState.isAtResolvedTarget); +- const shouldRetryUnalignedInitialScroll = isStillScrollingTo.isInitialScroll && !completionState.isAtResolvedTarget && numChecks <= maxChecks; +- const shouldRetryUnalignedEndScroll = Platform.OS === "ios" && !isStillScrollingTo.isInitialScroll && isEndAlignedLastItemTarget(ctx, isStillScrollingTo) && !completionState.isAtResolvedTarget && numChecks <= maxChecks; +- if (shouldRetrySilentInitialNativeScroll) { +- const targetOffset = (_b = (_a3 = getInitialScrollWatchdogTargetOffset(state)) != null ? _a3 : isStillScrollingTo.targetOffset) != null ? _b : 0; +- const jiggleOffset = targetOffset >= SILENT_INITIAL_SCROLL_TARGET_EPSILON ? targetOffset - SILENT_INITIAL_SCROLL_TARGET_EPSILON : targetOffset + SILENT_INITIAL_SCROLL_TARGET_EPSILON; +- initialScrollCompletion.markSilentInitialScrollRetry(state); +- scrollToFallbackOffset(ctx, jiggleOffset); +- state.scheduledWork.frame( +- () => scrollToFallbackOffset(ctx, targetOffset), +- "checkFinishedScrollRetryFrame" + const complete = () => { + var _a4, _b2, _c2, _d, _e; + const shouldReleaseDeferredPublicOnScroll = Platform.OS === "web" && ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; @@ -3168,124 +3706,179 @@ index 40e87cd..8e6f6d0 100644 + }, + PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, + "preservedInitialScroll" -+ ); -+ } + ); +- scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); +- } else if (shouldRetryUnalignedEndScroll) { +- scrollToFallbackOffset(ctx, completionState.clampedTargetOffset); +- scheduleFallbackCheck(100); +- } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) { +- finishScrollTo(ctx); +- } else if ((isNativeInitialPending || shouldRetryUnalignedInitialScroll) && numChecks <= maxChecks) { +- const targetOffset = (_d = (_c = getInitialScrollWatchdogTargetOffset(state)) != null ? _c : isStillScrollingTo.targetOffset) != null ? _d : state.scrollPending; +- scrollToFallbackOffset(ctx, targetOffset); +- scheduleFallbackCheck(silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : 100); +- } else { +- scheduleFallbackCheck(silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : 100); + } + } else { + clearPreservedInitialScrollTarget(state); + } + if (options == null ? void 0 : options.recalculateItems) { + recalculateSettledScroll(ctx); -+ } + } + setInitialRenderState(ctx, { didInitialScroll: true }); + if (shouldReleaseDeferredPublicOnScroll) { + releaseDeferredPublicOnScroll(ctx, finalScrollOffset); + } + (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); -+ }; + }; +- scheduleFallbackCheck(initialDelay); +-} +- +-// src/core/doScrollTo.native.ts +-function doScrollTo(ctx, params) { +- const state = ctx.state; +- const { animated, horizontal, isInitialScroll, offset } = params; +- const isAnimated = !!animated; +- const { refScroller } = state; +- const scroller = refScroller.current; +- if (!scroller) { + if (options == null ? void 0 : options.waitForCompletionFrame) { + requestAnimationFrame(complete); -+ return; -+ } + return; + } +- const isHorizontal = !!horizontal; +- const contentSize = isHorizontal ? getContentSize(ctx) : void 0; +- const nativeOffset = toNativeHorizontalOffset(state, offset, contentSize); +- scroller.scrollTo({ +- animated: isAnimated, +- x: isHorizontal ? nativeOffset : 0, +- y: isHorizontal ? 0 : offset +- }); +- if (isInitialScroll) { +- initialScrollCompletion.markInitialScrollNativeDispatch(state); +- } +- if (isAnimated && Math.abs(state.scroll - offset) <= 1) { +- checkFinishedScroll(ctx); +- } +- if (!isAnimated) { +- state.scroll = offset; +- checkFinishedScrollFallback(ctx); +- } + complete(); -+} -+ - // src/core/scrollToIndex.ts - function clampScrollIndex(index, dataLength) { - if (dataLength <= 0) { -@@ -4299,6 +4434,7 @@ function calculateItemsInView(ctx, params = {}) { - startIndex - }); - totalSize = getContentSize(ctx); -+ const minIndexSizeChangedThisPass = minIndexSizeChanged; - if (minIndexSizeChanged !== void 0) { - state.minIndexSizeChanged = void 0; + } + + // src/core/scrollRequestTracker.ts +@@ -1591,60 +1711,6 @@ function getScrollRequestTracker(ctx) { + return ctx.scrollRequestTracker; + } + +-// src/utils/requestAdjust.ts +-function requestAdjust(ctx, positionDiff, source) { +- const state = ctx.state; +- if (Math.abs(positionDiff) > 0.1) { +- const dataChanged = source === "data"; +- const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff; +- const doit = () => { +- if (needsScrollWorkaround) { +- doScrollTo(ctx, { horizontal: state.props.horizontal, offset: state.scroll }); +- } else { +- state.scrollAdjustHandler.requestAdjust(positionDiff); +- if (state.adjustingFromInitialMount) { +- state.adjustingFromInitialMount--; +- } +- } +- }; +- state.scroll += positionDiff; +- state.scrollForNextCalculateItemsInView = void 0; +- const readyToRender = peek$(ctx, "readyToRender"); +- if (readyToRender) { +- doit(); +- if (Platform.OS !== "web" && source !== "item-size") { +- const threshold = state.scroll - positionDiff / 2; +- if (!state.ignoreScrollFromMVCP) { +- state.ignoreScrollFromMVCP = {}; +- } +- if (positionDiff > 0) { +- state.ignoreScrollFromMVCP.lt = threshold; +- } else { +- state.ignoreScrollFromMVCP.gt = threshold; +- } +- const delay = needsScrollWorkaround ? 250 : 100; +- state.scheduledWork.timeout( +- () => { +- var _a3; +- state.ignoreScrollFromMVCP = void 0; +- const shouldForceUpdate = state.ignoreScrollFromMVCPIgnored && state.scrollProcessingEnabled !== false; +- if (shouldForceUpdate) { +- state.ignoreScrollFromMVCPIgnored = false; +- state.scrollPending = state.scroll; +- (_a3 = state.reprocessCurrentScroll) == null ? void 0 : _a3.call(state); +- } +- }, +- delay, +- "ignoreScrollFromMVCP" +- ); +- } +- } else { +- state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; +- requestAnimationFrame(doit); +- } +- } +-} +- + // src/core/doMaintainScrollAtEnd.ts + function finishMaintainScrollAtEnd(ctx) { + var _a3; +@@ -2281,7 +2347,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + } + } + function scrollTo(ctx, params) { +- var _a3, _b; ++ var _a3, _b, _c; + const state = ctx.state; + const { noScrollingTo, forceScroll, ...scrollTarget } = params; + const { +@@ -2313,6 +2379,15 @@ function scrollTo(ctx, params) { + }; + if (!isInitialScroll) { + pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); ++ if (scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { ++ beginScrollTargetSettle(ctx, { ++ index: scrollTarget.index, ++ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, ++ viewPosition: scrollTarget.viewPosition ++ }); ++ } else { ++ clearScrollTargetSettle(state); ++ } } -@@ -4316,8 +4452,18 @@ function calculateItemsInView(ctx, params = {}) { + } + state.scrollPending = targetOffset; +@@ -2320,7 +2395,7 @@ function scrollTo(ctx, params) { + if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { + if (animated) { + if (state.scrollTargetPinnedRange) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); + } + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); +@@ -4394,7 +4469,8 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); - const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); -- if (didMVCPAdjustScroll) { -+ const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; -+ const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); -+ const mvcp = state.props.maintainVisibleContentPosition; -+ const isUnanchoredDataChange = dataChanged && !mvcp.data; -+ const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; -+ if (!suppressInitialScrollSideEffects) { -+ settleScrollTarget(ctx, { -+ isCompensating, -+ minIndexSizeChanged: isUnanchoredDataChange ? 0 : minIndexSizeChangedThisPass -+ }); -+ } -+ if (didMVCPAdjust) { ++ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx); ++ const didMVCPAdjustScroll = didSettleScrollTarget || !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); + if (didMVCPAdjustScroll) { updateScroll2(state.scroll); updateScrollRange(); - } -@@ -5810,6 +5956,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize - return Platform.OS === "web" && props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; - } - function setHeaderSize(ctx, size) { -+ var _a3; - const { state } = ctx; - const previousHeaderSize = peek$(ctx, "headerSize") || 0; - const didChange = previousHeaderSize !== size; -@@ -5819,6 +5966,8 @@ function setHeaderSize(ctx, size) { - updateContentMetricsState(ctx); - if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { - requestAdjust(ctx, size - previousHeaderSize); -+ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) { -+ doMaintainScrollAtEnd(ctx); - } - } - state.didMeasureHeader = true; -@@ -6905,13 +7054,14 @@ function getRenderedItem(ctx, key) { - - // src/utils/normalizeMaintainScrollAtEnd.ts - function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) { -- var _a3, _b, _c, _d; -+ var _a3, _b, _c, _d, _e; - return { - animated: false, - onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true, - onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true, -- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true, -- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true -+ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true, -+ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true, -+ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true - }; - } - function normalizeMaintainScrollAtEnd(value) { -@@ -7631,6 +7781,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded - onScrollBeginDrag: (event) => { - var _a4, _b2; - prepareReachedEdgeForNextUserScroll(ctx); -+ clearScrollTargetSettle(state); - (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); - }, - onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) -diff --git a/node_modules/@legendapp/list/react-native.web.d.ts b/node_modules/@legendapp/list/react-native.web.d.ts -index b6c7481..ace847e 100644 ---- a/node_modules/@legendapp/list/react-native.web.d.ts -+++ b/node_modules/@legendapp/list/react-native.web.d.ts -@@ -511,6 +511,12 @@ interface AlwaysRenderConfig { - interface MaintainScrollAtEndOnOptions { - dataChange?: boolean; - footerLayout?: boolean; -+ /** -+ * Keep the list pinned to the end when the header changes size. A header that measures larger -+ * than its estimate pushes every item down, which leaves a list that had already scrolled to -+ * its end short of it by the difference. -+ */ -+ headerLayout?: boolean; - itemLayout?: boolean; - layout?: boolean; - } diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js -index 914d2da..ccb19f4 100644 +index b2240b5..69282b3 100644 --- a/node_modules/@legendapp/list/react-native.web.js +++ b/node_modules/@legendapp/list/react-native.web.js -@@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; +@@ -430,171 +430,271 @@ var EDGE_POSITION_EPSILON = 1; var ENABLE_DEVMODE = IS_DEV && false; var ENABLE_DEBUG_VIEW = IS_DEV && false; @@ -3302,15 +3895,31 @@ index 914d2da..ccb19f4 100644 - state.pendingScrollResolve = void 0; - state.pendingScrollToEnd = void 0; - resolvePendingScroll == null ? void 0 : resolvePendingScroll(); --} ++// src/core/getStartOffsetAdjustment.ts ++function getStartOffsetAdjustment(ctx) { ++ const { state } = ctx; ++ const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; ++ return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); + } -function cancelImperativeScroll(state) { - cancelScrollCompletionChecks(state); - state.scheduledWork.cancel("imperativeScrollReady"); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; - settlePendingImperativeScroll(state); --} -- ++ ++// src/utils/getId.ts ++function getId(state, index) { ++ const { data, keyExtractor } = state.props; ++ if (!data) { ++ return ""; ++ } ++ const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; ++ const id = ret; ++ state.idCache[index] = id; ++ return id; + } + -// src/core/deferredPublicOnScroll.ts -function withResolvedContentOffset(state, event, resolvedOffset) { - return { @@ -3318,12 +3927,43 @@ index 914d2da..ccb19f4 100644 - nativeEvent: { - ...event.nativeEvent, - contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -- } ++// src/core/updateContentMetricsState.ts ++function getRawContentLength(ctx) { ++ var _a3, _b, _c; ++ const { state, values } = ctx; ++ return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); ++} ++function getAlignItemsAtEndPadding(ctx) { ++ const { state } = ctx; ++ const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; ++ return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; ++} ++function updateContentMetricsState(ctx) { ++ var _a3; ++ const { state } = ctx; ++ const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; ++ const nextPadding = getAlignItemsAtEndPadding(ctx); ++ if (previousPadding !== nextPadding) { ++ const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated"; ++ const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold")); ++ if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) { ++ state.scheduledWork.microtask(() => { ++ if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) { ++ set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx)); ++ } ++ }, "alignItemsAtEndPaddingFallback"); ++ } else if (!isMaintainExpected) { ++ set$(ctx, "alignItemsAtEndPadding", nextPadding); + } - }; --} ++ } + } -function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { - var _a3, _b, _c, _d; -- const state = ctx.state; ++ ++// src/core/addTotalSize.ts ++function addTotalSize(ctx, key, add, notifyTotalSize = true) { + const state = ctx.state; - const deferredEvent = state.deferredPublicOnScrollEvent; - state.deferredPublicOnScrollEvent = void 0; - if (deferredEvent) { @@ -3335,18 +3975,54 @@ index 914d2da..ccb19f4 100644 - (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 - ) - ); -- } --} -- ++ const prevTotalSize = state.totalSize; ++ let totalSize = state.totalSize; ++ if (key === null) { ++ totalSize = add; ++ if (state.timeoutSetPaddingTop) { ++ clearTimeout(state.timeoutSetPaddingTop); ++ state.timeoutSetPaddingTop = void 0; ++ } ++ } else { ++ totalSize += add; ++ } ++ if (prevTotalSize !== totalSize) { ++ { ++ state.pendingTotalSize = void 0; ++ state.totalSize = totalSize; ++ if (notifyTotalSize) { ++ set$(ctx, "totalSize", totalSize); ++ } ++ updateContentMetricsState(ctx); ++ } ++ } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { ++ set$(ctx, "totalSize", totalSize); + } + } + -// src/core/initialScrollSession.ts -var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -function hasInitialScrollSessionCompletion(completion) { - return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); --} ++// src/core/setSize.ts ++function setSize(ctx, itemKey, size, notifyTotalSize = true) { ++ const state = ctx.state; ++ const { sizes } = state; ++ const previousSize = sizes.get(itemKey); ++ const diff = previousSize !== void 0 ? size - previousSize : size; ++ if (diff !== 0) { ++ addTotalSize(ctx, itemKey, diff, notifyTotalSize); ++ } ++ sizes.set(itemKey, size); + } -function clearInitialScrollSession(state) { - state.initialScrollSession = void 0; - return void 0; --} ++ ++// src/utils/helpers.ts ++function isFunction(obj) { ++ return typeof obj === "function"; + } -function createInitialScrollSession(options) { - const { bootstrap, completion, kind, previousDataLength } = options; - return kind === "offset" ? { @@ -3359,7 +4035,9 @@ index 914d2da..ccb19f4 100644 - kind, - previousDataLength - }; --} ++function isArray(obj) { ++ return Array.isArray(obj); + } -function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { - var _a4, _b2; - if (!state.initialScrollSession) { @@ -3375,10 +4053,15 @@ index 914d2da..ccb19f4 100644 - kind, - previousDataLength: state.initialScrollSession.previousDataLength - }); -- } ++var warned = /* @__PURE__ */ new Set(); ++function warnDevOnce(id, text) { ++ if (IS_DEV && !warned.has(id)) { ++ warned.add(id); ++ console.warn(`[legend-list] ${text}`); + } - (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; - return state.initialScrollSession.completion; --} + } -var initialScrollCompletion = { - didDispatchNativeScroll(state) { - var _a3, _b; @@ -3397,11 +4080,36 @@ index 914d2da..ccb19f4 100644 - resetFlags(state) { - if (!state.initialScrollSession) { - return; -- } ++function roundSize(size) { ++ return Math.floor(size * 8) / 8; ++} ++function isNullOrUndefined(value) { ++ return value === null || value === void 0; ++} ++function getPadding(s, type) { ++ var _a3, _b, _c; ++ const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; ++ return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; ++} ++function extractPadding(style, contentContainerStyle, type) { ++ return getPadding(style, type) + getPadding(contentContainerStyle, type); ++} ++function findContainerId(ctx, key) { ++ var _a3, _b; ++ const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); ++ if (directMatch !== void 0) { ++ return directMatch; ++ } ++ const numContainers = peek$(ctx, "numContainers"); ++ for (let i = 0; i < numContainers; i++) { ++ const itemKey = peek$(ctx, `containerItemKey${i}`); ++ if (itemKey === key) { ++ return i; + } - const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); - completion.didDispatchNativeScroll = void 0; - completion.didRetrySilentInitialScroll = void 0; -- } + } -}; -var initialScrollWatchdog = { - clear(state) { @@ -3425,16 +4133,46 @@ index 914d2da..ccb19f4 100644 - var _a3, _b; - if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { - return; -- } ++ return -1; ++} ++ ++// src/utils/getItemSize.ts ++function getKnownOrFixedSize(ctx, key, index, data, resolved) { ++ var _a3, _b; ++ const state = ctx.state; ++ const { getFixedItemSize, getItemType } = state.props; ++ let size = key ? state.sizesKnown.get(key) : void 0; ++ if (size === void 0 && key && getFixedItemSize) { ++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; ++ const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); ++ if (fixedSize !== void 0) { ++ size = fixedSize + ctx.scrollAxisGap; ++ state.sizesKnown.set(key, size); + } - const completion = ensureInitialScrollSessionCompletion(state); - completion.watchdog = watchdog ? { - startScroll: watchdog.startScroll, - targetOffset: watchdog.targetOffset - } : void 0; -- } + } -}; -function setInitialScrollSession(state, options = {}) { -- var _a3, _b, _c, _d; ++ return size; ++} ++function getKnownOrFixedItemSize(ctx, index) { ++ const key = getId(ctx.state, index); ++ return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); ++} ++function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { ++ for (let index = startIndex; index <= endIndex; index++) { ++ if (getKnownOrFixedItemSize(ctx, index) === void 0) { ++ return false; ++ } ++ } ++ return true; ++} ++function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { + var _a3, _b, _c, _d; - const existingSession = state.initialScrollSession; - const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; - const completion = existingSession == null ? void 0 : existingSession.completion; @@ -3442,10 +4180,26 @@ index 914d2da..ccb19f4 100644 - const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; - if (!kind) { - return clearInitialScrollSession(state); -- } ++ const state = ctx.state; ++ const { ++ sizes, ++ averageSizes, ++ props: { estimatedItemSize, getItemType }, ++ scrollingTo ++ } = state; ++ const sizeKnown = state.sizesKnown.get(key); ++ if (sizeKnown !== void 0) { ++ return sizeKnown; + } - if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { - return clearInitialScrollSession(state); -- } ++ let size; ++ const renderedSize = sizes.get(key); ++ if (preferCachedSize) { ++ if (renderedSize !== void 0) { ++ return renderedSize; ++ } + } - const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; - state.initialScrollSession = createInitialScrollSession({ - bootstrap, @@ -3454,222 +4208,96 @@ index 914d2da..ccb19f4 100644 - previousDataLength - }); - return state.initialScrollSession; --} -- --// src/utils/checkThreshold.ts --var HYSTERESIS_MULTIPLIER = 1.3; --function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { -- const absDistance = Math.abs(distance); -- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; --} --var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { -- const absDistance = Math.abs(distance); -- const within = atThreshold || threshold > 0 && absDistance <= threshold; -- const updateSnapshot = () => { -- setSnapshot({ -- atThreshold, -- contentSize: context.contentSize, -- dataLength: context.dataLength, -- scrollPosition: context.scrollPosition -- }); -- }; -- if (!wasReached) { -- if (!within) { -- return false; -- } -- onReached(distance); -- updateSnapshot(); -- return true; -- } -- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); -- if (reset) { -- setSnapshot(void 0); -- return false; -- } -- if (within) { -- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; -- if (changed) { -- updateSnapshot(); -- } -- } -- return true; --}; -- --// src/utils/edgeReachedGate.ts --function resetEdgeLatch(ctx, edge) { -- const state = ctx.state; -- if (edge === "start") { -- state.isStartReached = false; -- state.startReachedSnapshot = void 0; -- } else { -- state.isEndReached = false; -- state.endReachedSnapshot = void 0; -- } --} --function resetSharedEdgeGateIfOutsideHysteresis(ctx) { -- const state = ctx.state; -- if (!state.edgeReachedGate) { -- return; -- } -- const contentSize = getContentSize(ctx); -- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); -- const isContentLess = contentSize < state.scrollLength; -- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; -- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; -- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); -- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); -- if (isOutsideStart && isOutsideEnd) { -- state.edgeReachedGate = void 0; -- } --} --function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { -- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; --} --function markReachedEdge(ctx) { -- ctx.state.edgeReachedGate = "closed"; --} --function prepareReachedEdgeForNextUserScroll(ctx) { -- if (ctx.state.edgeReachedGate) { -- ctx.state.edgeReachedGate = "prepared"; -- } --} --function beginReachedEdgeUserScroll(ctx, scrollDelta) { -- const state = ctx.state; -- if (state.edgeReachedGate !== "prepared") { -- return void 0; -- } -- const allowedEdge = scrollDelta < 0 ? "start" : "end"; -- state.edgeReachedGate = "closed"; -- resetEdgeLatch(ctx, allowedEdge); -- return allowedEdge; --} -- --// src/utils/hasActiveInitialScroll.ts --function hasActiveInitialScroll(state) { -- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; --} -- --// src/utils/checkAtBottom.ts --function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -- var _a3; -- const state = ctx.state; -- if (!state) { -- return; -- } -- const { -- queuedInitialLayout, -- scrollLength, -- scroll, -- maintainingScrollAtEnd, -- props: { maintainScrollAtEndThreshold, onEndReachedThreshold } -- } = state; -- const contentSize = getContentSize(ctx); -- resetSharedEdgeGateIfOutsideHysteresis(ctx); -- if (contentSize > 0 && queuedInitialLayout) { -- const insetEnd = getContentInsetEnd(ctx); -- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; -- const isContentLess = contentSize < scrollLength; -- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); -- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); -- set$( -- ctx, -- "isWithinMaintainScrollAtEndThreshold", -- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength -- ); -- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; -- if (!shouldSkipThresholdChecks) { -- state.isEndReached = checkThreshold( -- distanceFromEnd, -- isContentLess, -- onEndReachedThreshold * scrollLength, -- state.isEndReached, -- state.endReachedSnapshot, -- { -- contentSize, -- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, -- scrollPosition: scroll -- }, -- (distance) => { -- var _a4, _b; -- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { -- markReachedEdge(ctx); -- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); -- } -- }, -- (snapshot) => { -- state.endReachedSnapshot = snapshot; -- } -- ); -- } -- } --} -- --// src/utils/checkAtTop.ts --function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -- const state = ctx == null ? void 0 : ctx.state; -- if (!state) { -- return; -- } -- const { -- isStartReached, -- props: { data, onStartReachedThreshold }, -- scroll, -- scrollLength, -- startReachedSnapshot, -- totalSize -- } = state; -- const dataLength = data.length; -- const threshold = onStartReachedThreshold * scrollLength; -- resetSharedEdgeGateIfOutsideHysteresis(ctx); -- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { -- state.isStartReached = false; -- state.startReachedSnapshot = void 0; -- } -- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); -- set$(ctx, "isNearStart", scroll <= threshold); -- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; -- if (!shouldSkipThresholdChecks) { -- state.isStartReached = checkThreshold( -- scroll, -- false, -- threshold, -- state.isStartReached, -- startReachedSnapshot, -- { -- contentSize: totalSize, -- dataLength, -- scrollPosition: scroll -- }, -- (distance) => { -- var _a3, _b; -- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { -- markReachedEdge(ctx); -- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); -- } -- }, -- (snapshot) => { -- state.startReachedSnapshot = snapshot; -- } -- ); -- } --} -- --// src/utils/checkThresholds.ts --function checkThresholds(ctx, allowedEdge) { -- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; -- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); --} -- --// src/core/recalculateSettledScroll.ts --function recalculateSettledScroll(ctx) { -- var _a3, _b; -- const state = ctx.state; -- if ((_a3 = state.props) == null ? void 0 : _a3.data) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); -- } -- checkThresholds(ctx); --} ++ size = getKnownOrFixedSize(ctx, key, index, data, resolved); ++ if (size !== void 0) { ++ setSize(ctx, key, size, notifyTotalSize); ++ return size; ++ } ++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; ++ if (useAverageSize && !scrollingTo) { ++ const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; ++ if (averageSizeForType !== void 0) { ++ size = roundSize(averageSizeForType); ++ } ++ } ++ if (size === void 0 && renderedSize !== void 0) { ++ return renderedSize; ++ } ++ if (size === void 0 && useAverageSize && scrollingTo) { ++ const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; ++ if (averageSizeForType !== void 0) { ++ size = roundSize(averageSizeForType); ++ } ++ } ++ if (size === void 0) { ++ size = estimatedItemSize + ctx.scrollAxisGap; ++ } ++ setSize(ctx, key, size, notifyTotalSize); ++ return size; ++} ++function getItemSizeAtIndex(ctx, index) { ++ if (index === void 0 || index < 0) { ++ return void 0; ++ } ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; ++ } ++ if (index !== void 0) { ++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); ++ if (startOffsetAdjustment) { ++ offset += startOffsetAdjustment; ++ } ++ } ++ if (viewPosition !== void 0 && index !== void 0) { ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return offset; ++ } ++ const isOutOfBounds = index < 0 || index >= dataLength; ++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; ++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); ++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); ++ const trailingInset = getContentInsetEnd(ctx); ++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); ++ if (!isOutOfBounds && index === state.props.data.length - 1) { ++ const footerSize = peek$(ctx, "footerSize") || 0; ++ offset += footerSize; ++ } ++ } ++ return offset; ++} ++ ++// src/core/clampScrollOffset.ts ++function clampScrollOffset(ctx, offset, scrollTarget) { ++ const state = ctx.state; ++ const contentSize = getContentSize(ctx); ++ let clampedOffset = offset; ++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { ++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); ++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; ++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; ++ const maxOffset = baseMaxOffset + extraEndOffset; ++ clampedOffset = Math.min(offset, maxOffset); ++ } ++ clampedOffset = Math.max(0, clampedOffset); ++ return clampedOffset; + } + + // src/utils/checkThreshold.ts +@@ -817,626 +917,616 @@ function recalculateSettledScroll(ctx) { + } + checkThresholds(ctx); + } -var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; -var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; -var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; @@ -3694,7 +4322,10 @@ index 914d2da..ccb19f4 100644 - } -} -function resetAdaptiveRender(ctx) { -- var _a3, _b; ++ ++// src/core/finishScrollTo.ts ++function finishScrollTo(ctx) { + var _a3, _b; - clearAdaptiveRenderExitTimeout(ctx); - const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; - if (peek$(ctx, "adaptiveRender") !== mode) { @@ -3703,7 +4334,7 @@ index 914d2da..ccb19f4 100644 -} -function updateAdaptiveRender(ctx, scrollVelocity, options) { - var _a3, _b, _c; -- const state = ctx.state; + const state = ctx.state; - const adaptiveRender = state.props.adaptiveRender; - const currentMode = peek$(ctx, "adaptiveRender"); - if (peek$(ctx, "readyToRender")) { @@ -3724,10 +4355,40 @@ index 914d2da..ccb19f4 100644 - } - } else { - resetAdaptiveRender(ctx); -- } -- } --} -- ++ if (state == null ? void 0 : state.scrollingTo) { ++ cancelScrollCompletionChecks(state); ++ const resolvePendingScroll = state.pendingScrollResolve; ++ state.pendingScrollResolve = void 0; ++ const scrollingTo = state.scrollingTo; ++ state.scrollHistory.length = 0; ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ if (state.pendingTotalSize !== void 0) { ++ addTotalSize(ctx, null, state.pendingTotalSize); ++ } ++ { ++ state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); + } ++ if (scrollingTo.isInitialScroll || state.initialScroll) { ++ const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; ++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; ++ finishInitialScroll(ctx, { ++ onFinished: () => { ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); ++ }, ++ preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, ++ recalculateItems: true, ++ schedulePreservedTargetClear: shouldPreserveResizeTarget, ++ syncObservedOffset: isOffsetSession, ++ waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame ++ }); ++ return; ++ } ++ recalculateSettledScroll(ctx); ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); + } + } + -// src/utils/getEffectiveDrawDistance.ts -var INITIAL_DRAW_DISTANCE = 50; -function getEffectiveDrawDistance(ctx, mode) { @@ -3741,8 +4402,21 @@ index 914d2da..ccb19f4 100644 -function scheduleFullDrawDistancePrewarm(ctx) { - const { state } = ctx; - if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { -- return; -- } ++// src/core/doScrollTo.ts ++var SCROLL_END_IDLE_MS = 80; ++var SCROLL_END_MAX_MS = 1500; ++var SMOOTH_SCROLL_DURATION_MS = 320; ++var SCROLL_END_TARGET_EPSILON = 1; ++function doScrollTo(ctx, params) { ++ var _a3, _b; ++ const state = ctx.state; ++ const { animated, horizontal, offset } = params; ++ state.scheduledWork.cancel("platformScrollCompletion"); ++ const scroller = state.refScroller.current; ++ const node = scroller == null ? void 0 : scroller.getScrollableNode(); ++ if (!scroller || !node) { + return; + } - state.scheduledWork.frame(() => { - var _a3; - return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); @@ -3761,10 +4435,35 @@ index 914d2da..ccb19f4 100644 - } - if (resetInitialScroll) { - state.didFinishInitialScroll = false; -- } ++ const isAnimated = !!animated; ++ const isHorizontal = !!horizontal; ++ const contentSize = isHorizontal ? getContentSize(ctx) : void 0; ++ const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; ++ const top = isHorizontal ? 0 : offset; ++ scroller.scrollTo({ animated: isAnimated, x: left, y: top }); ++ if (isAnimated) { ++ const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; ++ listenForScrollEnd(ctx, { ++ readOffset: () => scroller.getCurrentScrollOffset(), ++ target, ++ targetOffset: offset ++ }); ++ } else { ++ state.scroll = offset; ++ const targetToken = state.scrollingTo; ++ state.scheduledWork.timeout( ++ () => { ++ if (targetToken === state.scrollingTo) { ++ finishScrollTo(ctx); ++ } ++ }, ++ 100, ++ "platformScrollCompletion" ++ ); + } - set$(ctx, "readyToRender", false); - resetAdaptiveRender(ctx); --} + } -function setInitialRenderState(ctx, { - didLayout, - didInitialScroll @@ -3779,20 +4478,36 @@ index 914d2da..ccb19f4 100644 - } - if (didInitialScroll) { - state.didFinishInitialScroll = true; -- } ++function listenForScrollEnd(ctx, params) { ++ const { readOffset, target, targetOffset } = params; ++ if (!target) { ++ finishScrollTo(ctx); ++ return; + } - const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); - if (isReadyToRender && !peek$(ctx, "readyToRender")) { - set$(ctx, "readyToRender", true); - setAdaptiveRender(ctx, "normal", "ready"); - if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { - scheduleFullDrawDistancePrewarm(ctx); -- } ++ const supportsScrollEnd = "onscrollend" in target; ++ let idleTimeout; ++ let settled = false; ++ const { scheduledWork, scrollingTo: targetToken } = ctx.state; ++ const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); ++ const cleanup = () => { ++ target.removeEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.removeEventListener("scrollend", onScrollEnd); + } - if (!state.didLoad) { - state.didLoad = true; - if (onLoad) { - onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); - } -- } ++ if (idleTimeout !== void 0) { ++ clearTimeout(idleTimeout); + } - } -} - @@ -3819,12 +4534,24 @@ index 914d2da..ccb19f4 100644 - if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { - if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { - cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -- } ++ clearTimeout(maxTimeout); ++ }; ++ const cancel = () => { ++ if (!settled) { ++ settled = true; ++ cleanup(); + } - if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { - cancelScrollCompletionChecks(state); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; -- } ++ }; ++ const finish = (reason) => { ++ if (settled) return; ++ if (targetToken !== ctx.state.scrollingTo) { ++ scheduledWork.cancel("platformScrollCompletion"); ++ return; + } - initialScrollCompletion.resetFlags(state); - setInitialScrollSession(state, { bootstrap: null }); - finishInitialScroll(ctx); @@ -3839,8 +4566,27 @@ index 914d2da..ccb19f4 100644 - const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); - if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { - syncInitialScrollOffset(state, observedOffset); -- } -- } ++ const currentOffset = readOffset(); ++ const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; ++ if (reason === "scrollend" && !isNearTarget) { ++ return; + } ++ scheduledWork.cancel("platformScrollCompletion"); ++ finishScrollTo(ctx); ++ }; ++ const onScroll2 = () => { ++ if (idleTimeout !== void 0) { ++ clearTimeout(idleTimeout); ++ } ++ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); ++ }; ++ const onScrollEnd = () => finish("scrollend"); ++ target.addEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.addEventListener("scrollend", onScrollEnd); ++ } else { ++ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); + } - const complete = () => { - var _a4, _b2, _c2, _d, _e; - const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; @@ -3862,8 +4608,27 @@ index 914d2da..ccb19f4 100644 - PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, - "preservedInitialScroll" - ); -- } -- } else { ++ scheduledWork.register("platformScrollCompletion", cancel); ++} ++ ++// src/utils/requestAdjust.ts ++function requestAdjust(ctx, positionDiff, source) { ++ const state = ctx.state; ++ if (Math.abs(positionDiff) > 0.1) { ++ const doit = () => { ++ { ++ state.scrollAdjustHandler.requestAdjust(positionDiff); ++ if (state.adjustingFromInitialMount) { ++ state.adjustingFromInitialMount--; ++ } + } ++ }; ++ state.scroll += positionDiff; ++ state.scrollForNextCalculateItemsInView = void 0; ++ const readyToRender = peek$(ctx, "readyToRender"); ++ if (readyToRender) { ++ doit(); + } else { - clearPreservedInitialScrollTarget(state); - } - if (options == null ? void 0 : options.recalculateItems) { @@ -3872,358 +4637,185 @@ index 914d2da..ccb19f4 100644 - setInitialRenderState(ctx, { didInitialScroll: true }); - if (shouldReleaseDeferredPublicOnScroll) { - releaseDeferredPublicOnScroll(ctx, finalScrollOffset); -- } ++ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; ++ requestAnimationFrame(doit); + } - (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); - }; - if (options == null ? void 0 : options.waitForCompletionFrame) { - requestAnimationFrame(complete); - return; -- } + } - complete(); --} -- + } + -// src/core/calculateOffsetForIndex.ts -function calculateOffsetForIndex(ctx, index) { -- const state = ctx.state; -- return index !== void 0 ? state.positions[index] || 0 : 0; --} -- - // src/core/getStartOffsetAdjustment.ts - function getStartOffsetAdjustment(ctx) { - const { state } = ctx; -@@ -1234,58 +635,273 @@ function getItemSizeAtIndex(ctx, index) { - if (index === void 0 || index < 0) { - return void 0; - } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+ const targetId = getId(ctx.state, index); -+ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++// src/core/scrollTargetSettle.ts ++var SETTLE_POSITION_EPSILON = 0.5; ++var SETTLE_QUIET_PASSES_TO_RELEASE = 2; ++var SETTLE_TTL_MS = 500; ++function getSettleTargetOffset(ctx, settle, index, position) { ++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; ++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} -+ -+// src/core/calculateOffsetWithOffsetPosition.ts -+function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -+ var _a3; -+ const state = ctx.state; ++function clearScrollTargetSettle(state) { ++ state.scrollTargetSettle = void 0; ++} ++function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; +- return index !== void 0 ? state.positions[index] || 0 : 0; + const { index, viewOffset, viewPosition } = params; -+ let offset = offsetParam; -+ if (viewOffset) { -+ offset -= viewOffset; -+ } -+ if (index !== void 0) { -+ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -+ if (startOffsetAdjustment) { -+ offset += startOffsetAdjustment; -+ } -+ } -+ if (viewPosition !== void 0 && index !== void 0) { -+ const dataLength = state.props.data.length; -+ if (dataLength === 0) { -+ return offset; -+ } -+ const isOutOfBounds = index < 0 || index >= dataLength; -+ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -+ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -+ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -+ const trailingInset = getContentInsetEnd(ctx); -+ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -+ if (!isOutOfBounds && index === state.props.data.length - 1) { -+ const footerSize = peek$(ctx, "footerSize") || 0; -+ offset += footerSize; -+ } ++ const { data } = state.props; ++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; ++ if (index < 0 || index >= data.length || isEndAlignedLastItem) { ++ clearScrollTargetSettle(state); ++ return; + } -+ return offset; ++ state.scrollTargetSettle = { ++ expiresAt: Date.now() + SETTLE_TTL_MS, ++ id: getId(state, index), ++ quietPasses: 0, ++ viewOffset, ++ viewPosition ++ }; +} -+ -+// src/core/clampScrollOffset.ts -+function clampScrollOffset(ctx, offset, scrollTarget) { ++function settleScrollTarget(ctx) { ++ var _a3; + const state = ctx.state; -+ const contentSize = getContentSize(ctx); -+ let clampedOffset = offset; -+ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { -+ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); -+ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; -+ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; -+ const maxOffset = baseMaxOffset + extraEndOffset; -+ clampedOffset = Math.min(offset, maxOffset); ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ return false; + } -+ clampedOffset = Math.max(0, clampedOffset); -+ return clampedOffset; -+} -+ -+// src/utils/checkThreshold.ts -+var HYSTERESIS_MULTIPLIER = 1.3; -+function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { -+ const absDistance = Math.abs(distance); -+ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; -+} -+var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { -+ const absDistance = Math.abs(distance); -+ const within = atThreshold || threshold > 0 && absDistance <= threshold; -+ const updateSnapshot = () => { -+ setSnapshot({ -+ atThreshold, -+ contentSize: context.contentSize, -+ dataLength: context.dataLength, -+ scrollPosition: context.scrollPosition -+ }); -+ }; -+ if (!wasReached) { -+ if (!within) { -+ return false; -+ } -+ onReached(distance); -+ updateSnapshot(); -+ return true; ++ const index = state.indexByKey.get(settle.id); ++ const position = index === void 0 ? void 0 : state.positions[index]; ++ if (index === void 0 || position === void 0) { ++ clearScrollTargetSettle(state); ++ return false; + } -+ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); -+ if (reset) { -+ setSnapshot(void 0); ++ const now = Date.now(); ++ if (now > settle.expiresAt) { ++ clearScrollTargetSettle(state); + return false; + } -+ if (within) { -+ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; -+ if (changed) { -+ updateSnapshot(); ++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position); ++ const diff = targetOffset - state.scroll; ++ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ settle.quietPasses++; ++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { ++ clearScrollTargetSettle(state); + } ++ return false; + } -+ return true; -+}; -+ -+// src/utils/edgeReachedGate.ts -+function resetEdgeLatch(ctx, edge) { -+ const state = ctx.state; -+ if (edge === "start") { -+ state.isStartReached = false; -+ state.startReachedSnapshot = void 0; -+ } else { -+ state.isEndReached = false; -+ state.endReachedSnapshot = void 0; -+ } -+} -+function resetSharedEdgeGateIfOutsideHysteresis(ctx) { -+ const state = ctx.state; -+ if (!state.edgeReachedGate) { -+ return; -+ } -+ const contentSize = getContentSize(ctx); -+ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); -+ const isContentLess = contentSize < state.scrollLength; -+ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; -+ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; -+ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); -+ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); -+ if (isOutsideStart && isOutsideEnd) { -+ state.edgeReachedGate = void 0; -+ } -+} -+function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { -+ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; -+} -+function markReachedEdge(ctx) { -+ ctx.state.edgeReachedGate = "closed"; -+} -+function prepareReachedEdgeForNextUserScroll(ctx) { -+ if (ctx.state.edgeReachedGate) { -+ ctx.state.edgeReachedGate = "prepared"; -+ } -+} -+function beginReachedEdgeUserScroll(ctx, scrollDelta) { -+ const state = ctx.state; -+ if (state.edgeReachedGate !== "prepared") { -+ return void 0; ++ settle.quietPasses = 0; ++ settle.expiresAt = now + SETTLE_TTL_MS; ++ if (((_a3 = state.scrollingTo) == null ? void 0 : _a3.index) === index) { ++ state.scrollingTo.offset = position; ++ state.scrollingTo.targetOffset = targetOffset; + } -+ const allowedEdge = scrollDelta < 0 ? "start" : "end"; -+ state.edgeReachedGate = "closed"; -+ resetEdgeLatch(ctx, allowedEdge); -+ return allowedEdge; ++ requestAdjust(ctx, diff); ++ return true; + } + +-// src/core/getStartOffsetAdjustment.ts +-function getStartOffsetAdjustment(ctx) { +- const { state } = ctx; +- const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; +- return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); ++// src/core/cancelImperativeScroll.ts ++function cancelScrollCompletionChecks({ scheduledWork }) { ++ scheduledWork.cancel("checkFinishedScrollFrame"); ++ scheduledWork.cancel("checkFinishedScrollRetryFrame"); ++ scheduledWork.cancel("checkFinishedScrollFallback"); ++ scheduledWork.cancel("platformScrollCompletion"); +} -+ -+// src/utils/hasActiveInitialScroll.ts -+function hasActiveInitialScroll(state) { -+ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; ++function settlePendingImperativeScroll(state) { ++ var _a3, _b; ++ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; ++ state.pendingScrollResolve = void 0; ++ state.pendingScrollToEnd = void 0; ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +} -+ -+// src/utils/checkAtBottom.ts -+function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -+ var _a3; -+ const state = ctx.state; -+ if (!state) { -+ return; -+ } -+ const { -+ queuedInitialLayout, -+ scrollLength, -+ scroll, -+ maintainingScrollAtEnd, -+ props: { maintainScrollAtEndThreshold, onEndReachedThreshold } -+ } = state; -+ const contentSize = getContentSize(ctx); -+ resetSharedEdgeGateIfOutsideHysteresis(ctx); -+ if (contentSize > 0 && queuedInitialLayout) { -+ const insetEnd = getContentInsetEnd(ctx); -+ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; -+ const isContentLess = contentSize < scrollLength; -+ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); -+ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); -+ set$( -+ ctx, -+ "isWithinMaintainScrollAtEndThreshold", -+ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength -+ ); -+ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; -+ if (!shouldSkipThresholdChecks) { -+ state.isEndReached = checkThreshold( -+ distanceFromEnd, -+ isContentLess, -+ onEndReachedThreshold * scrollLength, -+ state.isEndReached, -+ state.endReachedSnapshot, -+ { -+ contentSize, -+ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, -+ scrollPosition: scroll -+ }, -+ (distance) => { -+ var _a4, _b; -+ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { -+ markReachedEdge(ctx); -+ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); -+ } -+ }, -+ (snapshot) => { -+ state.endReachedSnapshot = snapshot; -+ } -+ ); ++function cancelImperativeScroll(state) { ++ cancelScrollCompletionChecks(state); ++ state.scheduledWork.cancel("imperativeScrollReady"); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ clearScrollTargetSettle(state); ++ settlePendingImperativeScroll(state); + } + +-// src/utils/getId.ts +-function getId(state, index) { +- const { data, keyExtractor } = state.props; +- if (!data) { +- return ""; ++// src/core/deferredPublicOnScroll.ts ++function withResolvedContentOffset(state, event, resolvedOffset) { ++ return { ++ ...event, ++ nativeEvent: { ++ ...event.nativeEvent, ++ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } + } -+ } ++ }; +} -+ -+// src/utils/checkAtTop.ts -+function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -+ const state = ctx == null ? void 0 : ctx.state; -+ if (!state) { -+ return; -+ } -+ const { -+ isStartReached, -+ props: { data, onStartReachedThreshold }, -+ scroll, -+ scrollLength, -+ startReachedSnapshot, -+ totalSize -+ } = state; -+ const dataLength = data.length; -+ const threshold = onStartReachedThreshold * scrollLength; -+ resetSharedEdgeGateIfOutsideHysteresis(ctx); -+ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { -+ state.isStartReached = false; -+ state.startReachedSnapshot = void 0; -+ } -+ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); -+ set$(ctx, "isNearStart", scroll <= threshold); -+ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; -+ if (!shouldSkipThresholdChecks) { -+ state.isStartReached = checkThreshold( -+ scroll, -+ false, -+ threshold, -+ state.isStartReached, -+ startReachedSnapshot, -+ { -+ contentSize: totalSize, -+ dataLength, -+ scrollPosition: scroll -+ }, -+ (distance) => { -+ var _a3, _b; -+ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { -+ markReachedEdge(ctx); -+ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); -+ } -+ }, -+ (snapshot) => { -+ state.startReachedSnapshot = snapshot; -+ } ++function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { ++ var _a3, _b, _c, _d; ++ const state = ctx.state; ++ const deferredEvent = state.deferredPublicOnScrollEvent; ++ state.deferredPublicOnScrollEvent = void 0; ++ if (deferredEvent) { ++ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( ++ _c, ++ withResolvedContentOffset( ++ state, ++ deferredEvent, ++ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 ++ ) + ); -+ } + } +- const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; +- const id = ret; +- state.idCache[index] = id; +- return id; } --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +-// src/core/updateContentMetricsState.ts +-function getRawContentLength(ctx) { +- var _a3, _b, _c; +- const { state, values } = ctx; +- return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); ++// src/core/initialScrollSession.ts ++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; ++function hasInitialScrollSessionCompletion(completion) { ++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); + } +-function getAlignItemsAtEndPadding(ctx) { +- const { state } = ctx; +- const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; +- return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; ++function clearInitialScrollSession(state) { ++ state.initialScrollSession = void 0; ++ return void 0; + } +-function updateContentMetricsState(ctx) { - var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; -- } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } -- } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; +- const { state } = ctx; +- const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; +- const nextPadding = getAlignItemsAtEndPadding(ctx); +- if (previousPadding !== nextPadding) { +- const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated"; +- const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold")); +- if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) { +- state.scheduledWork.microtask(() => { +- if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) { +- set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx)); +- } +- }, "alignItemsAtEndPaddingFallback"); +- } else if (!isMaintainExpected) { +- set$(ctx, "alignItemsAtEndPadding", nextPadding); - } -- } -- return offset; -+// src/utils/checkThresholds.ts -+function checkThresholds(ctx, allowedEdge) { -+ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; -+ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); - } - --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { -+// src/core/recalculateSettledScroll.ts -+function recalculateSettledScroll(ctx) { -+ var _a3, _b; - const state = ctx.state; -- const contentSize = getContentSize(ctx); -- let clampedOffset = offset; -- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { -- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); -- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; -- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; -- const maxOffset = baseMaxOffset + extraEndOffset; -- clampedOffset = Math.min(offset, maxOffset); -+ if ((_a3 = state.props) == null ? void 0 : _a3.data) { -+ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); - } -- clampedOffset = Math.max(0, clampedOffset); -- return clampedOffset; -+ checkThresholds(ctx); - } - - // src/core/finishScrollTo.ts -@@ -1425,6 +1041,181 @@ function listenForScrollEnd(ctx, params) { - scheduledWork.register("platformScrollCompletion", cancel); - } - -+// src/core/initialScrollSession.ts -+var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -+function hasInitialScrollSessionCompletion(completion) { -+ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); -+} -+function clearInitialScrollSession(state) { -+ state.initialScrollSession = void 0; -+ return void 0; -+} +function createInitialScrollSession(options) { + const { bootstrap, completion, kind, previousDataLength } = options; + return kind === "offset" ? { @@ -4252,10 +4844,21 @@ index 914d2da..ccb19f4 100644 + kind, + previousDataLength: state.initialScrollSession.previousDataLength + }); -+ } + } + (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; + return state.initialScrollSession.completion; -+} + } +- +-// src/core/addTotalSize.ts +-function addTotalSize(ctx, key, add, notifyTotalSize = true) { +- const state = ctx.state; +- const prevTotalSize = state.totalSize; +- let totalSize = state.totalSize; +- if (key === null) { +- totalSize = add; +- if (state.timeoutSetPaddingTop) { +- clearTimeout(state.timeoutSetPaddingTop); +- state.timeoutSetPaddingTop = void 0; +var initialScrollCompletion = { + didDispatchNativeScroll(state) { + var _a3, _b; @@ -4274,11 +4877,21 @@ index 914d2da..ccb19f4 100644 + resetFlags(state) { + if (!state.initialScrollSession) { + return; -+ } + } +- } else { +- totalSize += add; + const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); + completion.didDispatchNativeScroll = void 0; + completion.didRetrySilentInitialScroll = void 0; -+ } + } +- if (prevTotalSize !== totalSize) { +- { +- state.pendingTotalSize = void 0; +- state.totalSize = totalSize; +- if (notifyTotalSize) { +- set$(ctx, "totalSize", totalSize); +- } +- updateContentMetricsState(ctx); +}; +var initialScrollWatchdog = { + clear(state) { @@ -4302,7 +4915,9 @@ index 914d2da..ccb19f4 100644 + var _a3, _b; + if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { + return; -+ } + } +- } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { +- set$(ctx, "totalSize", totalSize); + const completion = ensureInitialScrollSessionCompletion(state); + completion.watchdog = watchdog ? { + startScroll: watchdog.startScroll, @@ -4319,10 +4934,26 @@ index 914d2da..ccb19f4 100644 + const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; + if (!kind) { + return clearInitialScrollSession(state); -+ } + } +-} +- +-// src/core/setSize.ts +-function setSize(ctx, itemKey, size, notifyTotalSize = true) { +- const state = ctx.state; +- const { sizes } = state; +- const previousSize = sizes.get(itemKey); +- const diff = previousSize !== void 0 ? size - previousSize : size; +- if (diff !== 0) { +- addTotalSize(ctx, itemKey, diff, notifyTotalSize); + if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { + return clearInitialScrollSession(state); -+ } + } +- sizes.set(itemKey, size); +-} +- +-// src/utils/helpers.ts +-function isFunction(obj) { +- return typeof obj === "function"; + const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; + state.initialScrollSession = createInitialScrollSession({ + bootstrap, @@ -4331,13 +4962,20 @@ index 914d2da..ccb19f4 100644 + previousDataLength + }); + return state.initialScrollSession; -+} + } +-function isArray(obj) { +- return Array.isArray(obj); +var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; +var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; +var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; +function clearAdaptiveRenderExitTimeout(ctx) { + ctx.state.scheduledWork.cancel("adaptiveRender"); -+} + } +-var warned = /* @__PURE__ */ new Set(); +-function warnDevOnce(id, text) { +- if (IS_DEV && !warned.has(id)) { +- warned.add(id); +- console.warn(`[legend-list] ${text}`); +function scheduleAdaptiveRenderExit(ctx, exitDelay) { + const state = ctx.state; + clearAdaptiveRenderExitTimeout(ctx); @@ -4345,18 +4983,47 @@ index 914d2da..ccb19f4 100644 + setAdaptiveRender(ctx, "normal", "scroll"); + } else { + state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); -+ } -+} + } + } +-function roundSize(size) { +- return Math.floor(size * 8) / 8; +-} +-function isNullOrUndefined(value) { +- return value === null || value === void 0; +-} +-function getPadding(s, type) { +- var _a3, _b, _c; +- const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; +- return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; +-} +-function extractPadding(style, contentContainerStyle, type) { +- return getPadding(style, type) + getPadding(contentContainerStyle, type); +-} +-function findContainerId(ctx, key) { +function setAdaptiveRender(ctx, mode, reason) { -+ var _a3, _b; + var _a3, _b; +- const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); +- if (directMatch !== void 0) { +- return directMatch; +- } +- const numContainers = peek$(ctx, "numContainers"); +- for (let i = 0; i < numContainers; i++) { +- const itemKey = peek$(ctx, `containerItemKey${i}`); +- if (itemKey === key) { +- return i; +- } + const previousMode = peek$(ctx, "adaptiveRender"); + if (previousMode !== mode) { + set$(ctx, "adaptiveRender", mode); + (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); -+ } -+} + } +- return -1; + } +- +-// src/utils/getItemSize.ts +-function getKnownOrFixedSize(ctx, key, index, data, resolved) { +function resetAdaptiveRender(ctx) { -+ var _a3, _b; + var _a3, _b; + clearAdaptiveRenderExitTimeout(ctx); + const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; + if (peek$(ctx, "adaptiveRender") !== mode) { @@ -4365,7 +5032,15 @@ index 914d2da..ccb19f4 100644 +} +function updateAdaptiveRender(ctx, scrollVelocity, options) { + var _a3, _b, _c; -+ const state = ctx.state; + const state = ctx.state; +- const { getFixedItemSize, getItemType } = state.props; +- let size = key ? state.sizesKnown.get(key) : void 0; +- if (size === void 0 && key && getFixedItemSize) { +- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; +- const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); +- if (fixedSize !== void 0) { +- size = fixedSize + ctx.scrollAxisGap; +- state.sizesKnown.set(key, size); + const adaptiveRender = state.props.adaptiveRender; + const currentMode = peek$(ctx, "adaptiveRender"); + if (peek$(ctx, "readyToRender")) { @@ -4386,31 +5061,14 @@ index 914d2da..ccb19f4 100644 + } + } else { + resetAdaptiveRender(ctx); -+ } -+ } -+} -+ - // src/core/doMaintainScrollAtEnd.ts - function doMaintainScrollAtEnd(ctx) { - const state = ctx.state; -@@ -1451,9 +1242,12 @@ function doMaintainScrollAtEnd(ctx) { - const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; - const scrollAtRequest = state.scroll; - state.maintainingScrollAtEnd = pendingState; -+ clearScrollTargetSettle(state); - requestAnimationFrame(() => { - const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); -- const didScrollSinceRequest = state.scroll !== scrollAtRequest; -+ const lastIssued = state.lastIssuedScrollOffset; -+ const isSettlingOntoIssuedScroll = lastIssued !== void 0 && Math.abs(state.scroll - lastIssued) <= EDGE_POSITION_EPSILON; -+ const didScrollSinceRequest = state.scroll !== scrollAtRequest && !isSettlingOntoIssuedScroll; - if (isStillWithinThreshold || !didScrollSinceRequest) { - state.maintainingScrollAtEnd = activeState; - const scroller = refScroller.current; -@@ -1809,6 +1603,27 @@ function prepareMVCP(ctx, dataChanged) { + } } +- return size; } - +-function getKnownOrFixedItemSize(ctx, index) { +- const key = getId(ctx.state, index); +- return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); ++ +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { @@ -4420,294 +5078,178 @@ index 914d2da..ccb19f4 100644 + const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; + const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; + return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; -+} + } +-function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { +- for (let index = startIndex; index <= endIndex; index++) { +- if (getKnownOrFixedItemSize(ctx, index) === void 0) { +- return false; +- } +function scheduleFullDrawDistancePrewarm(ctx) { + const { state } = ctx; + if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { + return; -+ } + } +- return true; + state.scheduledWork.frame(() => { + var _a3; + return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); + }, "fullDrawDistancePrewarm"); -+} + } +-function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { +- var _a3, _b, _c, _d; +- const state = ctx.state; +- const { +- sizes, +- averageSizes, +- props: { estimatedItemSize, getItemType }, +- scrollingTo +- } = state; +- const sizeKnown = state.sizesKnown.get(key); +- if (sizeKnown !== void 0) { +- return sizeKnown; +- } +- let size; +- const renderedSize = sizes.get(key); +- if (preferCachedSize) { +- if (renderedSize !== void 0) { +- return renderedSize; +- } +- } +- size = getKnownOrFixedSize(ctx, key, index, data, resolved); +- if (size !== void 0) { +- setSize(ctx, key, size, notifyTotalSize); +- return size; +- } +- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; +- if (useAverageSize && !scrollingTo) { +- const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; +- if (averageSizeForType !== void 0) { +- size = roundSize(averageSizeForType); +- } +- } +- if (size === void 0 && renderedSize !== void 0) { +- return renderedSize; +- } +- if (size === void 0 && useAverageSize && scrollingTo) { +- const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; +- if (averageSizeForType !== void 0) { +- size = roundSize(averageSizeForType); +- } +- } +- if (size === void 0) { +- size = estimatedItemSize + ctx.scrollAxisGap; + - // src/utils/getScrollVelocity.ts - var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; - var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2069,7 +1884,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { ++// src/utils/setInitialRenderState.ts ++function resetInitialRenderState(ctx, { ++ resetLayout, ++ resetInitialScroll ++}) { ++ const { state } = ctx; ++ if (resetLayout) { ++ state.didContainersLayout = false; ++ state.queuedInitialLayout = false; + } +- setSize(ctx, key, size, notifyTotalSize); +- return size; +-} +-function getItemSizeAtIndex(ctx, index) { +- if (index === void 0 || index < 0) { +- return void 0; ++ if (resetInitialScroll) { ++ state.didFinishInitialScroll = false; } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++ set$(ctx, "readyToRender", false); ++ resetAdaptiveRender(ctx); } - function scrollTo(ctx, params) { -- var _a3, _b; -+ var _a3, _b, _c; - const state = ctx.state; - const { noScrollingTo, forceScroll, ...scrollTarget } = params; - const { -@@ -2091,6 +1906,7 @@ function scrollTo(ctx, params) { - if (!noScrollingTo) { - if (isInitialScroll) { - initialScrollCompletion.resetFlags(state); -+ clearScrollTargetSettle(state); - } - const averageSizeSnapshot = getAverageSizeSnapshot(state); - state.scrollingTo = { -@@ -2101,6 +1917,15 @@ function scrollTo(ctx, params) { - }; - if (!isInitialScroll) { - pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); -+ if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { -+ beginScrollTargetSettle(ctx, { -+ index: scrollTarget.index, -+ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, -+ viewPosition: scrollTarget.viewPosition -+ }); -+ } else { -+ clearScrollTargetSettle(state); +- +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; ++function setInitialRenderState(ctx, { ++ didLayout, ++ didInitialScroll ++}) { ++ const { state } = ctx; ++ const { ++ loadStartTime, ++ props: { onLoad } ++ } = state; ++ if (didLayout) { ++ state.didContainersLayout = true; + } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } ++ if (didInitialScroll) { ++ state.didFinishInitialScroll = true; + } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; ++ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); ++ if (isReadyToRender && !peek$(ctx, "readyToRender")) { ++ set$(ctx, "readyToRender", true); ++ setAdaptiveRender(ctx, "normal", "ready"); ++ if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { ++ scheduleFullDrawDistancePrewarm(ctx); ++ } ++ if (!state.didLoad) { ++ state.didLoad = true; ++ if (onLoad) { ++ onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); + } } } - state.scrollPending = targetOffset; -@@ -2108,7 +1933,7 @@ function scrollTo(ctx, params) { - if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { - if (animated) { - if (state.scrollTargetPinnedRange) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); -+ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); - } - } else { - updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2121,6 +1946,316 @@ function scrollTo(ctx, params) { - } +- return offset; } -+// src/core/scrollTargetSettle.ts -+var SETTLE_POSITION_EPSILON = 1; -+var SETTLE_QUIET_PASSES_TO_RELEASE = 2; -+var SETTLE_MAX_MS = 1e3; -+var SETTLE_MAX_CORRECTIONS = 8; -+function clearScrollTargetSettle(state) { -+ state.scrollTargetSettle = void 0; -+ state.scheduledWork.cancel("scrollTargetSettle"); -+ state.scheduledWork.cancel("scrollTargetSettleDeadline"); -+} -+function beginScrollTargetSettle(ctx, params) { -+ const state = ctx.state; -+ const { index, viewOffset, viewPosition } = params; -+ const { alignItemsAtEnd, data, maintainScrollAtEnd } = state.props; -+ const hasEndAnchoring = !!maintainScrollAtEnd || !!alignItemsAtEnd; -+ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; -+ if (index < 0 || index >= data.length || isEndAlignedLastItem && hasEndAnchoring) { -+ clearScrollTargetSettle(state); -+ return; -+ } -+ clearScrollTargetSettle(state); -+ const now = Date.now(); -+ state.scrollTargetSettle = { -+ corrections: 0, -+ deadline: now + SETTLE_MAX_MS, -+ id: getId(state, index), -+ measuredIndex: void 0, -+ quietPasses: 0, -+ viewOffset, -+ viewPosition -+ }; -+ state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline"); -+} -+function getSettleTargetOffset(ctx, settle, index, position) { -+ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; -+ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); -+} -+function applyScrollTargetCorrection(ctx, id) { -+ var _a3; -+ const state = ctx.state; -+ const settle = state.scrollTargetSettle; -+ if (!settle || settle.id !== id) { -+ return; -+ } -+ const index = state.indexByKey.get(id); -+ const position = index === void 0 ? void 0 : state.positions[index]; -+ if (index === void 0 || position === void 0 || Date.now() > settle.deadline) { -+ clearScrollTargetSettle(state); -+ return; -+ } -+ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { -+ clearScrollTargetSettle(state); -+ return; -+ } -+ settle.corrections++; -+ settle.measuredIndex = void 0; -+ scrollTo(ctx, { -+ animated: false, -+ index, -+ itemSize: getItemSizeAtIndex(ctx, index), -+ // Correcting where a scroll already landed is not a new imperative scroll: claiming the -+ // session would suppress the list's own handling of subsequent scroll events and re-arm -+ // this settle from inside its own correction. -+ noScrollingTo: true, -+ offset: position, -+ viewOffset: settle.viewOffset, -+ viewPosition: settle.viewPosition -+ }); -+ const scrollingTo = state.scrollingTo; -+ if (scrollingTo) { -+ scrollingTo.targetOffset = state.scrollPending; -+ scrollingTo.offset = position; -+ } -+ (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); -+} -+function settleScrollTarget(ctx, options) { -+ var _a3; -+ const state = ctx.state; -+ const settle = state.scrollTargetSettle; -+ if (!settle) { -+ return false; -+ } -+ const measured = options == null ? void 0 : options.minIndexSizeChanged; -+ if (measured !== void 0) { -+ settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); -+ } -+ if (options == null ? void 0 : options.isCompensating) { -+ if (Date.now() > settle.deadline) { -+ clearScrollTargetSettle(state); -+ return false; -+ } -+ settle.quietPasses = 0; -+ return false; -+ } -+ const index = state.indexByKey.get(settle.id); -+ const position = index === void 0 ? void 0 : state.positions[index]; -+ if (index === void 0 || position === void 0) { -+ clearScrollTargetSettle(state); -+ return false; -+ } -+ if (Date.now() > settle.deadline) { -+ clearScrollTargetSettle(state); -+ return false; -+ } -+ if (settle.measuredIndex === void 0 || settle.measuredIndex > index) { -+ return false; -+ } -+ const targetOffset = getSettleTargetOffset(ctx, settle, index, position); -+ if (Math.abs(targetOffset - state.scroll) <= SETTLE_POSITION_EPSILON) { -+ settle.measuredIndex = void 0; -+ settle.quietPasses++; -+ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { -+ clearScrollTargetSettle(state); -+ } -+ return false; -+ } -+ settle.quietPasses = 0; -+ state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); -+ return true; -+} -+ -+// src/core/cancelImperativeScroll.ts -+function cancelScrollCompletionChecks({ scheduledWork }) { -+ scheduledWork.cancel("checkFinishedScrollFrame"); -+ scheduledWork.cancel("checkFinishedScrollRetryFrame"); -+ scheduledWork.cancel("checkFinishedScrollFallback"); -+ scheduledWork.cancel("platformScrollCompletion"); -+} -+function settlePendingImperativeScroll(state) { -+ var _a3, _b; -+ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; -+ state.pendingScrollResolve = void 0; -+ state.pendingScrollToEnd = void 0; -+ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); -+} -+function cancelImperativeScroll(state) { -+ cancelScrollCompletionChecks(state); -+ state.scheduledWork.cancel("imperativeScrollReady"); -+ state.scrollingTo = void 0; -+ state.scrollTargetPinnedRange = void 0; -+ clearScrollTargetSettle(state); -+ settlePendingImperativeScroll(state); -+} -+ -+// src/core/deferredPublicOnScroll.ts -+function withResolvedContentOffset(state, event, resolvedOffset) { -+ return { -+ ...event, -+ nativeEvent: { -+ ...event.nativeEvent, -+ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -+ } -+ }; -+} -+function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { -+ var _a3, _b, _c, _d; -+ const state = ctx.state; -+ const deferredEvent = state.deferredPublicOnScrollEvent; -+ state.deferredPublicOnScrollEvent = void 0; -+ if (deferredEvent) { -+ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( -+ _c, -+ withResolvedContentOffset( -+ state, -+ deferredEvent, -+ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 -+ ) -+ ); -+ } -+} -+ -+// src/utils/setInitialRenderState.ts -+function resetInitialRenderState(ctx, { -+ resetLayout, -+ resetInitialScroll -+}) { -+ const { state } = ctx; -+ if (resetLayout) { -+ state.didContainersLayout = false; -+ state.queuedInitialLayout = false; -+ } -+ if (resetInitialScroll) { -+ state.didFinishInitialScroll = false; -+ } -+ set$(ctx, "readyToRender", false); -+ resetAdaptiveRender(ctx); -+} -+function setInitialRenderState(ctx, { -+ didLayout, -+ didInitialScroll -+}) { -+ const { state } = ctx; -+ const { -+ loadStartTime, -+ props: { onLoad } -+ } = state; -+ if (didLayout) { -+ state.didContainersLayout = true; -+ } -+ if (didInitialScroll) { -+ state.didFinishInitialScroll = true; -+ } -+ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); -+ if (isReadyToRender && !peek$(ctx, "readyToRender")) { -+ set$(ctx, "readyToRender", true); -+ setAdaptiveRender(ctx, "normal", "ready"); -+ if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { -+ scheduleFullDrawDistancePrewarm(ctx); -+ } -+ if (!state.didLoad) { -+ state.didLoad = true; -+ if (onLoad) { -+ onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); -+ } -+ } -+ } -+} -+ -+// src/core/finishInitialScroll.ts -+var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; -+function syncInitialScrollOffset(state, offset) { -+ state.scroll = offset; -+ state.scrollPending = offset; -+ state.scrollPrev = offset; -+} -+function clearPreservedInitialScrollTargetTimeout(state) { -+ state.scheduledWork.cancel("preservedInitialScroll"); +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +- const state = ctx.state; +- const contentSize = getContentSize(ctx); +- let clampedOffset = offset; +- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { +- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); +- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; +- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; +- const maxOffset = baseMaxOffset + extraEndOffset; +- clampedOffset = Math.min(offset, maxOffset); +- } +- clampedOffset = Math.max(0, clampedOffset); +- return clampedOffset; ++// src/core/finishInitialScroll.ts ++var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; ++function syncInitialScrollOffset(state, offset) { ++ state.scroll = offset; ++ state.scrollPending = offset; ++ state.scrollPrev = offset; + } +- +-// src/core/finishScrollTo.ts +-function finishScrollTo(ctx) { +- var _a3, _b; ++function clearPreservedInitialScrollTargetTimeout(state) { ++ state.scheduledWork.cancel("preservedInitialScroll"); +} +function clearPreservedInitialScrollTarget(state) { + clearPreservedInitialScrollTargetTimeout(state); @@ -4717,32 +5259,134 @@ index 914d2da..ccb19f4 100644 +} +function supersedeInitialScroll(ctx) { + var _a3, _b, _c; -+ const state = ctx.state; + const state = ctx.state; +- if (state == null ? void 0 : state.scrollingTo) { +- cancelScrollCompletionChecks(state); +- const resolvePendingScroll = state.pendingScrollResolve; +- state.pendingScrollResolve = void 0; +- const scrollingTo = state.scrollingTo; +- state.scrollHistory.length = 0; +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- if (state.pendingTotalSize !== void 0) { +- addTotalSize(ctx, null, state.pendingTotalSize); +- } +- { +- state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); + const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; + if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { + if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { + cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -+ } + } +- if (scrollingTo.isInitialScroll || state.initialScroll) { +- const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; +- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; +- finishInitialScroll(ctx, { +- onFinished: () => { +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +- }, +- preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, +- recalculateItems: true, +- schedulePreservedTargetClear: shouldPreserveResizeTarget, +- syncObservedOffset: isOffsetSession, +- waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame +- }); +- return; + if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { + cancelScrollCompletionChecks(state); + state.scrollingTo = void 0; + state.scrollTargetPinnedRange = void 0; -+ } + } +- recalculateSettledScroll(ctx); +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); + initialScrollCompletion.resetFlags(state); + setInitialScrollSession(state, { bootstrap: null }); + finishInitialScroll(ctx); -+ } -+} + } + } +- +-// src/core/doScrollTo.ts +-var SCROLL_END_IDLE_MS = 80; +-var SCROLL_END_MAX_MS = 1500; +-var SMOOTH_SCROLL_DURATION_MS = 320; +-var SCROLL_END_TARGET_EPSILON = 1; +-function doScrollTo(ctx, params) { +- var _a3, _b; +function finishInitialScroll(ctx, options) { + var _a3, _b, _c; -+ const state = ctx.state; + const state = ctx.state; +- const { animated, horizontal, offset } = params; +- state.scheduledWork.cancel("platformScrollCompletion"); +- const scroller = state.refScroller.current; +- const node = scroller == null ? void 0 : scroller.getScrollableNode(); +- if (!scroller || !node) { +- return; +- } +- const isAnimated = !!animated; +- const isHorizontal = !!horizontal; +- const contentSize = isHorizontal ? getContentSize(ctx) : void 0; +- const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; +- const top = isHorizontal ? 0 : offset; +- scroller.scrollTo({ animated: isAnimated, x: left, y: top }); +- if (isAnimated) { +- const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; +- listenForScrollEnd(ctx, { +- readOffset: () => scroller.getCurrentScrollOffset(), +- target, +- targetOffset: offset +- }); +- } else { +- state.scroll = offset; +- const targetToken = state.scrollingTo; +- state.scheduledWork.timeout( +- () => { +- if (targetToken === state.scrollingTo) { +- finishScrollTo(ctx); +- } +- }, +- 100, +- "platformScrollCompletion" +- ); +- } +-} +-function listenForScrollEnd(ctx, params) { +- const { readOffset, target, targetOffset } = params; +- if (!target) { +- finishScrollTo(ctx); +- return; +- } +- const supportsScrollEnd = "onscrollend" in target; +- let idleTimeout; +- let settled = false; +- const { scheduledWork, scrollingTo: targetToken } = ctx.state; +- const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); +- const cleanup = () => { +- target.removeEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.removeEventListener("scrollend", onScrollEnd); +- } +- if (idleTimeout !== void 0) { +- clearTimeout(idleTimeout); +- } +- clearTimeout(maxTimeout); +- }; +- const cancel = () => { +- if (!settled) { +- settled = true; +- cleanup(); + if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { + syncInitialScrollOffset(state, options.resolvedOffset); + } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { + const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); + if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { + syncInitialScrollOffset(state, observedOffset); -+ } + } +- }; +- const finish = (reason) => { +- if (settled) return; +- if (targetToken !== ctx.state.scrollingTo) { +- scheduledWork.cancel("platformScrollCompletion"); +- return; + } + const complete = () => { + var _a4, _b2, _c2, _d, _e; @@ -4768,20 +5412,38 @@ index 914d2da..ccb19f4 100644 + } + } else { + clearPreservedInitialScrollTarget(state); -+ } + } +- const currentOffset = readOffset(); +- const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; +- if (reason === "scrollend" && !isNearTarget) { +- return; + if (options == null ? void 0 : options.recalculateItems) { + recalculateSettledScroll(ctx); -+ } + } +- scheduledWork.cancel("platformScrollCompletion"); +- finishScrollTo(ctx); +- }; +- const onScroll2 = () => { +- if (idleTimeout !== void 0) { +- clearTimeout(idleTimeout); + setInitialRenderState(ctx, { didInitialScroll: true }); + if (shouldReleaseDeferredPublicOnScroll) { + releaseDeferredPublicOnScroll(ctx, finalScrollOffset); -+ } + } +- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); + (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); -+ }; + }; +- const onScrollEnd = () => finish("scrollend"); +- target.addEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.addEventListener("scrollend", onScrollEnd); +- } else { +- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); + if (options == null ? void 0 : options.waitForCompletionFrame) { + requestAnimationFrame(complete); + return; -+ } + } +- scheduledWork.register("platformScrollCompletion", cancel); + complete(); +} + @@ -4789,178 +5451,89 @@ index 914d2da..ccb19f4 100644 +function calculateOffsetForIndex(ctx, index) { + const state = ctx.state; + return index !== void 0 ? state.positions[index] || 0 : 0; -+} -+ - // src/core/scrollToIndex.ts - function clampScrollIndex(index, dataLength) { - if (dataLength <= 0) { -@@ -4350,6 +4485,7 @@ function calculateItemsInView(ctx, params = {}) { - startIndex - }); - totalSize = getContentSize(ctx); -+ const minIndexSizeChangedThisPass = minIndexSizeChanged; - if (minIndexSizeChanged !== void 0) { - state.minIndexSizeChanged = void 0; + } + + // src/core/scrollRequestTracker.ts +@@ -1483,30 +1573,6 @@ function getScrollRequestTracker(ctx) { + return ctx.scrollRequestTracker; + } + +-// src/utils/requestAdjust.ts +-function requestAdjust(ctx, positionDiff, source) { +- const state = ctx.state; +- if (Math.abs(positionDiff) > 0.1) { +- const doit = () => { +- { +- state.scrollAdjustHandler.requestAdjust(positionDiff); +- if (state.adjustingFromInitialMount) { +- state.adjustingFromInitialMount--; +- } +- } +- }; +- state.scroll += positionDiff; +- state.scrollForNextCalculateItemsInView = void 0; +- const readyToRender = peek$(ctx, "readyToRender"); +- if (readyToRender) { +- doit(); +- } else { +- state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; +- requestAnimationFrame(doit); +- } +- } +-} +- + // src/core/doMaintainScrollAtEnd.ts + function finishMaintainScrollAtEnd(ctx) { + var _a3; +@@ -2141,7 +2207,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + } + } + function scrollTo(ctx, params) { +- var _a3, _b; ++ var _a3, _b, _c; + const state = ctx.state; + const { noScrollingTo, forceScroll, ...scrollTarget } = params; + const { +@@ -2173,6 +2239,15 @@ function scrollTo(ctx, params) { + }; + if (!isInitialScroll) { + pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); ++ if (scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { ++ beginScrollTargetSettle(ctx, { ++ index: scrollTarget.index, ++ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, ++ viewPosition: scrollTarget.viewPosition ++ }); ++ } else { ++ clearScrollTargetSettle(state); ++ } } -@@ -4367,8 +4503,18 @@ function calculateItemsInView(ctx, params = {}) { + } + state.scrollPending = targetOffset; +@@ -2180,7 +2255,7 @@ function scrollTo(ctx, params) { + if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { + if (animated) { + if (state.scrollTargetPinnedRange) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); + } + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); +@@ -4444,7 +4519,8 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); - const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); -- if (didMVCPAdjustScroll) { -+ const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; -+ const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); -+ const mvcp = state.props.maintainVisibleContentPosition; -+ const isUnanchoredDataChange = dataChanged && !mvcp.data; -+ const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; -+ if (!suppressInitialScrollSideEffects) { -+ settleScrollTarget(ctx, { -+ isCompensating, -+ minIndexSizeChanged: isUnanchoredDataChange ? 0 : minIndexSizeChangedThisPass -+ }); -+ } -+ if (didMVCPAdjust) { ++ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx); ++ const didMVCPAdjustScroll = didSettleScrollTarget || !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); + if (didMVCPAdjustScroll) { updateScroll2(state.scroll); updateScrollRange(); - } -@@ -6006,6 +6152,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll - // src/components/ListComponentScrollView.tsx - var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; - var SCROLL_END_FALLBACK_MS = 200; -+var SCROLL_EXTENT_EPSILON = 1; -+var REACHABLE_RETRY_FRAMES = 30; - var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; - function ensureScrollbarHiddenStyle() { - if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6094,6 +6242,26 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - } - return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; - }, [getMaxScrollOffset, horizontal, isWindowScroll]); -+ const reissueHandleRef = React3.useRef(0); -+ const scrollUntilReachable = React3.useCallback( -+ (offset, animated, run) => { -+ cancelAnimationFrame(reissueHandleRef.current); -+ let attempts = 0; -+ let previousMaxOffset = Number.NEGATIVE_INFINITY; -+ const attempt = () => { -+ const liveMaxOffset = getMaxScrollOffset(); -+ run(clampOffset(offset, liveMaxOffset)); -+ const isStillCommitting = liveMaxOffset > previousMaxOffset; -+ previousMaxOffset = liveMaxOffset; -+ if (!animated && isStillCommitting && Number.isFinite(offset) && offset - liveMaxOffset > SCROLL_EXTENT_EPSILON && ++attempts < REACHABLE_RETRY_FRAMES) { -+ reissueHandleRef.current = requestAnimationFrame(attempt); -+ } -+ }; -+ attempt(); -+ }, -+ [getMaxScrollOffset] -+ ); -+ React3.useEffect(() => () => cancelAnimationFrame(reissueHandleRef.current), []); - const scrollToLocalOffset = React3.useCallback( - (offset, animated) => { - const scrollElement = scrollRef.current; -@@ -6101,29 +6269,34 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - if (!target || typeof target.scrollTo !== "function") { - return; - } -- const maxOffset = getMaxScrollOffset(); -- const clampedOffset = clampOffset(offset, maxOffset); - const behavior = animated ? "smooth" : "auto"; - const options = { behavior }; - if (isWindowScroll) { - const scroll = getWindowScrollPosition(); - const listPos = getElementDocumentPosition(scrollElement, scroll); - const { left, top } = resolveWindowScrollTarget({ -- clampedOffset, -+ clampedOffset: clampOffset(offset, getMaxScrollOffset()), - horizontal, - listPos, - scroll - }); - options.left = left; - options.top = top; -- } else if (horizontal) { -- options.left = clampedOffset; -- } else { -- options.top = clampedOffset; -+ ctx.state.lastIssuedScrollOffset = clampOffset(offset, getMaxScrollOffset()); -+ target.scrollTo(options); -+ return; - } -- target.scrollTo(options); -+ scrollUntilReachable(offset, animated, (reachableOffset) => { -+ if (horizontal) { -+ options.left = reachableOffset; -+ } else { -+ options.top = reachableOffset; -+ } -+ ctx.state.lastIssuedScrollOffset = reachableOffset; -+ target.scrollTo(options); -+ }); - }, -- [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] -+ [ctx, getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable] - ); - React3.useImperativeHandle(ref, () => { - const api = { -@@ -6149,8 +6322,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - }, - scrollToEnd: (options = {}) => { - const { animated = true } = options; -- const endOffset = getMaxScrollOffset(); -- scrollToLocalOffset(endOffset, animated); -+ scrollToLocalOffset(getMaxScrollOffset(), animated); - }, - scrollToOffset: (params) => { - const { offset, animated = true } = params; -@@ -6499,6 +6671,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize - return props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; - } - function setHeaderSize(ctx, size) { -+ var _a3; - const { state } = ctx; - const previousHeaderSize = peek$(ctx, "headerSize") || 0; - const didChange = previousHeaderSize !== size; -@@ -6508,6 +6681,8 @@ function setHeaderSize(ctx, size) { - updateContentMetricsState(ctx); - if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { - requestAdjust(ctx, size - previousHeaderSize); -+ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) { -+ doMaintainScrollAtEnd(ctx); - } - } - state.didMeasureHeader = true; -@@ -7570,13 +7745,14 @@ function getRenderedItem(ctx, key) { - - // src/utils/normalizeMaintainScrollAtEnd.ts - function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) { -- var _a3, _b, _c, _d; -+ var _a3, _b, _c, _d, _e; - return { - animated: false, - onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true, - onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true, -- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true, -- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true -+ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true, -+ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true, -+ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true - }; - } - function normalizeMaintainScrollAtEnd(value) { -@@ -8288,6 +8464,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded - onScrollBeginDrag: (event) => { - var _a4, _b2; - prepareReachedEdgeForNextUserScroll(ctx); -+ clearScrollTargetSettle(state); - (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); - }, - onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs -index 95465f2..4fb1e41 100644 +index a97be05..a1e2968 100644 --- a/node_modules/@legendapp/list/react-native.web.mjs +++ b/node_modules/@legendapp/list/react-native.web.mjs -@@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; +@@ -409,171 +409,271 @@ var EDGE_POSITION_EPSILON = 1; var ENABLE_DEVMODE = IS_DEV && false; var ENABLE_DEBUG_VIEW = IS_DEV && false; @@ -4977,15 +5550,31 @@ index 95465f2..4fb1e41 100644 - state.pendingScrollResolve = void 0; - state.pendingScrollToEnd = void 0; - resolvePendingScroll == null ? void 0 : resolvePendingScroll(); --} ++// src/core/getStartOffsetAdjustment.ts ++function getStartOffsetAdjustment(ctx) { ++ const { state } = ctx; ++ const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; ++ return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); + } -function cancelImperativeScroll(state) { - cancelScrollCompletionChecks(state); - state.scheduledWork.cancel("imperativeScrollReady"); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; - settlePendingImperativeScroll(state); --} -- ++ ++// src/utils/getId.ts ++function getId(state, index) { ++ const { data, keyExtractor } = state.props; ++ if (!data) { ++ return ""; ++ } ++ const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; ++ const id = ret; ++ state.idCache[index] = id; ++ return id; + } + -// src/core/deferredPublicOnScroll.ts -function withResolvedContentOffset(state, event, resolvedOffset) { - return { @@ -4993,12 +5582,43 @@ index 95465f2..4fb1e41 100644 - nativeEvent: { - ...event.nativeEvent, - contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -- } ++// src/core/updateContentMetricsState.ts ++function getRawContentLength(ctx) { ++ var _a3, _b, _c; ++ const { state, values } = ctx; ++ return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); ++} ++function getAlignItemsAtEndPadding(ctx) { ++ const { state } = ctx; ++ const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; ++ return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; ++} ++function updateContentMetricsState(ctx) { ++ var _a3; ++ const { state } = ctx; ++ const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; ++ const nextPadding = getAlignItemsAtEndPadding(ctx); ++ if (previousPadding !== nextPadding) { ++ const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated"; ++ const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold")); ++ if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) { ++ state.scheduledWork.microtask(() => { ++ if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) { ++ set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx)); ++ } ++ }, "alignItemsAtEndPaddingFallback"); ++ } else if (!isMaintainExpected) { ++ set$(ctx, "alignItemsAtEndPadding", nextPadding); + } - }; --} ++ } + } -function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { - var _a3, _b, _c, _d; -- const state = ctx.state; ++ ++// src/core/addTotalSize.ts ++function addTotalSize(ctx, key, add, notifyTotalSize = true) { + const state = ctx.state; - const deferredEvent = state.deferredPublicOnScrollEvent; - state.deferredPublicOnScrollEvent = void 0; - if (deferredEvent) { @@ -5010,18 +5630,54 @@ index 95465f2..4fb1e41 100644 - (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 - ) - ); -- } --} -- ++ const prevTotalSize = state.totalSize; ++ let totalSize = state.totalSize; ++ if (key === null) { ++ totalSize = add; ++ if (state.timeoutSetPaddingTop) { ++ clearTimeout(state.timeoutSetPaddingTop); ++ state.timeoutSetPaddingTop = void 0; ++ } ++ } else { ++ totalSize += add; ++ } ++ if (prevTotalSize !== totalSize) { ++ { ++ state.pendingTotalSize = void 0; ++ state.totalSize = totalSize; ++ if (notifyTotalSize) { ++ set$(ctx, "totalSize", totalSize); ++ } ++ updateContentMetricsState(ctx); ++ } ++ } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { ++ set$(ctx, "totalSize", totalSize); + } + } + -// src/core/initialScrollSession.ts -var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -function hasInitialScrollSessionCompletion(completion) { - return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); --} ++// src/core/setSize.ts ++function setSize(ctx, itemKey, size, notifyTotalSize = true) { ++ const state = ctx.state; ++ const { sizes } = state; ++ const previousSize = sizes.get(itemKey); ++ const diff = previousSize !== void 0 ? size - previousSize : size; ++ if (diff !== 0) { ++ addTotalSize(ctx, itemKey, diff, notifyTotalSize); ++ } ++ sizes.set(itemKey, size); + } -function clearInitialScrollSession(state) { - state.initialScrollSession = void 0; - return void 0; --} ++ ++// src/utils/helpers.ts ++function isFunction(obj) { ++ return typeof obj === "function"; + } -function createInitialScrollSession(options) { - const { bootstrap, completion, kind, previousDataLength } = options; - return kind === "offset" ? { @@ -5034,7 +5690,9 @@ index 95465f2..4fb1e41 100644 - kind, - previousDataLength - }; --} ++function isArray(obj) { ++ return Array.isArray(obj); + } -function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { - var _a4, _b2; - if (!state.initialScrollSession) { @@ -5050,10 +5708,15 @@ index 95465f2..4fb1e41 100644 - kind, - previousDataLength: state.initialScrollSession.previousDataLength - }); -- } ++var warned = /* @__PURE__ */ new Set(); ++function warnDevOnce(id, text) { ++ if (IS_DEV && !warned.has(id)) { ++ warned.add(id); ++ console.warn(`[legend-list] ${text}`); + } - (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; - return state.initialScrollSession.completion; --} + } -var initialScrollCompletion = { - didDispatchNativeScroll(state) { - var _a3, _b; @@ -5072,11 +5735,36 @@ index 95465f2..4fb1e41 100644 - resetFlags(state) { - if (!state.initialScrollSession) { - return; -- } ++function roundSize(size) { ++ return Math.floor(size * 8) / 8; ++} ++function isNullOrUndefined(value) { ++ return value === null || value === void 0; ++} ++function getPadding(s, type) { ++ var _a3, _b, _c; ++ const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; ++ return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; ++} ++function extractPadding(style, contentContainerStyle, type) { ++ return getPadding(style, type) + getPadding(contentContainerStyle, type); ++} ++function findContainerId(ctx, key) { ++ var _a3, _b; ++ const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); ++ if (directMatch !== void 0) { ++ return directMatch; ++ } ++ const numContainers = peek$(ctx, "numContainers"); ++ for (let i = 0; i < numContainers; i++) { ++ const itemKey = peek$(ctx, `containerItemKey${i}`); ++ if (itemKey === key) { ++ return i; + } - const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); - completion.didDispatchNativeScroll = void 0; - completion.didRetrySilentInitialScroll = void 0; -- } + } -}; -var initialScrollWatchdog = { - clear(state) { @@ -5100,16 +5788,46 @@ index 95465f2..4fb1e41 100644 - var _a3, _b; - if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { - return; -- } ++ return -1; ++} ++ ++// src/utils/getItemSize.ts ++function getKnownOrFixedSize(ctx, key, index, data, resolved) { ++ var _a3, _b; ++ const state = ctx.state; ++ const { getFixedItemSize, getItemType } = state.props; ++ let size = key ? state.sizesKnown.get(key) : void 0; ++ if (size === void 0 && key && getFixedItemSize) { ++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; ++ const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); ++ if (fixedSize !== void 0) { ++ size = fixedSize + ctx.scrollAxisGap; ++ state.sizesKnown.set(key, size); + } - const completion = ensureInitialScrollSessionCompletion(state); - completion.watchdog = watchdog ? { - startScroll: watchdog.startScroll, - targetOffset: watchdog.targetOffset - } : void 0; -- } + } -}; -function setInitialScrollSession(state, options = {}) { -- var _a3, _b, _c, _d; ++ return size; ++} ++function getKnownOrFixedItemSize(ctx, index) { ++ const key = getId(ctx.state, index); ++ return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); ++} ++function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { ++ for (let index = startIndex; index <= endIndex; index++) { ++ if (getKnownOrFixedItemSize(ctx, index) === void 0) { ++ return false; ++ } ++ } ++ return true; ++} ++function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { + var _a3, _b, _c, _d; - const existingSession = state.initialScrollSession; - const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; - const completion = existingSession == null ? void 0 : existingSession.completion; @@ -5117,10 +5835,26 @@ index 95465f2..4fb1e41 100644 - const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; - if (!kind) { - return clearInitialScrollSession(state); -- } ++ const state = ctx.state; ++ const { ++ sizes, ++ averageSizes, ++ props: { estimatedItemSize, getItemType }, ++ scrollingTo ++ } = state; ++ const sizeKnown = state.sizesKnown.get(key); ++ if (sizeKnown !== void 0) { ++ return sizeKnown; + } - if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { - return clearInitialScrollSession(state); -- } ++ let size; ++ const renderedSize = sizes.get(key); ++ if (preferCachedSize) { ++ if (renderedSize !== void 0) { ++ return renderedSize; ++ } + } - const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; - state.initialScrollSession = createInitialScrollSession({ - bootstrap, @@ -5129,222 +5863,96 @@ index 95465f2..4fb1e41 100644 - previousDataLength - }); - return state.initialScrollSession; --} -- --// src/utils/checkThreshold.ts --var HYSTERESIS_MULTIPLIER = 1.3; --function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { -- const absDistance = Math.abs(distance); -- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; --} --var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { -- const absDistance = Math.abs(distance); -- const within = atThreshold || threshold > 0 && absDistance <= threshold; -- const updateSnapshot = () => { -- setSnapshot({ -- atThreshold, -- contentSize: context.contentSize, -- dataLength: context.dataLength, -- scrollPosition: context.scrollPosition -- }); -- }; -- if (!wasReached) { -- if (!within) { -- return false; -- } -- onReached(distance); -- updateSnapshot(); -- return true; -- } -- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); -- if (reset) { -- setSnapshot(void 0); -- return false; -- } -- if (within) { -- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; -- if (changed) { -- updateSnapshot(); -- } -- } -- return true; --}; -- --// src/utils/edgeReachedGate.ts --function resetEdgeLatch(ctx, edge) { -- const state = ctx.state; -- if (edge === "start") { -- state.isStartReached = false; -- state.startReachedSnapshot = void 0; -- } else { -- state.isEndReached = false; -- state.endReachedSnapshot = void 0; -- } --} --function resetSharedEdgeGateIfOutsideHysteresis(ctx) { -- const state = ctx.state; -- if (!state.edgeReachedGate) { -- return; -- } -- const contentSize = getContentSize(ctx); -- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); -- const isContentLess = contentSize < state.scrollLength; -- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; -- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; -- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); -- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); -- if (isOutsideStart && isOutsideEnd) { -- state.edgeReachedGate = void 0; -- } --} --function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { -- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; --} --function markReachedEdge(ctx) { -- ctx.state.edgeReachedGate = "closed"; --} --function prepareReachedEdgeForNextUserScroll(ctx) { -- if (ctx.state.edgeReachedGate) { -- ctx.state.edgeReachedGate = "prepared"; -- } --} --function beginReachedEdgeUserScroll(ctx, scrollDelta) { -- const state = ctx.state; -- if (state.edgeReachedGate !== "prepared") { -- return void 0; -- } -- const allowedEdge = scrollDelta < 0 ? "start" : "end"; -- state.edgeReachedGate = "closed"; -- resetEdgeLatch(ctx, allowedEdge); -- return allowedEdge; --} -- --// src/utils/hasActiveInitialScroll.ts --function hasActiveInitialScroll(state) { -- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; --} -- --// src/utils/checkAtBottom.ts --function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -- var _a3; -- const state = ctx.state; -- if (!state) { -- return; -- } -- const { -- queuedInitialLayout, -- scrollLength, -- scroll, -- maintainingScrollAtEnd, -- props: { maintainScrollAtEndThreshold, onEndReachedThreshold } -- } = state; -- const contentSize = getContentSize(ctx); -- resetSharedEdgeGateIfOutsideHysteresis(ctx); -- if (contentSize > 0 && queuedInitialLayout) { -- const insetEnd = getContentInsetEnd(ctx); -- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; -- const isContentLess = contentSize < scrollLength; -- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); -- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); -- set$( -- ctx, -- "isWithinMaintainScrollAtEndThreshold", -- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength -- ); -- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; -- if (!shouldSkipThresholdChecks) { -- state.isEndReached = checkThreshold( -- distanceFromEnd, -- isContentLess, -- onEndReachedThreshold * scrollLength, -- state.isEndReached, -- state.endReachedSnapshot, -- { -- contentSize, -- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, -- scrollPosition: scroll -- }, -- (distance) => { -- var _a4, _b; -- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { -- markReachedEdge(ctx); -- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); -- } -- }, -- (snapshot) => { -- state.endReachedSnapshot = snapshot; -- } -- ); -- } -- } --} -- --// src/utils/checkAtTop.ts --function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -- const state = ctx == null ? void 0 : ctx.state; -- if (!state) { -- return; -- } -- const { -- isStartReached, -- props: { data, onStartReachedThreshold }, -- scroll, -- scrollLength, -- startReachedSnapshot, -- totalSize -- } = state; -- const dataLength = data.length; -- const threshold = onStartReachedThreshold * scrollLength; -- resetSharedEdgeGateIfOutsideHysteresis(ctx); -- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { -- state.isStartReached = false; -- state.startReachedSnapshot = void 0; -- } -- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); -- set$(ctx, "isNearStart", scroll <= threshold); -- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; -- if (!shouldSkipThresholdChecks) { -- state.isStartReached = checkThreshold( -- scroll, -- false, -- threshold, -- state.isStartReached, -- startReachedSnapshot, -- { -- contentSize: totalSize, -- dataLength, -- scrollPosition: scroll -- }, -- (distance) => { -- var _a3, _b; -- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { -- markReachedEdge(ctx); -- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); -- } -- }, -- (snapshot) => { -- state.startReachedSnapshot = snapshot; -- } -- ); -- } --} -- --// src/utils/checkThresholds.ts --function checkThresholds(ctx, allowedEdge) { -- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; -- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); --} -- --// src/core/recalculateSettledScroll.ts --function recalculateSettledScroll(ctx) { -- var _a3, _b; -- const state = ctx.state; -- if ((_a3 = state.props) == null ? void 0 : _a3.data) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); -- } -- checkThresholds(ctx); --} ++ size = getKnownOrFixedSize(ctx, key, index, data, resolved); ++ if (size !== void 0) { ++ setSize(ctx, key, size, notifyTotalSize); ++ return size; ++ } ++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; ++ if (useAverageSize && !scrollingTo) { ++ const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; ++ if (averageSizeForType !== void 0) { ++ size = roundSize(averageSizeForType); ++ } ++ } ++ if (size === void 0 && renderedSize !== void 0) { ++ return renderedSize; ++ } ++ if (size === void 0 && useAverageSize && scrollingTo) { ++ const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; ++ if (averageSizeForType !== void 0) { ++ size = roundSize(averageSizeForType); ++ } ++ } ++ if (size === void 0) { ++ size = estimatedItemSize + ctx.scrollAxisGap; ++ } ++ setSize(ctx, key, size, notifyTotalSize); ++ return size; ++} ++function getItemSizeAtIndex(ctx, index) { ++ if (index === void 0 || index < 0) { ++ return void 0; ++ } ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; ++ } ++ if (index !== void 0) { ++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); ++ if (startOffsetAdjustment) { ++ offset += startOffsetAdjustment; ++ } ++ } ++ if (viewPosition !== void 0 && index !== void 0) { ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return offset; ++ } ++ const isOutOfBounds = index < 0 || index >= dataLength; ++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; ++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); ++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); ++ const trailingInset = getContentInsetEnd(ctx); ++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); ++ if (!isOutOfBounds && index === state.props.data.length - 1) { ++ const footerSize = peek$(ctx, "footerSize") || 0; ++ offset += footerSize; ++ } ++ } ++ return offset; ++} ++ ++// src/core/clampScrollOffset.ts ++function clampScrollOffset(ctx, offset, scrollTarget) { ++ const state = ctx.state; ++ const contentSize = getContentSize(ctx); ++ let clampedOffset = offset; ++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { ++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); ++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; ++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; ++ const maxOffset = baseMaxOffset + extraEndOffset; ++ clampedOffset = Math.min(offset, maxOffset); ++ } ++ clampedOffset = Math.max(0, clampedOffset); ++ return clampedOffset; + } + + // src/utils/checkThreshold.ts +@@ -796,626 +896,616 @@ function recalculateSettledScroll(ctx) { + } + checkThresholds(ctx); + } -var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; -var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; -var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; @@ -5369,7 +5977,10 @@ index 95465f2..4fb1e41 100644 - } -} -function resetAdaptiveRender(ctx) { -- var _a3, _b; ++ ++// src/core/finishScrollTo.ts ++function finishScrollTo(ctx) { + var _a3, _b; - clearAdaptiveRenderExitTimeout(ctx); - const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; - if (peek$(ctx, "adaptiveRender") !== mode) { @@ -5378,7 +5989,7 @@ index 95465f2..4fb1e41 100644 -} -function updateAdaptiveRender(ctx, scrollVelocity, options) { - var _a3, _b, _c; -- const state = ctx.state; + const state = ctx.state; - const adaptiveRender = state.props.adaptiveRender; - const currentMode = peek$(ctx, "adaptiveRender"); - if (peek$(ctx, "readyToRender")) { @@ -5399,10 +6010,40 @@ index 95465f2..4fb1e41 100644 - } - } else { - resetAdaptiveRender(ctx); -- } -- } --} -- ++ if (state == null ? void 0 : state.scrollingTo) { ++ cancelScrollCompletionChecks(state); ++ const resolvePendingScroll = state.pendingScrollResolve; ++ state.pendingScrollResolve = void 0; ++ const scrollingTo = state.scrollingTo; ++ state.scrollHistory.length = 0; ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ if (state.pendingTotalSize !== void 0) { ++ addTotalSize(ctx, null, state.pendingTotalSize); ++ } ++ { ++ state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); + } ++ if (scrollingTo.isInitialScroll || state.initialScroll) { ++ const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; ++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; ++ finishInitialScroll(ctx, { ++ onFinished: () => { ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); ++ }, ++ preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, ++ recalculateItems: true, ++ schedulePreservedTargetClear: shouldPreserveResizeTarget, ++ syncObservedOffset: isOffsetSession, ++ waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame ++ }); ++ return; ++ } ++ recalculateSettledScroll(ctx); ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); + } + } + -// src/utils/getEffectiveDrawDistance.ts -var INITIAL_DRAW_DISTANCE = 50; -function getEffectiveDrawDistance(ctx, mode) { @@ -5416,8 +6057,21 @@ index 95465f2..4fb1e41 100644 -function scheduleFullDrawDistancePrewarm(ctx) { - const { state } = ctx; - if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { -- return; -- } ++// src/core/doScrollTo.ts ++var SCROLL_END_IDLE_MS = 80; ++var SCROLL_END_MAX_MS = 1500; ++var SMOOTH_SCROLL_DURATION_MS = 320; ++var SCROLL_END_TARGET_EPSILON = 1; ++function doScrollTo(ctx, params) { ++ var _a3, _b; ++ const state = ctx.state; ++ const { animated, horizontal, offset } = params; ++ state.scheduledWork.cancel("platformScrollCompletion"); ++ const scroller = state.refScroller.current; ++ const node = scroller == null ? void 0 : scroller.getScrollableNode(); ++ if (!scroller || !node) { + return; + } - state.scheduledWork.frame(() => { - var _a3; - return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); @@ -5436,10 +6090,35 @@ index 95465f2..4fb1e41 100644 - } - if (resetInitialScroll) { - state.didFinishInitialScroll = false; -- } ++ const isAnimated = !!animated; ++ const isHorizontal = !!horizontal; ++ const contentSize = isHorizontal ? getContentSize(ctx) : void 0; ++ const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; ++ const top = isHorizontal ? 0 : offset; ++ scroller.scrollTo({ animated: isAnimated, x: left, y: top }); ++ if (isAnimated) { ++ const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; ++ listenForScrollEnd(ctx, { ++ readOffset: () => scroller.getCurrentScrollOffset(), ++ target, ++ targetOffset: offset ++ }); ++ } else { ++ state.scroll = offset; ++ const targetToken = state.scrollingTo; ++ state.scheduledWork.timeout( ++ () => { ++ if (targetToken === state.scrollingTo) { ++ finishScrollTo(ctx); ++ } ++ }, ++ 100, ++ "platformScrollCompletion" ++ ); + } - set$(ctx, "readyToRender", false); - resetAdaptiveRender(ctx); --} + } -function setInitialRenderState(ctx, { - didLayout, - didInitialScroll @@ -5454,20 +6133,36 @@ index 95465f2..4fb1e41 100644 - } - if (didInitialScroll) { - state.didFinishInitialScroll = true; -- } ++function listenForScrollEnd(ctx, params) { ++ const { readOffset, target, targetOffset } = params; ++ if (!target) { ++ finishScrollTo(ctx); ++ return; + } - const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); - if (isReadyToRender && !peek$(ctx, "readyToRender")) { - set$(ctx, "readyToRender", true); - setAdaptiveRender(ctx, "normal", "ready"); - if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { - scheduleFullDrawDistancePrewarm(ctx); -- } ++ const supportsScrollEnd = "onscrollend" in target; ++ let idleTimeout; ++ let settled = false; ++ const { scheduledWork, scrollingTo: targetToken } = ctx.state; ++ const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); ++ const cleanup = () => { ++ target.removeEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.removeEventListener("scrollend", onScrollEnd); + } - if (!state.didLoad) { - state.didLoad = true; - if (onLoad) { - onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); - } -- } ++ if (idleTimeout !== void 0) { ++ clearTimeout(idleTimeout); + } - } -} - @@ -5494,12 +6189,24 @@ index 95465f2..4fb1e41 100644 - if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { - if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { - cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -- } ++ clearTimeout(maxTimeout); ++ }; ++ const cancel = () => { ++ if (!settled) { ++ settled = true; ++ cleanup(); + } - if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { - cancelScrollCompletionChecks(state); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; -- } ++ }; ++ const finish = (reason) => { ++ if (settled) return; ++ if (targetToken !== ctx.state.scrollingTo) { ++ scheduledWork.cancel("platformScrollCompletion"); ++ return; + } - initialScrollCompletion.resetFlags(state); - setInitialScrollSession(state, { bootstrap: null }); - finishInitialScroll(ctx); @@ -5514,8 +6221,27 @@ index 95465f2..4fb1e41 100644 - const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); - if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { - syncInitialScrollOffset(state, observedOffset); -- } -- } ++ const currentOffset = readOffset(); ++ const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; ++ if (reason === "scrollend" && !isNearTarget) { ++ return; + } ++ scheduledWork.cancel("platformScrollCompletion"); ++ finishScrollTo(ctx); ++ }; ++ const onScroll2 = () => { ++ if (idleTimeout !== void 0) { ++ clearTimeout(idleTimeout); ++ } ++ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); ++ }; ++ const onScrollEnd = () => finish("scrollend"); ++ target.addEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.addEventListener("scrollend", onScrollEnd); ++ } else { ++ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); + } - const complete = () => { - var _a4, _b2, _c2, _d, _e; - const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; @@ -5537,8 +6263,27 @@ index 95465f2..4fb1e41 100644 - PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, - "preservedInitialScroll" - ); -- } -- } else { ++ scheduledWork.register("platformScrollCompletion", cancel); ++} ++ ++// src/utils/requestAdjust.ts ++function requestAdjust(ctx, positionDiff, source) { ++ const state = ctx.state; ++ if (Math.abs(positionDiff) > 0.1) { ++ const doit = () => { ++ { ++ state.scrollAdjustHandler.requestAdjust(positionDiff); ++ if (state.adjustingFromInitialMount) { ++ state.adjustingFromInitialMount--; ++ } + } ++ }; ++ state.scroll += positionDiff; ++ state.scrollForNextCalculateItemsInView = void 0; ++ const readyToRender = peek$(ctx, "readyToRender"); ++ if (readyToRender) { ++ doit(); + } else { - clearPreservedInitialScrollTarget(state); - } - if (options == null ? void 0 : options.recalculateItems) { @@ -5547,358 +6292,185 @@ index 95465f2..4fb1e41 100644 - setInitialRenderState(ctx, { didInitialScroll: true }); - if (shouldReleaseDeferredPublicOnScroll) { - releaseDeferredPublicOnScroll(ctx, finalScrollOffset); -- } ++ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; ++ requestAnimationFrame(doit); + } - (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); - }; - if (options == null ? void 0 : options.waitForCompletionFrame) { - requestAnimationFrame(complete); - return; -- } + } - complete(); --} -- + } + -// src/core/calculateOffsetForIndex.ts -function calculateOffsetForIndex(ctx, index) { -- const state = ctx.state; -- return index !== void 0 ? state.positions[index] || 0 : 0; --} -- - // src/core/getStartOffsetAdjustment.ts - function getStartOffsetAdjustment(ctx) { - const { state } = ctx; -@@ -1213,58 +614,273 @@ function getItemSizeAtIndex(ctx, index) { - if (index === void 0 || index < 0) { - return void 0; - } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+ const targetId = getId(ctx.state, index); -+ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++// src/core/scrollTargetSettle.ts ++var SETTLE_POSITION_EPSILON = 0.5; ++var SETTLE_QUIET_PASSES_TO_RELEASE = 2; ++var SETTLE_TTL_MS = 500; ++function getSettleTargetOffset(ctx, settle, index, position) { ++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; ++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} -+ -+// src/core/calculateOffsetWithOffsetPosition.ts -+function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -+ var _a3; -+ const state = ctx.state; ++function clearScrollTargetSettle(state) { ++ state.scrollTargetSettle = void 0; ++} ++function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; +- return index !== void 0 ? state.positions[index] || 0 : 0; + const { index, viewOffset, viewPosition } = params; -+ let offset = offsetParam; -+ if (viewOffset) { -+ offset -= viewOffset; -+ } -+ if (index !== void 0) { -+ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -+ if (startOffsetAdjustment) { -+ offset += startOffsetAdjustment; -+ } -+ } -+ if (viewPosition !== void 0 && index !== void 0) { -+ const dataLength = state.props.data.length; -+ if (dataLength === 0) { -+ return offset; -+ } -+ const isOutOfBounds = index < 0 || index >= dataLength; -+ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -+ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -+ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -+ const trailingInset = getContentInsetEnd(ctx); -+ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -+ if (!isOutOfBounds && index === state.props.data.length - 1) { -+ const footerSize = peek$(ctx, "footerSize") || 0; -+ offset += footerSize; -+ } ++ const { data } = state.props; ++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; ++ if (index < 0 || index >= data.length || isEndAlignedLastItem) { ++ clearScrollTargetSettle(state); ++ return; + } -+ return offset; ++ state.scrollTargetSettle = { ++ expiresAt: Date.now() + SETTLE_TTL_MS, ++ id: getId(state, index), ++ quietPasses: 0, ++ viewOffset, ++ viewPosition ++ }; +} -+ -+// src/core/clampScrollOffset.ts -+function clampScrollOffset(ctx, offset, scrollTarget) { ++function settleScrollTarget(ctx) { ++ var _a3; + const state = ctx.state; -+ const contentSize = getContentSize(ctx); -+ let clampedOffset = offset; -+ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { -+ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); -+ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; -+ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; -+ const maxOffset = baseMaxOffset + extraEndOffset; -+ clampedOffset = Math.min(offset, maxOffset); ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ return false; + } -+ clampedOffset = Math.max(0, clampedOffset); -+ return clampedOffset; -+} -+ -+// src/utils/checkThreshold.ts -+var HYSTERESIS_MULTIPLIER = 1.3; -+function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { -+ const absDistance = Math.abs(distance); -+ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; -+} -+var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { -+ const absDistance = Math.abs(distance); -+ const within = atThreshold || threshold > 0 && absDistance <= threshold; -+ const updateSnapshot = () => { -+ setSnapshot({ -+ atThreshold, -+ contentSize: context.contentSize, -+ dataLength: context.dataLength, -+ scrollPosition: context.scrollPosition -+ }); -+ }; -+ if (!wasReached) { -+ if (!within) { -+ return false; -+ } -+ onReached(distance); -+ updateSnapshot(); -+ return true; ++ const index = state.indexByKey.get(settle.id); ++ const position = index === void 0 ? void 0 : state.positions[index]; ++ if (index === void 0 || position === void 0) { ++ clearScrollTargetSettle(state); ++ return false; + } -+ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); -+ if (reset) { -+ setSnapshot(void 0); ++ const now = Date.now(); ++ if (now > settle.expiresAt) { ++ clearScrollTargetSettle(state); + return false; + } -+ if (within) { -+ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; -+ if (changed) { -+ updateSnapshot(); ++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position); ++ const diff = targetOffset - state.scroll; ++ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ settle.quietPasses++; ++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { ++ clearScrollTargetSettle(state); + } ++ return false; + } -+ return true; -+}; -+ -+// src/utils/edgeReachedGate.ts -+function resetEdgeLatch(ctx, edge) { -+ const state = ctx.state; -+ if (edge === "start") { -+ state.isStartReached = false; -+ state.startReachedSnapshot = void 0; -+ } else { -+ state.isEndReached = false; -+ state.endReachedSnapshot = void 0; -+ } -+} -+function resetSharedEdgeGateIfOutsideHysteresis(ctx) { -+ const state = ctx.state; -+ if (!state.edgeReachedGate) { -+ return; -+ } -+ const contentSize = getContentSize(ctx); -+ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); -+ const isContentLess = contentSize < state.scrollLength; -+ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; -+ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; -+ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); -+ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); -+ if (isOutsideStart && isOutsideEnd) { -+ state.edgeReachedGate = void 0; -+ } -+} -+function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { -+ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; -+} -+function markReachedEdge(ctx) { -+ ctx.state.edgeReachedGate = "closed"; -+} -+function prepareReachedEdgeForNextUserScroll(ctx) { -+ if (ctx.state.edgeReachedGate) { -+ ctx.state.edgeReachedGate = "prepared"; -+ } -+} -+function beginReachedEdgeUserScroll(ctx, scrollDelta) { -+ const state = ctx.state; -+ if (state.edgeReachedGate !== "prepared") { -+ return void 0; ++ settle.quietPasses = 0; ++ settle.expiresAt = now + SETTLE_TTL_MS; ++ if (((_a3 = state.scrollingTo) == null ? void 0 : _a3.index) === index) { ++ state.scrollingTo.offset = position; ++ state.scrollingTo.targetOffset = targetOffset; + } -+ const allowedEdge = scrollDelta < 0 ? "start" : "end"; -+ state.edgeReachedGate = "closed"; -+ resetEdgeLatch(ctx, allowedEdge); -+ return allowedEdge; ++ requestAdjust(ctx, diff); ++ return true; + } + +-// src/core/getStartOffsetAdjustment.ts +-function getStartOffsetAdjustment(ctx) { +- const { state } = ctx; +- const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; +- return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); ++// src/core/cancelImperativeScroll.ts ++function cancelScrollCompletionChecks({ scheduledWork }) { ++ scheduledWork.cancel("checkFinishedScrollFrame"); ++ scheduledWork.cancel("checkFinishedScrollRetryFrame"); ++ scheduledWork.cancel("checkFinishedScrollFallback"); ++ scheduledWork.cancel("platformScrollCompletion"); +} -+ -+// src/utils/hasActiveInitialScroll.ts -+function hasActiveInitialScroll(state) { -+ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; ++function settlePendingImperativeScroll(state) { ++ var _a3, _b; ++ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; ++ state.pendingScrollResolve = void 0; ++ state.pendingScrollToEnd = void 0; ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +} -+ -+// src/utils/checkAtBottom.ts -+function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -+ var _a3; -+ const state = ctx.state; -+ if (!state) { -+ return; -+ } -+ const { -+ queuedInitialLayout, -+ scrollLength, -+ scroll, -+ maintainingScrollAtEnd, -+ props: { maintainScrollAtEndThreshold, onEndReachedThreshold } -+ } = state; -+ const contentSize = getContentSize(ctx); -+ resetSharedEdgeGateIfOutsideHysteresis(ctx); -+ if (contentSize > 0 && queuedInitialLayout) { -+ const insetEnd = getContentInsetEnd(ctx); -+ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; -+ const isContentLess = contentSize < scrollLength; -+ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); -+ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); -+ set$( -+ ctx, -+ "isWithinMaintainScrollAtEndThreshold", -+ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength -+ ); -+ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; -+ if (!shouldSkipThresholdChecks) { -+ state.isEndReached = checkThreshold( -+ distanceFromEnd, -+ isContentLess, -+ onEndReachedThreshold * scrollLength, -+ state.isEndReached, -+ state.endReachedSnapshot, -+ { -+ contentSize, -+ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, -+ scrollPosition: scroll -+ }, -+ (distance) => { -+ var _a4, _b; -+ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { -+ markReachedEdge(ctx); -+ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); -+ } -+ }, -+ (snapshot) => { -+ state.endReachedSnapshot = snapshot; -+ } -+ ); ++function cancelImperativeScroll(state) { ++ cancelScrollCompletionChecks(state); ++ state.scheduledWork.cancel("imperativeScrollReady"); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ clearScrollTargetSettle(state); ++ settlePendingImperativeScroll(state); + } + +-// src/utils/getId.ts +-function getId(state, index) { +- const { data, keyExtractor } = state.props; +- if (!data) { +- return ""; ++// src/core/deferredPublicOnScroll.ts ++function withResolvedContentOffset(state, event, resolvedOffset) { ++ return { ++ ...event, ++ nativeEvent: { ++ ...event.nativeEvent, ++ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } + } -+ } ++ }; +} -+ -+// src/utils/checkAtTop.ts -+function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -+ const state = ctx == null ? void 0 : ctx.state; -+ if (!state) { -+ return; -+ } -+ const { -+ isStartReached, -+ props: { data, onStartReachedThreshold }, -+ scroll, -+ scrollLength, -+ startReachedSnapshot, -+ totalSize -+ } = state; -+ const dataLength = data.length; -+ const threshold = onStartReachedThreshold * scrollLength; -+ resetSharedEdgeGateIfOutsideHysteresis(ctx); -+ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { -+ state.isStartReached = false; -+ state.startReachedSnapshot = void 0; -+ } -+ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); -+ set$(ctx, "isNearStart", scroll <= threshold); -+ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; -+ if (!shouldSkipThresholdChecks) { -+ state.isStartReached = checkThreshold( -+ scroll, -+ false, -+ threshold, -+ state.isStartReached, -+ startReachedSnapshot, -+ { -+ contentSize: totalSize, -+ dataLength, -+ scrollPosition: scroll -+ }, -+ (distance) => { -+ var _a3, _b; -+ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { -+ markReachedEdge(ctx); -+ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); -+ } -+ }, -+ (snapshot) => { -+ state.startReachedSnapshot = snapshot; -+ } ++function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { ++ var _a3, _b, _c, _d; ++ const state = ctx.state; ++ const deferredEvent = state.deferredPublicOnScrollEvent; ++ state.deferredPublicOnScrollEvent = void 0; ++ if (deferredEvent) { ++ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( ++ _c, ++ withResolvedContentOffset( ++ state, ++ deferredEvent, ++ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 ++ ) + ); -+ } - } - --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; -- } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } -- } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } -- } -- return offset; -+// src/utils/checkThresholds.ts -+function checkThresholds(ctx, allowedEdge) { -+ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; -+ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); - } - --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { -+// src/core/recalculateSettledScroll.ts -+function recalculateSettledScroll(ctx) { -+ var _a3, _b; - const state = ctx.state; -- const contentSize = getContentSize(ctx); -- let clampedOffset = offset; -- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { -- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); -- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; -- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; -- const maxOffset = baseMaxOffset + extraEndOffset; -- clampedOffset = Math.min(offset, maxOffset); -+ if ((_a3 = state.props) == null ? void 0 : _a3.data) { -+ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); - } -- clampedOffset = Math.max(0, clampedOffset); -- return clampedOffset; -+ checkThresholds(ctx); - } - - // src/core/finishScrollTo.ts -@@ -1404,6 +1020,181 @@ function listenForScrollEnd(ctx, params) { - scheduledWork.register("platformScrollCompletion", cancel); + } +- const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; +- const id = ret; +- state.idCache[index] = id; +- return id; } +-// src/core/updateContentMetricsState.ts +-function getRawContentLength(ctx) { +- var _a3, _b, _c; +- const { state, values } = ctx; +- return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); +// src/core/initialScrollSession.ts +var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +function hasInitialScrollSessionCompletion(completion) { + return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); -+} + } +-function getAlignItemsAtEndPadding(ctx) { +- const { state } = ctx; +- const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; +- return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; +function clearInitialScrollSession(state) { + state.initialScrollSession = void 0; + return void 0; -+} + } +-function updateContentMetricsState(ctx) { +- var _a3; +- const { state } = ctx; +- const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; +- const nextPadding = getAlignItemsAtEndPadding(ctx); +- if (previousPadding !== nextPadding) { +- const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated"; +- const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold")); +- if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) { +- state.scheduledWork.microtask(() => { +- if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) { +- set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx)); +- } +- }, "alignItemsAtEndPaddingFallback"); +- } else if (!isMaintainExpected) { +- set$(ctx, "alignItemsAtEndPadding", nextPadding); +- } +function createInitialScrollSession(options) { + const { bootstrap, completion, kind, previousDataLength } = options; + return kind === "offset" ? { @@ -5927,10 +6499,21 @@ index 95465f2..4fb1e41 100644 + kind, + previousDataLength: state.initialScrollSession.previousDataLength + }); -+ } + } + (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; + return state.initialScrollSession.completion; -+} + } +- +-// src/core/addTotalSize.ts +-function addTotalSize(ctx, key, add, notifyTotalSize = true) { +- const state = ctx.state; +- const prevTotalSize = state.totalSize; +- let totalSize = state.totalSize; +- if (key === null) { +- totalSize = add; +- if (state.timeoutSetPaddingTop) { +- clearTimeout(state.timeoutSetPaddingTop); +- state.timeoutSetPaddingTop = void 0; +var initialScrollCompletion = { + didDispatchNativeScroll(state) { + var _a3, _b; @@ -5949,11 +6532,21 @@ index 95465f2..4fb1e41 100644 + resetFlags(state) { + if (!state.initialScrollSession) { + return; -+ } + } +- } else { +- totalSize += add; + const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); + completion.didDispatchNativeScroll = void 0; + completion.didRetrySilentInitialScroll = void 0; -+ } + } +- if (prevTotalSize !== totalSize) { +- { +- state.pendingTotalSize = void 0; +- state.totalSize = totalSize; +- if (notifyTotalSize) { +- set$(ctx, "totalSize", totalSize); +- } +- updateContentMetricsState(ctx); +}; +var initialScrollWatchdog = { + clear(state) { @@ -5977,7 +6570,9 @@ index 95465f2..4fb1e41 100644 + var _a3, _b; + if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { + return; -+ } + } +- } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { +- set$(ctx, "totalSize", totalSize); + const completion = ensureInitialScrollSessionCompletion(state); + completion.watchdog = watchdog ? { + startScroll: watchdog.startScroll, @@ -5994,10 +6589,26 @@ index 95465f2..4fb1e41 100644 + const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; + if (!kind) { + return clearInitialScrollSession(state); -+ } + } +-} +- +-// src/core/setSize.ts +-function setSize(ctx, itemKey, size, notifyTotalSize = true) { +- const state = ctx.state; +- const { sizes } = state; +- const previousSize = sizes.get(itemKey); +- const diff = previousSize !== void 0 ? size - previousSize : size; +- if (diff !== 0) { +- addTotalSize(ctx, itemKey, diff, notifyTotalSize); + if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { + return clearInitialScrollSession(state); -+ } + } +- sizes.set(itemKey, size); +-} +- +-// src/utils/helpers.ts +-function isFunction(obj) { +- return typeof obj === "function"; + const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; + state.initialScrollSession = createInitialScrollSession({ + bootstrap, @@ -6006,13 +6617,20 @@ index 95465f2..4fb1e41 100644 + previousDataLength + }); + return state.initialScrollSession; -+} + } +-function isArray(obj) { +- return Array.isArray(obj); +var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; +var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; +var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; +function clearAdaptiveRenderExitTimeout(ctx) { + ctx.state.scheduledWork.cancel("adaptiveRender"); -+} + } +-var warned = /* @__PURE__ */ new Set(); +-function warnDevOnce(id, text) { +- if (IS_DEV && !warned.has(id)) { +- warned.add(id); +- console.warn(`[legend-list] ${text}`); +function scheduleAdaptiveRenderExit(ctx, exitDelay) { + const state = ctx.state; + clearAdaptiveRenderExitTimeout(ctx); @@ -6020,18 +6638,47 @@ index 95465f2..4fb1e41 100644 + setAdaptiveRender(ctx, "normal", "scroll"); + } else { + state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); -+ } -+} + } + } +-function roundSize(size) { +- return Math.floor(size * 8) / 8; +-} +-function isNullOrUndefined(value) { +- return value === null || value === void 0; +-} +-function getPadding(s, type) { +- var _a3, _b, _c; +- const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; +- return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; +-} +-function extractPadding(style, contentContainerStyle, type) { +- return getPadding(style, type) + getPadding(contentContainerStyle, type); +-} +-function findContainerId(ctx, key) { +function setAdaptiveRender(ctx, mode, reason) { -+ var _a3, _b; + var _a3, _b; +- const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); +- if (directMatch !== void 0) { +- return directMatch; +- } +- const numContainers = peek$(ctx, "numContainers"); +- for (let i = 0; i < numContainers; i++) { +- const itemKey = peek$(ctx, `containerItemKey${i}`); +- if (itemKey === key) { +- return i; +- } + const previousMode = peek$(ctx, "adaptiveRender"); + if (previousMode !== mode) { + set$(ctx, "adaptiveRender", mode); + (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); -+ } -+} + } +- return -1; + } +- +-// src/utils/getItemSize.ts +-function getKnownOrFixedSize(ctx, key, index, data, resolved) { +function resetAdaptiveRender(ctx) { -+ var _a3, _b; + var _a3, _b; + clearAdaptiveRenderExitTimeout(ctx); + const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; + if (peek$(ctx, "adaptiveRender") !== mode) { @@ -6040,7 +6687,15 @@ index 95465f2..4fb1e41 100644 +} +function updateAdaptiveRender(ctx, scrollVelocity, options) { + var _a3, _b, _c; -+ const state = ctx.state; + const state = ctx.state; +- const { getFixedItemSize, getItemType } = state.props; +- let size = key ? state.sizesKnown.get(key) : void 0; +- if (size === void 0 && key && getFixedItemSize) { +- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; +- const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); +- if (fixedSize !== void 0) { +- size = fixedSize + ctx.scrollAxisGap; +- state.sizesKnown.set(key, size); + const adaptiveRender = state.props.adaptiveRender; + const currentMode = peek$(ctx, "adaptiveRender"); + if (peek$(ctx, "readyToRender")) { @@ -6061,31 +6716,14 @@ index 95465f2..4fb1e41 100644 + } + } else { + resetAdaptiveRender(ctx); -+ } -+ } -+} -+ - // src/core/doMaintainScrollAtEnd.ts - function doMaintainScrollAtEnd(ctx) { - const state = ctx.state; -@@ -1430,9 +1221,12 @@ function doMaintainScrollAtEnd(ctx) { - const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; - const scrollAtRequest = state.scroll; - state.maintainingScrollAtEnd = pendingState; -+ clearScrollTargetSettle(state); - requestAnimationFrame(() => { - const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); -- const didScrollSinceRequest = state.scroll !== scrollAtRequest; -+ const lastIssued = state.lastIssuedScrollOffset; -+ const isSettlingOntoIssuedScroll = lastIssued !== void 0 && Math.abs(state.scroll - lastIssued) <= EDGE_POSITION_EPSILON; -+ const didScrollSinceRequest = state.scroll !== scrollAtRequest && !isSettlingOntoIssuedScroll; - if (isStillWithinThreshold || !didScrollSinceRequest) { - state.maintainingScrollAtEnd = activeState; - const scroller = refScroller.current; -@@ -1788,6 +1582,27 @@ function prepareMVCP(ctx, dataChanged) { + } } +- return size; } - +-function getKnownOrFixedItemSize(ctx, index) { +- const key = getId(ctx.state, index); +- return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); ++ +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { @@ -6095,292 +6733,176 @@ index 95465f2..4fb1e41 100644 + const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; + const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; + return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; -+} + } +-function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { +- for (let index = startIndex; index <= endIndex; index++) { +- if (getKnownOrFixedItemSize(ctx, index) === void 0) { +- return false; +- } +function scheduleFullDrawDistancePrewarm(ctx) { + const { state } = ctx; + if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { + return; -+ } + } +- return true; + state.scheduledWork.frame(() => { + var _a3; + return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); + }, "fullDrawDistancePrewarm"); -+} + } +-function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { +- var _a3, _b, _c, _d; +- const state = ctx.state; +- const { +- sizes, +- averageSizes, +- props: { estimatedItemSize, getItemType }, +- scrollingTo +- } = state; +- const sizeKnown = state.sizesKnown.get(key); +- if (sizeKnown !== void 0) { +- return sizeKnown; +- } +- let size; +- const renderedSize = sizes.get(key); +- if (preferCachedSize) { +- if (renderedSize !== void 0) { +- return renderedSize; +- } +- } +- size = getKnownOrFixedSize(ctx, key, index, data, resolved); +- if (size !== void 0) { +- setSize(ctx, key, size, notifyTotalSize); +- return size; +- } +- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; +- if (useAverageSize && !scrollingTo) { +- const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; +- if (averageSizeForType !== void 0) { +- size = roundSize(averageSizeForType); +- } +- } +- if (size === void 0 && renderedSize !== void 0) { +- return renderedSize; +- } +- if (size === void 0 && useAverageSize && scrollingTo) { +- const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; +- if (averageSizeForType !== void 0) { +- size = roundSize(averageSizeForType); +- } +- } +- if (size === void 0) { +- size = estimatedItemSize + ctx.scrollAxisGap; + - // src/utils/getScrollVelocity.ts - var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; - var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2048,7 +1863,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { ++// src/utils/setInitialRenderState.ts ++function resetInitialRenderState(ctx, { ++ resetLayout, ++ resetInitialScroll ++}) { ++ const { state } = ctx; ++ if (resetLayout) { ++ state.didContainersLayout = false; ++ state.queuedInitialLayout = false; + } +- setSize(ctx, key, size, notifyTotalSize); +- return size; +-} +-function getItemSizeAtIndex(ctx, index) { +- if (index === void 0 || index < 0) { +- return void 0; ++ if (resetInitialScroll) { ++ state.didFinishInitialScroll = false; } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++ set$(ctx, "readyToRender", false); ++ resetAdaptiveRender(ctx); } - function scrollTo(ctx, params) { -- var _a3, _b; -+ var _a3, _b, _c; - const state = ctx.state; - const { noScrollingTo, forceScroll, ...scrollTarget } = params; - const { -@@ -2070,6 +1885,7 @@ function scrollTo(ctx, params) { - if (!noScrollingTo) { - if (isInitialScroll) { - initialScrollCompletion.resetFlags(state); -+ clearScrollTargetSettle(state); - } - const averageSizeSnapshot = getAverageSizeSnapshot(state); - state.scrollingTo = { -@@ -2080,6 +1896,15 @@ function scrollTo(ctx, params) { - }; - if (!isInitialScroll) { - pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); -+ if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { -+ beginScrollTargetSettle(ctx, { -+ index: scrollTarget.index, -+ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, -+ viewPosition: scrollTarget.viewPosition -+ }); -+ } else { -+ clearScrollTargetSettle(state); +- +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; ++function setInitialRenderState(ctx, { ++ didLayout, ++ didInitialScroll ++}) { ++ const { state } = ctx; ++ const { ++ loadStartTime, ++ props: { onLoad } ++ } = state; ++ if (didLayout) { ++ state.didContainersLayout = true; + } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } ++ if (didInitialScroll) { ++ state.didFinishInitialScroll = true; + } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; ++ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); ++ if (isReadyToRender && !peek$(ctx, "readyToRender")) { ++ set$(ctx, "readyToRender", true); ++ setAdaptiveRender(ctx, "normal", "ready"); ++ if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { ++ scheduleFullDrawDistancePrewarm(ctx); ++ } ++ if (!state.didLoad) { ++ state.didLoad = true; ++ if (onLoad) { ++ onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); + } } } - state.scrollPending = targetOffset; -@@ -2087,7 +1912,7 @@ function scrollTo(ctx, params) { - if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { - if (animated) { - if (state.scrollTargetPinnedRange) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); -+ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); - } - } else { - updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2100,6 +1925,316 @@ function scrollTo(ctx, params) { - } +- return offset; } -+// src/core/scrollTargetSettle.ts -+var SETTLE_POSITION_EPSILON = 1; -+var SETTLE_QUIET_PASSES_TO_RELEASE = 2; -+var SETTLE_MAX_MS = 1e3; -+var SETTLE_MAX_CORRECTIONS = 8; -+function clearScrollTargetSettle(state) { -+ state.scrollTargetSettle = void 0; -+ state.scheduledWork.cancel("scrollTargetSettle"); -+ state.scheduledWork.cancel("scrollTargetSettleDeadline"); -+} -+function beginScrollTargetSettle(ctx, params) { -+ const state = ctx.state; -+ const { index, viewOffset, viewPosition } = params; -+ const { alignItemsAtEnd, data, maintainScrollAtEnd } = state.props; -+ const hasEndAnchoring = !!maintainScrollAtEnd || !!alignItemsAtEnd; -+ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; -+ if (index < 0 || index >= data.length || isEndAlignedLastItem && hasEndAnchoring) { -+ clearScrollTargetSettle(state); -+ return; -+ } -+ clearScrollTargetSettle(state); -+ const now = Date.now(); -+ state.scrollTargetSettle = { -+ corrections: 0, -+ deadline: now + SETTLE_MAX_MS, -+ id: getId(state, index), -+ measuredIndex: void 0, -+ quietPasses: 0, -+ viewOffset, -+ viewPosition -+ }; -+ state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline"); -+} -+function getSettleTargetOffset(ctx, settle, index, position) { -+ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; -+ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); -+} -+function applyScrollTargetCorrection(ctx, id) { -+ var _a3; -+ const state = ctx.state; -+ const settle = state.scrollTargetSettle; -+ if (!settle || settle.id !== id) { -+ return; -+ } -+ const index = state.indexByKey.get(id); -+ const position = index === void 0 ? void 0 : state.positions[index]; -+ if (index === void 0 || position === void 0 || Date.now() > settle.deadline) { -+ clearScrollTargetSettle(state); -+ return; -+ } -+ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { -+ clearScrollTargetSettle(state); -+ return; -+ } -+ settle.corrections++; -+ settle.measuredIndex = void 0; -+ scrollTo(ctx, { -+ animated: false, -+ index, -+ itemSize: getItemSizeAtIndex(ctx, index), -+ // Correcting where a scroll already landed is not a new imperative scroll: claiming the -+ // session would suppress the list's own handling of subsequent scroll events and re-arm -+ // this settle from inside its own correction. -+ noScrollingTo: true, -+ offset: position, -+ viewOffset: settle.viewOffset, -+ viewPosition: settle.viewPosition -+ }); -+ const scrollingTo = state.scrollingTo; -+ if (scrollingTo) { -+ scrollingTo.targetOffset = state.scrollPending; -+ scrollingTo.offset = position; -+ } -+ (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); -+} -+function settleScrollTarget(ctx, options) { -+ var _a3; -+ const state = ctx.state; -+ const settle = state.scrollTargetSettle; -+ if (!settle) { -+ return false; -+ } -+ const measured = options == null ? void 0 : options.minIndexSizeChanged; -+ if (measured !== void 0) { -+ settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); -+ } -+ if (options == null ? void 0 : options.isCompensating) { -+ if (Date.now() > settle.deadline) { -+ clearScrollTargetSettle(state); -+ return false; -+ } -+ settle.quietPasses = 0; -+ return false; -+ } -+ const index = state.indexByKey.get(settle.id); -+ const position = index === void 0 ? void 0 : state.positions[index]; -+ if (index === void 0 || position === void 0) { -+ clearScrollTargetSettle(state); -+ return false; -+ } -+ if (Date.now() > settle.deadline) { -+ clearScrollTargetSettle(state); -+ return false; -+ } -+ if (settle.measuredIndex === void 0 || settle.measuredIndex > index) { -+ return false; -+ } -+ const targetOffset = getSettleTargetOffset(ctx, settle, index, position); -+ if (Math.abs(targetOffset - state.scroll) <= SETTLE_POSITION_EPSILON) { -+ settle.measuredIndex = void 0; -+ settle.quietPasses++; -+ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { -+ clearScrollTargetSettle(state); -+ } -+ return false; -+ } -+ settle.quietPasses = 0; -+ state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); -+ return true; -+} -+ -+// src/core/cancelImperativeScroll.ts -+function cancelScrollCompletionChecks({ scheduledWork }) { -+ scheduledWork.cancel("checkFinishedScrollFrame"); -+ scheduledWork.cancel("checkFinishedScrollRetryFrame"); -+ scheduledWork.cancel("checkFinishedScrollFallback"); -+ scheduledWork.cancel("platformScrollCompletion"); -+} -+function settlePendingImperativeScroll(state) { -+ var _a3, _b; -+ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; -+ state.pendingScrollResolve = void 0; -+ state.pendingScrollToEnd = void 0; -+ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); -+} -+function cancelImperativeScroll(state) { -+ cancelScrollCompletionChecks(state); -+ state.scheduledWork.cancel("imperativeScrollReady"); -+ state.scrollingTo = void 0; -+ state.scrollTargetPinnedRange = void 0; -+ clearScrollTargetSettle(state); -+ settlePendingImperativeScroll(state); -+} -+ -+// src/core/deferredPublicOnScroll.ts -+function withResolvedContentOffset(state, event, resolvedOffset) { -+ return { -+ ...event, -+ nativeEvent: { -+ ...event.nativeEvent, -+ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -+ } -+ }; -+} -+function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { -+ var _a3, _b, _c, _d; -+ const state = ctx.state; -+ const deferredEvent = state.deferredPublicOnScrollEvent; -+ state.deferredPublicOnScrollEvent = void 0; -+ if (deferredEvent) { -+ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( -+ _c, -+ withResolvedContentOffset( -+ state, -+ deferredEvent, -+ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 -+ ) -+ ); -+ } -+} -+ -+// src/utils/setInitialRenderState.ts -+function resetInitialRenderState(ctx, { -+ resetLayout, -+ resetInitialScroll -+}) { -+ const { state } = ctx; -+ if (resetLayout) { -+ state.didContainersLayout = false; -+ state.queuedInitialLayout = false; -+ } -+ if (resetInitialScroll) { -+ state.didFinishInitialScroll = false; -+ } -+ set$(ctx, "readyToRender", false); -+ resetAdaptiveRender(ctx); -+} -+function setInitialRenderState(ctx, { -+ didLayout, -+ didInitialScroll -+}) { -+ const { state } = ctx; -+ const { -+ loadStartTime, -+ props: { onLoad } -+ } = state; -+ if (didLayout) { -+ state.didContainersLayout = true; -+ } -+ if (didInitialScroll) { -+ state.didFinishInitialScroll = true; -+ } -+ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); -+ if (isReadyToRender && !peek$(ctx, "readyToRender")) { -+ set$(ctx, "readyToRender", true); -+ setAdaptiveRender(ctx, "normal", "ready"); -+ if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { -+ scheduleFullDrawDistancePrewarm(ctx); -+ } -+ if (!state.didLoad) { -+ state.didLoad = true; -+ if (onLoad) { -+ onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); -+ } -+ } -+ } -+} -+ +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +- const state = ctx.state; +- const contentSize = getContentSize(ctx); +- let clampedOffset = offset; +- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { +- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); +- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; +- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; +- const maxOffset = baseMaxOffset + extraEndOffset; +- clampedOffset = Math.min(offset, maxOffset); +- } +- clampedOffset = Math.max(0, clampedOffset); +- return clampedOffset; +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; +function syncInitialScrollOffset(state, offset) { + state.scroll = offset; + state.scrollPending = offset; + state.scrollPrev = offset; -+} + } +- +-// src/core/finishScrollTo.ts +-function finishScrollTo(ctx) { +- var _a3, _b; +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -6392,32 +6914,134 @@ index 95465f2..4fb1e41 100644 +} +function supersedeInitialScroll(ctx) { + var _a3, _b, _c; -+ const state = ctx.state; + const state = ctx.state; +- if (state == null ? void 0 : state.scrollingTo) { +- cancelScrollCompletionChecks(state); +- const resolvePendingScroll = state.pendingScrollResolve; +- state.pendingScrollResolve = void 0; +- const scrollingTo = state.scrollingTo; +- state.scrollHistory.length = 0; +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- if (state.pendingTotalSize !== void 0) { +- addTotalSize(ctx, null, state.pendingTotalSize); +- } +- { +- state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); + const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; + if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { + if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { + cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -+ } + } +- if (scrollingTo.isInitialScroll || state.initialScroll) { +- const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; +- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; +- finishInitialScroll(ctx, { +- onFinished: () => { +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +- }, +- preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, +- recalculateItems: true, +- schedulePreservedTargetClear: shouldPreserveResizeTarget, +- syncObservedOffset: isOffsetSession, +- waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame +- }); +- return; + if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { + cancelScrollCompletionChecks(state); + state.scrollingTo = void 0; + state.scrollTargetPinnedRange = void 0; -+ } + } +- recalculateSettledScroll(ctx); +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); + initialScrollCompletion.resetFlags(state); + setInitialScrollSession(state, { bootstrap: null }); + finishInitialScroll(ctx); -+ } -+} + } + } +- +-// src/core/doScrollTo.ts +-var SCROLL_END_IDLE_MS = 80; +-var SCROLL_END_MAX_MS = 1500; +-var SMOOTH_SCROLL_DURATION_MS = 320; +-var SCROLL_END_TARGET_EPSILON = 1; +-function doScrollTo(ctx, params) { +- var _a3, _b; +function finishInitialScroll(ctx, options) { + var _a3, _b, _c; -+ const state = ctx.state; + const state = ctx.state; +- const { animated, horizontal, offset } = params; +- state.scheduledWork.cancel("platformScrollCompletion"); +- const scroller = state.refScroller.current; +- const node = scroller == null ? void 0 : scroller.getScrollableNode(); +- if (!scroller || !node) { +- return; +- } +- const isAnimated = !!animated; +- const isHorizontal = !!horizontal; +- const contentSize = isHorizontal ? getContentSize(ctx) : void 0; +- const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; +- const top = isHorizontal ? 0 : offset; +- scroller.scrollTo({ animated: isAnimated, x: left, y: top }); +- if (isAnimated) { +- const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; +- listenForScrollEnd(ctx, { +- readOffset: () => scroller.getCurrentScrollOffset(), +- target, +- targetOffset: offset +- }); +- } else { +- state.scroll = offset; +- const targetToken = state.scrollingTo; +- state.scheduledWork.timeout( +- () => { +- if (targetToken === state.scrollingTo) { +- finishScrollTo(ctx); +- } +- }, +- 100, +- "platformScrollCompletion" +- ); +- } +-} +-function listenForScrollEnd(ctx, params) { +- const { readOffset, target, targetOffset } = params; +- if (!target) { +- finishScrollTo(ctx); +- return; +- } +- const supportsScrollEnd = "onscrollend" in target; +- let idleTimeout; +- let settled = false; +- const { scheduledWork, scrollingTo: targetToken } = ctx.state; +- const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); +- const cleanup = () => { +- target.removeEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.removeEventListener("scrollend", onScrollEnd); +- } +- if (idleTimeout !== void 0) { +- clearTimeout(idleTimeout); +- } +- clearTimeout(maxTimeout); +- }; +- const cancel = () => { +- if (!settled) { +- settled = true; +- cleanup(); + if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { + syncInitialScrollOffset(state, options.resolvedOffset); + } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { + const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); + if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { + syncInitialScrollOffset(state, observedOffset); -+ } + } +- }; +- const finish = (reason) => { +- if (settled) return; +- if (targetToken !== ctx.state.scrollingTo) { +- scheduledWork.cancel("platformScrollCompletion"); +- return; + } + const complete = () => { + var _a4, _b2, _c2, _d, _e; @@ -6443,20 +7067,38 @@ index 95465f2..4fb1e41 100644 + } + } else { + clearPreservedInitialScrollTarget(state); -+ } + } +- const currentOffset = readOffset(); +- const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; +- if (reason === "scrollend" && !isNearTarget) { +- return; + if (options == null ? void 0 : options.recalculateItems) { + recalculateSettledScroll(ctx); -+ } + } +- scheduledWork.cancel("platformScrollCompletion"); +- finishScrollTo(ctx); +- }; +- const onScroll2 = () => { +- if (idleTimeout !== void 0) { +- clearTimeout(idleTimeout); + setInitialRenderState(ctx, { didInitialScroll: true }); + if (shouldReleaseDeferredPublicOnScroll) { + releaseDeferredPublicOnScroll(ctx, finalScrollOffset); -+ } + } +- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); + (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); -+ }; + }; +- const onScrollEnd = () => finish("scrollend"); +- target.addEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.addEventListener("scrollend", onScrollEnd); +- } else { +- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); + if (options == null ? void 0 : options.waitForCompletionFrame) { + requestAnimationFrame(complete); + return; -+ } + } +- scheduledWork.register("platformScrollCompletion", cancel); + complete(); +} + @@ -6464,195 +7106,89 @@ index 95465f2..4fb1e41 100644 +function calculateOffsetForIndex(ctx, index) { + const state = ctx.state; + return index !== void 0 ? state.positions[index] || 0 : 0; -+} -+ - // src/core/scrollToIndex.ts - function clampScrollIndex(index, dataLength) { - if (dataLength <= 0) { -@@ -4329,6 +4464,7 @@ function calculateItemsInView(ctx, params = {}) { - startIndex - }); - totalSize = getContentSize(ctx); -+ const minIndexSizeChangedThisPass = minIndexSizeChanged; - if (minIndexSizeChanged !== void 0) { - state.minIndexSizeChanged = void 0; - } -@@ -4346,8 +4482,18 @@ function calculateItemsInView(ctx, params = {}) { - const scrollBeforeMVCP = state.scroll; - const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; - checkMVCP == null ? void 0 : checkMVCP(); -- const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); -- if (didMVCPAdjustScroll) { -+ const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; -+ const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); -+ const mvcp = state.props.maintainVisibleContentPosition; -+ const isUnanchoredDataChange = dataChanged && !mvcp.data; -+ const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; -+ if (!suppressInitialScrollSideEffects) { -+ settleScrollTarget(ctx, { -+ isCompensating, -+ minIndexSizeChanged: isUnanchoredDataChange ? 0 : minIndexSizeChangedThisPass -+ }); -+ } -+ if (didMVCPAdjust) { - updateScroll2(state.scroll); - updateScrollRange(); - } -@@ -5985,6 +6131,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll - // src/components/ListComponentScrollView.tsx - var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; - var SCROLL_END_FALLBACK_MS = 200; -+var SCROLL_EXTENT_EPSILON = 1; -+var REACHABLE_RETRY_FRAMES = 30; - var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; - function ensureScrollbarHiddenStyle() { - if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6073,6 +6221,26 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - } - return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; - }, [getMaxScrollOffset, horizontal, isWindowScroll]); -+ const reissueHandleRef = useRef(0); -+ const scrollUntilReachable = useCallback( -+ (offset, animated, run) => { -+ cancelAnimationFrame(reissueHandleRef.current); -+ let attempts = 0; -+ let previousMaxOffset = Number.NEGATIVE_INFINITY; -+ const attempt = () => { -+ const liveMaxOffset = getMaxScrollOffset(); -+ run(clampOffset(offset, liveMaxOffset)); -+ const isStillCommitting = liveMaxOffset > previousMaxOffset; -+ previousMaxOffset = liveMaxOffset; -+ if (!animated && isStillCommitting && Number.isFinite(offset) && offset - liveMaxOffset > SCROLL_EXTENT_EPSILON && ++attempts < REACHABLE_RETRY_FRAMES) { -+ reissueHandleRef.current = requestAnimationFrame(attempt); -+ } -+ }; -+ attempt(); -+ }, -+ [getMaxScrollOffset] -+ ); -+ useEffect(() => () => cancelAnimationFrame(reissueHandleRef.current), []); - const scrollToLocalOffset = useCallback( - (offset, animated) => { - const scrollElement = scrollRef.current; -@@ -6080,29 +6248,34 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - if (!target || typeof target.scrollTo !== "function") { - return; - } -- const maxOffset = getMaxScrollOffset(); -- const clampedOffset = clampOffset(offset, maxOffset); - const behavior = animated ? "smooth" : "auto"; - const options = { behavior }; - if (isWindowScroll) { - const scroll = getWindowScrollPosition(); - const listPos = getElementDocumentPosition(scrollElement, scroll); - const { left, top } = resolveWindowScrollTarget({ -- clampedOffset, -+ clampedOffset: clampOffset(offset, getMaxScrollOffset()), - horizontal, - listPos, - scroll - }); - options.left = left; - options.top = top; -- } else if (horizontal) { -- options.left = clampedOffset; -- } else { -- options.top = clampedOffset; -+ ctx.state.lastIssuedScrollOffset = clampOffset(offset, getMaxScrollOffset()); -+ target.scrollTo(options); -+ return; - } -- target.scrollTo(options); -+ scrollUntilReachable(offset, animated, (reachableOffset) => { -+ if (horizontal) { -+ options.left = reachableOffset; -+ } else { -+ options.top = reachableOffset; -+ } -+ ctx.state.lastIssuedScrollOffset = reachableOffset; -+ target.scrollTo(options); -+ }); - }, -- [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] -+ [ctx, getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable] - ); - useImperativeHandle(ref, () => { - const api = { -@@ -6128,8 +6301,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - }, - scrollToEnd: (options = {}) => { - const { animated = true } = options; -- const endOffset = getMaxScrollOffset(); -- scrollToLocalOffset(endOffset, animated); -+ scrollToLocalOffset(getMaxScrollOffset(), animated); - }, - scrollToOffset: (params) => { - const { offset, animated = true } = params; -@@ -6478,6 +6650,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize - return props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; } - function setHeaderSize(ctx, size) { -+ var _a3; - const { state } = ctx; - const previousHeaderSize = peek$(ctx, "headerSize") || 0; - const didChange = previousHeaderSize !== size; -@@ -6487,6 +6660,8 @@ function setHeaderSize(ctx, size) { - updateContentMetricsState(ctx); - if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { - requestAdjust(ctx, size - previousHeaderSize); -+ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) { -+ doMaintainScrollAtEnd(ctx); - } - } - state.didMeasureHeader = true; -@@ -7549,13 +7724,14 @@ function getRenderedItem(ctx, key) { - // src/utils/normalizeMaintainScrollAtEnd.ts - function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) { -- var _a3, _b, _c, _d; -+ var _a3, _b, _c, _d, _e; - return { - animated: false, - onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true, - onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true, -- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true, -- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true -+ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true, -+ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true, -+ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true - }; + // src/core/scrollRequestTracker.ts +@@ -1462,30 +1552,6 @@ function getScrollRequestTracker(ctx) { + return ctx.scrollRequestTracker; } - function normalizeMaintainScrollAtEnd(value) { -@@ -8267,6 +8443,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded - onScrollBeginDrag: (event) => { - var _a4, _b2; - prepareReachedEdgeForNextUserScroll(ctx); -+ clearScrollTargetSettle(state); - (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); - }, - onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) -diff --git a/node_modules/@legendapp/list/react.d.ts b/node_modules/@legendapp/list/react.d.ts -index b6c7481..ace847e 100644 ---- a/node_modules/@legendapp/list/react.d.ts -+++ b/node_modules/@legendapp/list/react.d.ts -@@ -511,6 +511,12 @@ interface AlwaysRenderConfig { - interface MaintainScrollAtEndOnOptions { - dataChange?: boolean; - footerLayout?: boolean; -+ /** -+ * Keep the list pinned to the end when the header changes size. A header that measures larger -+ * than its estimate pushes every item down, which leaves a list that had already scrolled to -+ * its end short of it by the difference. -+ */ -+ headerLayout?: boolean; - itemLayout?: boolean; - layout?: boolean; + +-// src/utils/requestAdjust.ts +-function requestAdjust(ctx, positionDiff, source) { +- const state = ctx.state; +- if (Math.abs(positionDiff) > 0.1) { +- const doit = () => { +- { +- state.scrollAdjustHandler.requestAdjust(positionDiff); +- if (state.adjustingFromInitialMount) { +- state.adjustingFromInitialMount--; +- } +- } +- }; +- state.scroll += positionDiff; +- state.scrollForNextCalculateItemsInView = void 0; +- const readyToRender = peek$(ctx, "readyToRender"); +- if (readyToRender) { +- doit(); +- } else { +- state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; +- requestAnimationFrame(doit); +- } +- } +-} +- + // src/core/doMaintainScrollAtEnd.ts + function finishMaintainScrollAtEnd(ctx) { + var _a3; +@@ -2120,7 +2186,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + } } + function scrollTo(ctx, params) { +- var _a3, _b; ++ var _a3, _b, _c; + const state = ctx.state; + const { noScrollingTo, forceScroll, ...scrollTarget } = params; + const { +@@ -2152,6 +2218,15 @@ function scrollTo(ctx, params) { + }; + if (!isInitialScroll) { + pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); ++ if (scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { ++ beginScrollTargetSettle(ctx, { ++ index: scrollTarget.index, ++ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, ++ viewPosition: scrollTarget.viewPosition ++ }); ++ } else { ++ clearScrollTargetSettle(state); ++ } + } + } + state.scrollPending = targetOffset; +@@ -2159,7 +2234,7 @@ function scrollTo(ctx, params) { + if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { + if (animated) { + if (state.scrollTargetPinnedRange) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); + } + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); +@@ -4423,7 +4498,8 @@ function calculateItemsInView(ctx, params = {}) { + const scrollBeforeMVCP = state.scroll; + const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; + checkMVCP == null ? void 0 : checkMVCP(); +- const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); ++ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx); ++ const didMVCPAdjustScroll = didSettleScrollTarget || !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); + if (didMVCPAdjustScroll) { + updateScroll2(state.scroll); + updateScrollRange(); diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js -index 914d2da..ccb19f4 100644 +index b2240b5..69282b3 100644 --- a/node_modules/@legendapp/list/react.js +++ b/node_modules/@legendapp/list/react.js -@@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1; +@@ -430,171 +430,271 @@ var EDGE_POSITION_EPSILON = 1; var ENABLE_DEVMODE = IS_DEV && false; var ENABLE_DEBUG_VIEW = IS_DEV && false; @@ -6669,15 +7205,31 @@ index 914d2da..ccb19f4 100644 - state.pendingScrollResolve = void 0; - state.pendingScrollToEnd = void 0; - resolvePendingScroll == null ? void 0 : resolvePendingScroll(); --} ++// src/core/getStartOffsetAdjustment.ts ++function getStartOffsetAdjustment(ctx) { ++ const { state } = ctx; ++ const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; ++ return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); + } -function cancelImperativeScroll(state) { - cancelScrollCompletionChecks(state); - state.scheduledWork.cancel("imperativeScrollReady"); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; - settlePendingImperativeScroll(state); --} -- ++ ++// src/utils/getId.ts ++function getId(state, index) { ++ const { data, keyExtractor } = state.props; ++ if (!data) { ++ return ""; ++ } ++ const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; ++ const id = ret; ++ state.idCache[index] = id; ++ return id; + } + -// src/core/deferredPublicOnScroll.ts -function withResolvedContentOffset(state, event, resolvedOffset) { - return { @@ -6685,12 +7237,43 @@ index 914d2da..ccb19f4 100644 - nativeEvent: { - ...event.nativeEvent, - contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -- } ++// src/core/updateContentMetricsState.ts ++function getRawContentLength(ctx) { ++ var _a3, _b, _c; ++ const { state, values } = ctx; ++ return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); ++} ++function getAlignItemsAtEndPadding(ctx) { ++ const { state } = ctx; ++ const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; ++ return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; ++} ++function updateContentMetricsState(ctx) { ++ var _a3; ++ const { state } = ctx; ++ const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; ++ const nextPadding = getAlignItemsAtEndPadding(ctx); ++ if (previousPadding !== nextPadding) { ++ const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated"; ++ const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold")); ++ if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) { ++ state.scheduledWork.microtask(() => { ++ if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) { ++ set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx)); ++ } ++ }, "alignItemsAtEndPaddingFallback"); ++ } else if (!isMaintainExpected) { ++ set$(ctx, "alignItemsAtEndPadding", nextPadding); + } - }; --} ++ } + } -function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { - var _a3, _b, _c, _d; -- const state = ctx.state; ++ ++// src/core/addTotalSize.ts ++function addTotalSize(ctx, key, add, notifyTotalSize = true) { + const state = ctx.state; - const deferredEvent = state.deferredPublicOnScrollEvent; - state.deferredPublicOnScrollEvent = void 0; - if (deferredEvent) { @@ -6702,18 +7285,54 @@ index 914d2da..ccb19f4 100644 - (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 - ) - ); -- } --} -- ++ const prevTotalSize = state.totalSize; ++ let totalSize = state.totalSize; ++ if (key === null) { ++ totalSize = add; ++ if (state.timeoutSetPaddingTop) { ++ clearTimeout(state.timeoutSetPaddingTop); ++ state.timeoutSetPaddingTop = void 0; ++ } ++ } else { ++ totalSize += add; ++ } ++ if (prevTotalSize !== totalSize) { ++ { ++ state.pendingTotalSize = void 0; ++ state.totalSize = totalSize; ++ if (notifyTotalSize) { ++ set$(ctx, "totalSize", totalSize); ++ } ++ updateContentMetricsState(ctx); ++ } ++ } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { ++ set$(ctx, "totalSize", totalSize); + } + } + -// src/core/initialScrollSession.ts -var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -function hasInitialScrollSessionCompletion(completion) { - return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); --} ++// src/core/setSize.ts ++function setSize(ctx, itemKey, size, notifyTotalSize = true) { ++ const state = ctx.state; ++ const { sizes } = state; ++ const previousSize = sizes.get(itemKey); ++ const diff = previousSize !== void 0 ? size - previousSize : size; ++ if (diff !== 0) { ++ addTotalSize(ctx, itemKey, diff, notifyTotalSize); ++ } ++ sizes.set(itemKey, size); + } -function clearInitialScrollSession(state) { - state.initialScrollSession = void 0; - return void 0; --} ++ ++// src/utils/helpers.ts ++function isFunction(obj) { ++ return typeof obj === "function"; + } -function createInitialScrollSession(options) { - const { bootstrap, completion, kind, previousDataLength } = options; - return kind === "offset" ? { @@ -6726,7 +7345,9 @@ index 914d2da..ccb19f4 100644 - kind, - previousDataLength - }; --} ++function isArray(obj) { ++ return Array.isArray(obj); + } -function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { - var _a4, _b2; - if (!state.initialScrollSession) { @@ -6742,10 +7363,15 @@ index 914d2da..ccb19f4 100644 - kind, - previousDataLength: state.initialScrollSession.previousDataLength - }); -- } ++var warned = /* @__PURE__ */ new Set(); ++function warnDevOnce(id, text) { ++ if (IS_DEV && !warned.has(id)) { ++ warned.add(id); ++ console.warn(`[legend-list] ${text}`); + } - (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; - return state.initialScrollSession.completion; --} + } -var initialScrollCompletion = { - didDispatchNativeScroll(state) { - var _a3, _b; @@ -6764,11 +7390,36 @@ index 914d2da..ccb19f4 100644 - resetFlags(state) { - if (!state.initialScrollSession) { - return; -- } ++function roundSize(size) { ++ return Math.floor(size * 8) / 8; ++} ++function isNullOrUndefined(value) { ++ return value === null || value === void 0; ++} ++function getPadding(s, type) { ++ var _a3, _b, _c; ++ const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; ++ return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; ++} ++function extractPadding(style, contentContainerStyle, type) { ++ return getPadding(style, type) + getPadding(contentContainerStyle, type); ++} ++function findContainerId(ctx, key) { ++ var _a3, _b; ++ const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); ++ if (directMatch !== void 0) { ++ return directMatch; ++ } ++ const numContainers = peek$(ctx, "numContainers"); ++ for (let i = 0; i < numContainers; i++) { ++ const itemKey = peek$(ctx, `containerItemKey${i}`); ++ if (itemKey === key) { ++ return i; + } - const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); - completion.didDispatchNativeScroll = void 0; - completion.didRetrySilentInitialScroll = void 0; -- } + } -}; -var initialScrollWatchdog = { - clear(state) { @@ -6792,16 +7443,46 @@ index 914d2da..ccb19f4 100644 - var _a3, _b; - if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { - return; -- } ++ return -1; ++} ++ ++// src/utils/getItemSize.ts ++function getKnownOrFixedSize(ctx, key, index, data, resolved) { ++ var _a3, _b; ++ const state = ctx.state; ++ const { getFixedItemSize, getItemType } = state.props; ++ let size = key ? state.sizesKnown.get(key) : void 0; ++ if (size === void 0 && key && getFixedItemSize) { ++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; ++ const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); ++ if (fixedSize !== void 0) { ++ size = fixedSize + ctx.scrollAxisGap; ++ state.sizesKnown.set(key, size); + } - const completion = ensureInitialScrollSessionCompletion(state); - completion.watchdog = watchdog ? { - startScroll: watchdog.startScroll, - targetOffset: watchdog.targetOffset - } : void 0; -- } + } -}; -function setInitialScrollSession(state, options = {}) { -- var _a3, _b, _c, _d; ++ return size; ++} ++function getKnownOrFixedItemSize(ctx, index) { ++ const key = getId(ctx.state, index); ++ return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); ++} ++function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { ++ for (let index = startIndex; index <= endIndex; index++) { ++ if (getKnownOrFixedItemSize(ctx, index) === void 0) { ++ return false; ++ } ++ } ++ return true; ++} ++function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { + var _a3, _b, _c, _d; - const existingSession = state.initialScrollSession; - const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; - const completion = existingSession == null ? void 0 : existingSession.completion; @@ -6809,10 +7490,26 @@ index 914d2da..ccb19f4 100644 - const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; - if (!kind) { - return clearInitialScrollSession(state); -- } ++ const state = ctx.state; ++ const { ++ sizes, ++ averageSizes, ++ props: { estimatedItemSize, getItemType }, ++ scrollingTo ++ } = state; ++ const sizeKnown = state.sizesKnown.get(key); ++ if (sizeKnown !== void 0) { ++ return sizeKnown; + } - if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { - return clearInitialScrollSession(state); -- } ++ let size; ++ const renderedSize = sizes.get(key); ++ if (preferCachedSize) { ++ if (renderedSize !== void 0) { ++ return renderedSize; ++ } + } - const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; - state.initialScrollSession = createInitialScrollSession({ - bootstrap, @@ -6821,256 +7518,133 @@ index 914d2da..ccb19f4 100644 - previousDataLength - }); - return state.initialScrollSession; ++ size = getKnownOrFixedSize(ctx, key, index, data, resolved); ++ if (size !== void 0) { ++ setSize(ctx, key, size, notifyTotalSize); ++ return size; ++ } ++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; ++ if (useAverageSize && !scrollingTo) { ++ const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; ++ if (averageSizeForType !== void 0) { ++ size = roundSize(averageSizeForType); ++ } ++ } ++ if (size === void 0 && renderedSize !== void 0) { ++ return renderedSize; ++ } ++ if (size === void 0 && useAverageSize && scrollingTo) { ++ const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; ++ if (averageSizeForType !== void 0) { ++ size = roundSize(averageSizeForType); ++ } ++ } ++ if (size === void 0) { ++ size = estimatedItemSize + ctx.scrollAxisGap; ++ } ++ setSize(ctx, key, size, notifyTotalSize); ++ return size; ++} ++function getItemSizeAtIndex(ctx, index) { ++ if (index === void 0 || index < 0) { ++ return void 0; ++ } ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; ++ } ++ if (index !== void 0) { ++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); ++ if (startOffsetAdjustment) { ++ offset += startOffsetAdjustment; ++ } ++ } ++ if (viewPosition !== void 0 && index !== void 0) { ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return offset; ++ } ++ const isOutOfBounds = index < 0 || index >= dataLength; ++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; ++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); ++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); ++ const trailingInset = getContentInsetEnd(ctx); ++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); ++ if (!isOutOfBounds && index === state.props.data.length - 1) { ++ const footerSize = peek$(ctx, "footerSize") || 0; ++ offset += footerSize; ++ } ++ } ++ return offset; ++} ++ ++// src/core/clampScrollOffset.ts ++function clampScrollOffset(ctx, offset, scrollTarget) { ++ const state = ctx.state; ++ const contentSize = getContentSize(ctx); ++ let clampedOffset = offset; ++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { ++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); ++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; ++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; ++ const maxOffset = baseMaxOffset + extraEndOffset; ++ clampedOffset = Math.min(offset, maxOffset); ++ } ++ clampedOffset = Math.max(0, clampedOffset); ++ return clampedOffset; + } + + // src/utils/checkThreshold.ts +@@ -817,626 +917,616 @@ function recalculateSettledScroll(ctx) { + } + checkThresholds(ctx); + } +-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; +-function clearAdaptiveRenderExitTimeout(ctx) { +- ctx.state.scheduledWork.cancel("adaptiveRender"); -} -- --// src/utils/checkThreshold.ts --var HYSTERESIS_MULTIPLIER = 1.3; --function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { -- const absDistance = Math.abs(distance); -- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; --} --var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { -- const absDistance = Math.abs(distance); -- const within = atThreshold || threshold > 0 && absDistance <= threshold; -- const updateSnapshot = () => { -- setSnapshot({ -- atThreshold, -- contentSize: context.contentSize, -- dataLength: context.dataLength, -- scrollPosition: context.scrollPosition -- }); -- }; -- if (!wasReached) { -- if (!within) { -- return false; -- } -- onReached(distance); -- updateSnapshot(); -- return true; -- } -- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); -- if (reset) { -- setSnapshot(void 0); -- return false; -- } -- if (within) { -- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; -- if (changed) { -- updateSnapshot(); -- } -- } -- return true; --}; -- --// src/utils/edgeReachedGate.ts --function resetEdgeLatch(ctx, edge) { +-function scheduleAdaptiveRenderExit(ctx, exitDelay) { - const state = ctx.state; -- if (edge === "start") { -- state.isStartReached = false; -- state.startReachedSnapshot = void 0; +- clearAdaptiveRenderExitTimeout(ctx); +- if (exitDelay <= 0) { +- setAdaptiveRender(ctx, "normal", "scroll"); - } else { -- state.isEndReached = false; -- state.endReachedSnapshot = void 0; -- } --} --function resetSharedEdgeGateIfOutsideHysteresis(ctx) { -- const state = ctx.state; -- if (!state.edgeReachedGate) { -- return; -- } -- const contentSize = getContentSize(ctx); -- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); -- const isContentLess = contentSize < state.scrollLength; -- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; -- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; -- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); -- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); -- if (isOutsideStart && isOutsideEnd) { -- state.edgeReachedGate = void 0; +- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); - } -} --function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { -- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; --} --function markReachedEdge(ctx) { -- ctx.state.edgeReachedGate = "closed"; --} --function prepareReachedEdgeForNextUserScroll(ctx) { -- if (ctx.state.edgeReachedGate) { -- ctx.state.edgeReachedGate = "prepared"; +-function setAdaptiveRender(ctx, mode, reason) { +- var _a3, _b; +- const previousMode = peek$(ctx, "adaptiveRender"); +- if (previousMode !== mode) { +- set$(ctx, "adaptiveRender", mode); +- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); - } -} --function beginReachedEdgeUserScroll(ctx, scrollDelta) { -- const state = ctx.state; -- if (state.edgeReachedGate !== "prepared") { -- return void 0; -- } -- const allowedEdge = scrollDelta < 0 ? "start" : "end"; -- state.edgeReachedGate = "closed"; -- resetEdgeLatch(ctx, allowedEdge); -- return allowedEdge; --} -- --// src/utils/hasActiveInitialScroll.ts --function hasActiveInitialScroll(state) { -- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; --} -- --// src/utils/checkAtBottom.ts --function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -- var _a3; -- const state = ctx.state; -- if (!state) { -- return; -- } -- const { -- queuedInitialLayout, -- scrollLength, -- scroll, -- maintainingScrollAtEnd, -- props: { maintainScrollAtEndThreshold, onEndReachedThreshold } -- } = state; -- const contentSize = getContentSize(ctx); -- resetSharedEdgeGateIfOutsideHysteresis(ctx); -- if (contentSize > 0 && queuedInitialLayout) { -- const insetEnd = getContentInsetEnd(ctx); -- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; -- const isContentLess = contentSize < scrollLength; -- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); -- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); -- set$( -- ctx, -- "isWithinMaintainScrollAtEndThreshold", -- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength -- ); -- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; -- if (!shouldSkipThresholdChecks) { -- state.isEndReached = checkThreshold( -- distanceFromEnd, -- isContentLess, -- onEndReachedThreshold * scrollLength, -- state.isEndReached, -- state.endReachedSnapshot, -- { -- contentSize, -- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, -- scrollPosition: scroll -- }, -- (distance) => { -- var _a4, _b; -- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { -- markReachedEdge(ctx); -- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); -- } -- }, -- (snapshot) => { -- state.endReachedSnapshot = snapshot; -- } -- ); -- } -- } --} -- --// src/utils/checkAtTop.ts --function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -- const state = ctx == null ? void 0 : ctx.state; -- if (!state) { -- return; -- } -- const { -- isStartReached, -- props: { data, onStartReachedThreshold }, -- scroll, -- scrollLength, -- startReachedSnapshot, -- totalSize -- } = state; -- const dataLength = data.length; -- const threshold = onStartReachedThreshold * scrollLength; -- resetSharedEdgeGateIfOutsideHysteresis(ctx); -- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { -- state.isStartReached = false; -- state.startReachedSnapshot = void 0; -- } -- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); -- set$(ctx, "isNearStart", scroll <= threshold); -- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; -- if (!shouldSkipThresholdChecks) { -- state.isStartReached = checkThreshold( -- scroll, -- false, -- threshold, -- state.isStartReached, -- startReachedSnapshot, -- { -- contentSize: totalSize, -- dataLength, -- scrollPosition: scroll -- }, -- (distance) => { -- var _a3, _b; -- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { -- markReachedEdge(ctx); -- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); -- } -- }, -- (snapshot) => { -- state.startReachedSnapshot = snapshot; -- } -- ); -- } --} -- --// src/utils/checkThresholds.ts --function checkThresholds(ctx, allowedEdge) { -- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; -- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); --} -- --// src/core/recalculateSettledScroll.ts --function recalculateSettledScroll(ctx) { -- var _a3, _b; -- const state = ctx.state; -- if ((_a3 = state.props) == null ? void 0 : _a3.data) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); -- } -- checkThresholds(ctx); --} --var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; --var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; --var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; --function clearAdaptiveRenderExitTimeout(ctx) { -- ctx.state.scheduledWork.cancel("adaptiveRender"); --} --function scheduleAdaptiveRenderExit(ctx, exitDelay) { -- const state = ctx.state; -- clearAdaptiveRenderExitTimeout(ctx); -- if (exitDelay <= 0) { -- setAdaptiveRender(ctx, "normal", "scroll"); -- } else { -- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); -- } --} --function setAdaptiveRender(ctx, mode, reason) { -- var _a3, _b; -- const previousMode = peek$(ctx, "adaptiveRender"); -- if (previousMode !== mode) { -- set$(ctx, "adaptiveRender", mode); -- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); -- } --} --function resetAdaptiveRender(ctx) { -- var _a3, _b; -- clearAdaptiveRenderExitTimeout(ctx); -- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; -- if (peek$(ctx, "adaptiveRender") !== mode) { -- setAdaptiveRender(ctx, mode, "initial"); +-function resetAdaptiveRender(ctx) { ++ ++// src/core/finishScrollTo.ts ++function finishScrollTo(ctx) { + var _a3, _b; +- clearAdaptiveRenderExitTimeout(ctx); +- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; +- if (peek$(ctx, "adaptiveRender") !== mode) { +- setAdaptiveRender(ctx, mode, "initial"); - } -} -function updateAdaptiveRender(ctx, scrollVelocity, options) { - var _a3, _b, _c; -- const state = ctx.state; + const state = ctx.state; - const adaptiveRender = state.props.adaptiveRender; - const currentMode = peek$(ctx, "adaptiveRender"); - if (peek$(ctx, "readyToRender")) { @@ -7091,10 +7665,40 @@ index 914d2da..ccb19f4 100644 - } - } else { - resetAdaptiveRender(ctx); -- } -- } --} -- ++ if (state == null ? void 0 : state.scrollingTo) { ++ cancelScrollCompletionChecks(state); ++ const resolvePendingScroll = state.pendingScrollResolve; ++ state.pendingScrollResolve = void 0; ++ const scrollingTo = state.scrollingTo; ++ state.scrollHistory.length = 0; ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ if (state.pendingTotalSize !== void 0) { ++ addTotalSize(ctx, null, state.pendingTotalSize); ++ } ++ { ++ state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); + } ++ if (scrollingTo.isInitialScroll || state.initialScroll) { ++ const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; ++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; ++ finishInitialScroll(ctx, { ++ onFinished: () => { ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); ++ }, ++ preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, ++ recalculateItems: true, ++ schedulePreservedTargetClear: shouldPreserveResizeTarget, ++ syncObservedOffset: isOffsetSession, ++ waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame ++ }); ++ return; ++ } ++ recalculateSettledScroll(ctx); ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); + } + } + -// src/utils/getEffectiveDrawDistance.ts -var INITIAL_DRAW_DISTANCE = 50; -function getEffectiveDrawDistance(ctx, mode) { @@ -7108,8 +7712,21 @@ index 914d2da..ccb19f4 100644 -function scheduleFullDrawDistancePrewarm(ctx) { - const { state } = ctx; - if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { -- return; -- } ++// src/core/doScrollTo.ts ++var SCROLL_END_IDLE_MS = 80; ++var SCROLL_END_MAX_MS = 1500; ++var SMOOTH_SCROLL_DURATION_MS = 320; ++var SCROLL_END_TARGET_EPSILON = 1; ++function doScrollTo(ctx, params) { ++ var _a3, _b; ++ const state = ctx.state; ++ const { animated, horizontal, offset } = params; ++ state.scheduledWork.cancel("platformScrollCompletion"); ++ const scroller = state.refScroller.current; ++ const node = scroller == null ? void 0 : scroller.getScrollableNode(); ++ if (!scroller || !node) { + return; + } - state.scheduledWork.frame(() => { - var _a3; - return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); @@ -7128,10 +7745,35 @@ index 914d2da..ccb19f4 100644 - } - if (resetInitialScroll) { - state.didFinishInitialScroll = false; -- } ++ const isAnimated = !!animated; ++ const isHorizontal = !!horizontal; ++ const contentSize = isHorizontal ? getContentSize(ctx) : void 0; ++ const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; ++ const top = isHorizontal ? 0 : offset; ++ scroller.scrollTo({ animated: isAnimated, x: left, y: top }); ++ if (isAnimated) { ++ const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; ++ listenForScrollEnd(ctx, { ++ readOffset: () => scroller.getCurrentScrollOffset(), ++ target, ++ targetOffset: offset ++ }); ++ } else { ++ state.scroll = offset; ++ const targetToken = state.scrollingTo; ++ state.scheduledWork.timeout( ++ () => { ++ if (targetToken === state.scrollingTo) { ++ finishScrollTo(ctx); ++ } ++ }, ++ 100, ++ "platformScrollCompletion" ++ ); + } - set$(ctx, "readyToRender", false); - resetAdaptiveRender(ctx); --} + } -function setInitialRenderState(ctx, { - didLayout, - didInitialScroll @@ -7146,20 +7788,36 @@ index 914d2da..ccb19f4 100644 - } - if (didInitialScroll) { - state.didFinishInitialScroll = true; -- } ++function listenForScrollEnd(ctx, params) { ++ const { readOffset, target, targetOffset } = params; ++ if (!target) { ++ finishScrollTo(ctx); ++ return; + } - const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); - if (isReadyToRender && !peek$(ctx, "readyToRender")) { - set$(ctx, "readyToRender", true); - setAdaptiveRender(ctx, "normal", "ready"); - if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { - scheduleFullDrawDistancePrewarm(ctx); -- } ++ const supportsScrollEnd = "onscrollend" in target; ++ let idleTimeout; ++ let settled = false; ++ const { scheduledWork, scrollingTo: targetToken } = ctx.state; ++ const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); ++ const cleanup = () => { ++ target.removeEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.removeEventListener("scrollend", onScrollEnd); + } - if (!state.didLoad) { - state.didLoad = true; - if (onLoad) { - onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); - } -- } ++ if (idleTimeout !== void 0) { ++ clearTimeout(idleTimeout); + } - } -} - @@ -7186,12 +7844,24 @@ index 914d2da..ccb19f4 100644 - if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { - if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { - cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -- } ++ clearTimeout(maxTimeout); ++ }; ++ const cancel = () => { ++ if (!settled) { ++ settled = true; ++ cleanup(); + } - if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { - cancelScrollCompletionChecks(state); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; -- } ++ }; ++ const finish = (reason) => { ++ if (settled) return; ++ if (targetToken !== ctx.state.scrollingTo) { ++ scheduledWork.cancel("platformScrollCompletion"); ++ return; + } - initialScrollCompletion.resetFlags(state); - setInitialScrollSession(state, { bootstrap: null }); - finishInitialScroll(ctx); @@ -7206,8 +7876,27 @@ index 914d2da..ccb19f4 100644 - const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); - if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { - syncInitialScrollOffset(state, observedOffset); -- } -- } ++ const currentOffset = readOffset(); ++ const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; ++ if (reason === "scrollend" && !isNearTarget) { ++ return; + } ++ scheduledWork.cancel("platformScrollCompletion"); ++ finishScrollTo(ctx); ++ }; ++ const onScroll2 = () => { ++ if (idleTimeout !== void 0) { ++ clearTimeout(idleTimeout); ++ } ++ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); ++ }; ++ const onScrollEnd = () => finish("scrollend"); ++ target.addEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.addEventListener("scrollend", onScrollEnd); ++ } else { ++ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); + } - const complete = () => { - var _a4, _b2, _c2, _d, _e; - const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; @@ -7229,8 +7918,27 @@ index 914d2da..ccb19f4 100644 - PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, - "preservedInitialScroll" - ); -- } -- } else { ++ scheduledWork.register("platformScrollCompletion", cancel); ++} ++ ++// src/utils/requestAdjust.ts ++function requestAdjust(ctx, positionDiff, source) { ++ const state = ctx.state; ++ if (Math.abs(positionDiff) > 0.1) { ++ const doit = () => { ++ { ++ state.scrollAdjustHandler.requestAdjust(positionDiff); ++ if (state.adjustingFromInitialMount) { ++ state.adjustingFromInitialMount--; ++ } + } ++ }; ++ state.scroll += positionDiff; ++ state.scrollForNextCalculateItemsInView = void 0; ++ const readyToRender = peek$(ctx, "readyToRender"); ++ if (readyToRender) { ++ doit(); + } else { - clearPreservedInitialScrollTarget(state); - } - if (options == null ? void 0 : options.recalculateItems) { @@ -7239,358 +7947,185 @@ index 914d2da..ccb19f4 100644 - setInitialRenderState(ctx, { didInitialScroll: true }); - if (shouldReleaseDeferredPublicOnScroll) { - releaseDeferredPublicOnScroll(ctx, finalScrollOffset); -- } ++ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; ++ requestAnimationFrame(doit); + } - (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); - }; - if (options == null ? void 0 : options.waitForCompletionFrame) { - requestAnimationFrame(complete); - return; -- } + } - complete(); --} -- + } + -// src/core/calculateOffsetForIndex.ts -function calculateOffsetForIndex(ctx, index) { -- const state = ctx.state; -- return index !== void 0 ? state.positions[index] || 0 : 0; --} -- - // src/core/getStartOffsetAdjustment.ts - function getStartOffsetAdjustment(ctx) { - const { state } = ctx; -@@ -1234,58 +635,273 @@ function getItemSizeAtIndex(ctx, index) { - if (index === void 0 || index < 0) { - return void 0; - } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+ const targetId = getId(ctx.state, index); -+ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++// src/core/scrollTargetSettle.ts ++var SETTLE_POSITION_EPSILON = 0.5; ++var SETTLE_QUIET_PASSES_TO_RELEASE = 2; ++var SETTLE_TTL_MS = 500; ++function getSettleTargetOffset(ctx, settle, index, position) { ++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; ++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); +} -+ -+// src/core/calculateOffsetWithOffsetPosition.ts -+function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -+ var _a3; -+ const state = ctx.state; ++function clearScrollTargetSettle(state) { ++ state.scrollTargetSettle = void 0; ++} ++function beginScrollTargetSettle(ctx, params) { + const state = ctx.state; +- return index !== void 0 ? state.positions[index] || 0 : 0; + const { index, viewOffset, viewPosition } = params; -+ let offset = offsetParam; -+ if (viewOffset) { -+ offset -= viewOffset; -+ } -+ if (index !== void 0) { -+ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -+ if (startOffsetAdjustment) { -+ offset += startOffsetAdjustment; -+ } -+ } -+ if (viewPosition !== void 0 && index !== void 0) { -+ const dataLength = state.props.data.length; -+ if (dataLength === 0) { -+ return offset; -+ } -+ const isOutOfBounds = index < 0 || index >= dataLength; -+ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -+ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -+ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -+ const trailingInset = getContentInsetEnd(ctx); -+ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -+ if (!isOutOfBounds && index === state.props.data.length - 1) { -+ const footerSize = peek$(ctx, "footerSize") || 0; -+ offset += footerSize; -+ } ++ const { data } = state.props; ++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; ++ if (index < 0 || index >= data.length || isEndAlignedLastItem) { ++ clearScrollTargetSettle(state); ++ return; + } -+ return offset; ++ state.scrollTargetSettle = { ++ expiresAt: Date.now() + SETTLE_TTL_MS, ++ id: getId(state, index), ++ quietPasses: 0, ++ viewOffset, ++ viewPosition ++ }; +} -+ -+// src/core/clampScrollOffset.ts -+function clampScrollOffset(ctx, offset, scrollTarget) { ++function settleScrollTarget(ctx) { ++ var _a3; + const state = ctx.state; -+ const contentSize = getContentSize(ctx); -+ let clampedOffset = offset; -+ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { -+ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); -+ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; -+ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; -+ const maxOffset = baseMaxOffset + extraEndOffset; -+ clampedOffset = Math.min(offset, maxOffset); ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ return false; + } -+ clampedOffset = Math.max(0, clampedOffset); -+ return clampedOffset; -+} -+ -+// src/utils/checkThreshold.ts -+var HYSTERESIS_MULTIPLIER = 1.3; -+function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { -+ const absDistance = Math.abs(distance); -+ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; -+} -+var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { -+ const absDistance = Math.abs(distance); -+ const within = atThreshold || threshold > 0 && absDistance <= threshold; -+ const updateSnapshot = () => { -+ setSnapshot({ -+ atThreshold, -+ contentSize: context.contentSize, -+ dataLength: context.dataLength, -+ scrollPosition: context.scrollPosition -+ }); -+ }; -+ if (!wasReached) { -+ if (!within) { -+ return false; -+ } -+ onReached(distance); -+ updateSnapshot(); -+ return true; ++ const index = state.indexByKey.get(settle.id); ++ const position = index === void 0 ? void 0 : state.positions[index]; ++ if (index === void 0 || position === void 0) { ++ clearScrollTargetSettle(state); ++ return false; + } -+ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); -+ if (reset) { -+ setSnapshot(void 0); ++ const now = Date.now(); ++ if (now > settle.expiresAt) { ++ clearScrollTargetSettle(state); + return false; + } -+ if (within) { -+ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; -+ if (changed) { -+ updateSnapshot(); ++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position); ++ const diff = targetOffset - state.scroll; ++ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { ++ settle.quietPasses++; ++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { ++ clearScrollTargetSettle(state); + } ++ return false; + } -+ return true; -+}; -+ -+// src/utils/edgeReachedGate.ts -+function resetEdgeLatch(ctx, edge) { -+ const state = ctx.state; -+ if (edge === "start") { -+ state.isStartReached = false; -+ state.startReachedSnapshot = void 0; -+ } else { -+ state.isEndReached = false; -+ state.endReachedSnapshot = void 0; -+ } -+} -+function resetSharedEdgeGateIfOutsideHysteresis(ctx) { -+ const state = ctx.state; -+ if (!state.edgeReachedGate) { -+ return; -+ } -+ const contentSize = getContentSize(ctx); -+ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); -+ const isContentLess = contentSize < state.scrollLength; -+ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; -+ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; -+ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); -+ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); -+ if (isOutsideStart && isOutsideEnd) { -+ state.edgeReachedGate = void 0; ++ settle.quietPasses = 0; ++ settle.expiresAt = now + SETTLE_TTL_MS; ++ if (((_a3 = state.scrollingTo) == null ? void 0 : _a3.index) === index) { ++ state.scrollingTo.offset = position; ++ state.scrollingTo.targetOffset = targetOffset; + } ++ requestAdjust(ctx, diff); ++ return true; + } + +-// src/core/getStartOffsetAdjustment.ts +-function getStartOffsetAdjustment(ctx) { +- const { state } = ctx; +- const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; +- return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); ++// src/core/cancelImperativeScroll.ts ++function cancelScrollCompletionChecks({ scheduledWork }) { ++ scheduledWork.cancel("checkFinishedScrollFrame"); ++ scheduledWork.cancel("checkFinishedScrollRetryFrame"); ++ scheduledWork.cancel("checkFinishedScrollFallback"); ++ scheduledWork.cancel("platformScrollCompletion"); +} -+function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { -+ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; -+} -+function markReachedEdge(ctx) { -+ ctx.state.edgeReachedGate = "closed"; -+} -+function prepareReachedEdgeForNextUserScroll(ctx) { -+ if (ctx.state.edgeReachedGate) { -+ ctx.state.edgeReachedGate = "prepared"; -+ } -+} -+function beginReachedEdgeUserScroll(ctx, scrollDelta) { -+ const state = ctx.state; -+ if (state.edgeReachedGate !== "prepared") { -+ return void 0; -+ } -+ const allowedEdge = scrollDelta < 0 ? "start" : "end"; -+ state.edgeReachedGate = "closed"; -+ resetEdgeLatch(ctx, allowedEdge); -+ return allowedEdge; -+} -+ -+// src/utils/hasActiveInitialScroll.ts -+function hasActiveInitialScroll(state) { -+ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; ++function settlePendingImperativeScroll(state) { ++ var _a3, _b; ++ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; ++ state.pendingScrollResolve = void 0; ++ state.pendingScrollToEnd = void 0; ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +} -+ -+// src/utils/checkAtBottom.ts -+function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -+ var _a3; -+ const state = ctx.state; -+ if (!state) { -+ return; -+ } -+ const { -+ queuedInitialLayout, -+ scrollLength, -+ scroll, -+ maintainingScrollAtEnd, -+ props: { maintainScrollAtEndThreshold, onEndReachedThreshold } -+ } = state; -+ const contentSize = getContentSize(ctx); -+ resetSharedEdgeGateIfOutsideHysteresis(ctx); -+ if (contentSize > 0 && queuedInitialLayout) { -+ const insetEnd = getContentInsetEnd(ctx); -+ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; -+ const isContentLess = contentSize < scrollLength; -+ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); -+ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); -+ set$( -+ ctx, -+ "isWithinMaintainScrollAtEndThreshold", -+ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength -+ ); -+ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; -+ if (!shouldSkipThresholdChecks) { -+ state.isEndReached = checkThreshold( -+ distanceFromEnd, -+ isContentLess, -+ onEndReachedThreshold * scrollLength, -+ state.isEndReached, -+ state.endReachedSnapshot, -+ { -+ contentSize, -+ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, -+ scrollPosition: scroll -+ }, -+ (distance) => { -+ var _a4, _b; -+ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { -+ markReachedEdge(ctx); -+ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); -+ } -+ }, -+ (snapshot) => { -+ state.endReachedSnapshot = snapshot; -+ } -+ ); ++function cancelImperativeScroll(state) { ++ cancelScrollCompletionChecks(state); ++ state.scheduledWork.cancel("imperativeScrollReady"); ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ clearScrollTargetSettle(state); ++ settlePendingImperativeScroll(state); + } + +-// src/utils/getId.ts +-function getId(state, index) { +- const { data, keyExtractor } = state.props; +- if (!data) { +- return ""; ++// src/core/deferredPublicOnScroll.ts ++function withResolvedContentOffset(state, event, resolvedOffset) { ++ return { ++ ...event, ++ nativeEvent: { ++ ...event.nativeEvent, ++ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } + } -+ } ++ }; +} -+ -+// src/utils/checkAtTop.ts -+function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -+ const state = ctx == null ? void 0 : ctx.state; -+ if (!state) { -+ return; -+ } -+ const { -+ isStartReached, -+ props: { data, onStartReachedThreshold }, -+ scroll, -+ scrollLength, -+ startReachedSnapshot, -+ totalSize -+ } = state; -+ const dataLength = data.length; -+ const threshold = onStartReachedThreshold * scrollLength; -+ resetSharedEdgeGateIfOutsideHysteresis(ctx); -+ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { -+ state.isStartReached = false; -+ state.startReachedSnapshot = void 0; -+ } -+ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); -+ set$(ctx, "isNearStart", scroll <= threshold); -+ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; -+ if (!shouldSkipThresholdChecks) { -+ state.isStartReached = checkThreshold( -+ scroll, -+ false, -+ threshold, -+ state.isStartReached, -+ startReachedSnapshot, -+ { -+ contentSize: totalSize, -+ dataLength, -+ scrollPosition: scroll -+ }, -+ (distance) => { -+ var _a3, _b; -+ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { -+ markReachedEdge(ctx); -+ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); -+ } -+ }, -+ (snapshot) => { -+ state.startReachedSnapshot = snapshot; -+ } ++function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { ++ var _a3, _b, _c, _d; ++ const state = ctx.state; ++ const deferredEvent = state.deferredPublicOnScrollEvent; ++ state.deferredPublicOnScrollEvent = void 0; ++ if (deferredEvent) { ++ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( ++ _c, ++ withResolvedContentOffset( ++ state, ++ deferredEvent, ++ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 ++ ) + ); -+ } - } - --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; -- } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } -- } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } -- } -- return offset; -+// src/utils/checkThresholds.ts -+function checkThresholds(ctx, allowedEdge) { -+ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; -+ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); - } - --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { -+// src/core/recalculateSettledScroll.ts -+function recalculateSettledScroll(ctx) { -+ var _a3, _b; - const state = ctx.state; -- const contentSize = getContentSize(ctx); -- let clampedOffset = offset; -- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { -- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); -- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; -- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; -- const maxOffset = baseMaxOffset + extraEndOffset; -- clampedOffset = Math.min(offset, maxOffset); -+ if ((_a3 = state.props) == null ? void 0 : _a3.data) { -+ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); } -- clampedOffset = Math.max(0, clampedOffset); -- return clampedOffset; -+ checkThresholds(ctx); - } - - // src/core/finishScrollTo.ts -@@ -1425,6 +1041,181 @@ function listenForScrollEnd(ctx, params) { - scheduledWork.register("platformScrollCompletion", cancel); +- const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; +- const id = ret; +- state.idCache[index] = id; +- return id; } +-// src/core/updateContentMetricsState.ts +-function getRawContentLength(ctx) { +- var _a3, _b, _c; +- const { state, values } = ctx; +- return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); +// src/core/initialScrollSession.ts +var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; +function hasInitialScrollSessionCompletion(completion) { + return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); -+} + } +-function getAlignItemsAtEndPadding(ctx) { +- const { state } = ctx; +- const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; +- return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; +function clearInitialScrollSession(state) { + state.initialScrollSession = void 0; + return void 0; -+} + } +-function updateContentMetricsState(ctx) { +- var _a3; +- const { state } = ctx; +- const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; +- const nextPadding = getAlignItemsAtEndPadding(ctx); +- if (previousPadding !== nextPadding) { +- const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated"; +- const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold")); +- if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) { +- state.scheduledWork.microtask(() => { +- if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) { +- set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx)); +- } +- }, "alignItemsAtEndPaddingFallback"); +- } else if (!isMaintainExpected) { +- set$(ctx, "alignItemsAtEndPadding", nextPadding); +- } +function createInitialScrollSession(options) { + const { bootstrap, completion, kind, previousDataLength } = options; + return kind === "offset" ? { @@ -7619,10 +8154,21 @@ index 914d2da..ccb19f4 100644 + kind, + previousDataLength: state.initialScrollSession.previousDataLength + }); -+ } + } + (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; + return state.initialScrollSession.completion; -+} + } +- +-// src/core/addTotalSize.ts +-function addTotalSize(ctx, key, add, notifyTotalSize = true) { +- const state = ctx.state; +- const prevTotalSize = state.totalSize; +- let totalSize = state.totalSize; +- if (key === null) { +- totalSize = add; +- if (state.timeoutSetPaddingTop) { +- clearTimeout(state.timeoutSetPaddingTop); +- state.timeoutSetPaddingTop = void 0; +var initialScrollCompletion = { + didDispatchNativeScroll(state) { + var _a3, _b; @@ -7641,11 +8187,21 @@ index 914d2da..ccb19f4 100644 + resetFlags(state) { + if (!state.initialScrollSession) { + return; -+ } + } +- } else { +- totalSize += add; + const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); + completion.didDispatchNativeScroll = void 0; + completion.didRetrySilentInitialScroll = void 0; -+ } + } +- if (prevTotalSize !== totalSize) { +- { +- state.pendingTotalSize = void 0; +- state.totalSize = totalSize; +- if (notifyTotalSize) { +- set$(ctx, "totalSize", totalSize); +- } +- updateContentMetricsState(ctx); +}; +var initialScrollWatchdog = { + clear(state) { @@ -7669,7 +8225,9 @@ index 914d2da..ccb19f4 100644 + var _a3, _b; + if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { + return; -+ } + } +- } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { +- set$(ctx, "totalSize", totalSize); + const completion = ensureInitialScrollSessionCompletion(state); + completion.watchdog = watchdog ? { + startScroll: watchdog.startScroll, @@ -7686,10 +8244,26 @@ index 914d2da..ccb19f4 100644 + const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; + if (!kind) { + return clearInitialScrollSession(state); -+ } + } +-} +- +-// src/core/setSize.ts +-function setSize(ctx, itemKey, size, notifyTotalSize = true) { +- const state = ctx.state; +- const { sizes } = state; +- const previousSize = sizes.get(itemKey); +- const diff = previousSize !== void 0 ? size - previousSize : size; +- if (diff !== 0) { +- addTotalSize(ctx, itemKey, diff, notifyTotalSize); + if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { + return clearInitialScrollSession(state); -+ } + } +- sizes.set(itemKey, size); +-} +- +-// src/utils/helpers.ts +-function isFunction(obj) { +- return typeof obj === "function"; + const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; + state.initialScrollSession = createInitialScrollSession({ + bootstrap, @@ -7698,13 +8272,20 @@ index 914d2da..ccb19f4 100644 + previousDataLength + }); + return state.initialScrollSession; -+} + } +-function isArray(obj) { +- return Array.isArray(obj); +var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; +var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; +var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; +function clearAdaptiveRenderExitTimeout(ctx) { + ctx.state.scheduledWork.cancel("adaptiveRender"); -+} + } +-var warned = /* @__PURE__ */ new Set(); +-function warnDevOnce(id, text) { +- if (IS_DEV && !warned.has(id)) { +- warned.add(id); +- console.warn(`[legend-list] ${text}`); +function scheduleAdaptiveRenderExit(ctx, exitDelay) { + const state = ctx.state; + clearAdaptiveRenderExitTimeout(ctx); @@ -7712,18 +8293,47 @@ index 914d2da..ccb19f4 100644 + setAdaptiveRender(ctx, "normal", "scroll"); + } else { + state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); -+ } -+} + } + } +-function roundSize(size) { +- return Math.floor(size * 8) / 8; +-} +-function isNullOrUndefined(value) { +- return value === null || value === void 0; +-} +-function getPadding(s, type) { +- var _a3, _b, _c; +- const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; +- return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; +-} +-function extractPadding(style, contentContainerStyle, type) { +- return getPadding(style, type) + getPadding(contentContainerStyle, type); +-} +-function findContainerId(ctx, key) { +function setAdaptiveRender(ctx, mode, reason) { -+ var _a3, _b; + var _a3, _b; +- const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); +- if (directMatch !== void 0) { +- return directMatch; +- } +- const numContainers = peek$(ctx, "numContainers"); +- for (let i = 0; i < numContainers; i++) { +- const itemKey = peek$(ctx, `containerItemKey${i}`); +- if (itemKey === key) { +- return i; +- } + const previousMode = peek$(ctx, "adaptiveRender"); + if (previousMode !== mode) { + set$(ctx, "adaptiveRender", mode); + (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); -+ } -+} + } +- return -1; + } +- +-// src/utils/getItemSize.ts +-function getKnownOrFixedSize(ctx, key, index, data, resolved) { +function resetAdaptiveRender(ctx) { -+ var _a3, _b; + var _a3, _b; + clearAdaptiveRenderExitTimeout(ctx); + const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; + if (peek$(ctx, "adaptiveRender") !== mode) { @@ -7732,7 +8342,15 @@ index 914d2da..ccb19f4 100644 +} +function updateAdaptiveRender(ctx, scrollVelocity, options) { + var _a3, _b, _c; -+ const state = ctx.state; + const state = ctx.state; +- const { getFixedItemSize, getItemType } = state.props; +- let size = key ? state.sizesKnown.get(key) : void 0; +- if (size === void 0 && key && getFixedItemSize) { +- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; +- const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); +- if (fixedSize !== void 0) { +- size = fixedSize + ctx.scrollAxisGap; +- state.sizesKnown.set(key, size); + const adaptiveRender = state.props.adaptiveRender; + const currentMode = peek$(ctx, "adaptiveRender"); + if (peek$(ctx, "readyToRender")) { @@ -7753,31 +8371,14 @@ index 914d2da..ccb19f4 100644 + } + } else { + resetAdaptiveRender(ctx); -+ } -+ } -+} -+ - // src/core/doMaintainScrollAtEnd.ts - function doMaintainScrollAtEnd(ctx) { - const state = ctx.state; -@@ -1451,9 +1242,12 @@ function doMaintainScrollAtEnd(ctx) { - const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; - const scrollAtRequest = state.scroll; - state.maintainingScrollAtEnd = pendingState; -+ clearScrollTargetSettle(state); - requestAnimationFrame(() => { - const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); -- const didScrollSinceRequest = state.scroll !== scrollAtRequest; -+ const lastIssued = state.lastIssuedScrollOffset; -+ const isSettlingOntoIssuedScroll = lastIssued !== void 0 && Math.abs(state.scroll - lastIssued) <= EDGE_POSITION_EPSILON; -+ const didScrollSinceRequest = state.scroll !== scrollAtRequest && !isSettlingOntoIssuedScroll; - if (isStillWithinThreshold || !didScrollSinceRequest) { - state.maintainingScrollAtEnd = activeState; - const scroller = refScroller.current; -@@ -1809,6 +1603,27 @@ function prepareMVCP(ctx, dataChanged) { + } } +- return size; } - +-function getKnownOrFixedItemSize(ctx, index) { +- const key = getId(ctx.state, index); +- return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); ++ +// src/utils/getEffectiveDrawDistance.ts +var INITIAL_DRAW_DISTANCE = 50; +function getEffectiveDrawDistance(ctx, mode) { @@ -7787,254 +8388,100 @@ index 914d2da..ccb19f4 100644 + const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; + const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; + return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; -+} + } +-function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { +- for (let index = startIndex; index <= endIndex; index++) { +- if (getKnownOrFixedItemSize(ctx, index) === void 0) { +- return false; +- } +function scheduleFullDrawDistancePrewarm(ctx) { + const { state } = ctx; + if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { + return; -+ } + } +- return true; + state.scheduledWork.frame(() => { + var _a3; + return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); + }, "fullDrawDistancePrewarm"); -+} + } +-function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { +- var _a3, _b, _c, _d; +- const state = ctx.state; +- const { +- sizes, +- averageSizes, +- props: { estimatedItemSize, getItemType }, +- scrollingTo +- } = state; +- const sizeKnown = state.sizesKnown.get(key); +- if (sizeKnown !== void 0) { +- return sizeKnown; +- } +- let size; +- const renderedSize = sizes.get(key); +- if (preferCachedSize) { +- if (renderedSize !== void 0) { +- return renderedSize; +- } +- } +- size = getKnownOrFixedSize(ctx, key, index, data, resolved); +- if (size !== void 0) { +- setSize(ctx, key, size, notifyTotalSize); +- return size; +- } +- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; +- if (useAverageSize && !scrollingTo) { +- const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; +- if (averageSizeForType !== void 0) { +- size = roundSize(averageSizeForType); +- } +- } +- if (size === void 0 && renderedSize !== void 0) { +- return renderedSize; +- } +- if (size === void 0 && useAverageSize && scrollingTo) { +- const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; +- if (averageSizeForType !== void 0) { +- size = roundSize(averageSizeForType); +- } +- } +- if (size === void 0) { +- size = estimatedItemSize + ctx.scrollAxisGap; + - // src/utils/getScrollVelocity.ts - var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; - var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2069,7 +1884,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { ++// src/utils/setInitialRenderState.ts ++function resetInitialRenderState(ctx, { ++ resetLayout, ++ resetInitialScroll ++}) { ++ const { state } = ctx; ++ if (resetLayout) { ++ state.didContainersLayout = false; ++ state.queuedInitialLayout = false; } - } - function scrollTo(ctx, params) { -- var _a3, _b; -+ var _a3, _b, _c; - const state = ctx.state; - const { noScrollingTo, forceScroll, ...scrollTarget } = params; - const { -@@ -2091,6 +1906,7 @@ function scrollTo(ctx, params) { - if (!noScrollingTo) { - if (isInitialScroll) { - initialScrollCompletion.resetFlags(state); -+ clearScrollTargetSettle(state); - } - const averageSizeSnapshot = getAverageSizeSnapshot(state); - state.scrollingTo = { -@@ -2101,6 +1917,15 @@ function scrollTo(ctx, params) { - }; - if (!isInitialScroll) { - pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); -+ if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { -+ beginScrollTargetSettle(ctx, { -+ index: scrollTarget.index, -+ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, -+ viewPosition: scrollTarget.viewPosition -+ }); -+ } else { -+ clearScrollTargetSettle(state); -+ } - } +- setSize(ctx, key, size, notifyTotalSize); +- return size; +-} +-function getItemSizeAtIndex(ctx, index) { +- if (index === void 0 || index < 0) { +- return void 0; ++ if (resetInitialScroll) { ++ state.didFinishInitialScroll = false; } - state.scrollPending = targetOffset; -@@ -2108,7 +1933,7 @@ function scrollTo(ctx, params) { - if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { - if (animated) { - if (state.scrollTargetPinnedRange) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); -+ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); - } - } else { - updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2121,6 +1946,316 @@ function scrollTo(ctx, params) { - } - } - -+// src/core/scrollTargetSettle.ts -+var SETTLE_POSITION_EPSILON = 1; -+var SETTLE_QUIET_PASSES_TO_RELEASE = 2; -+var SETTLE_MAX_MS = 1e3; -+var SETTLE_MAX_CORRECTIONS = 8; -+function clearScrollTargetSettle(state) { -+ state.scrollTargetSettle = void 0; -+ state.scheduledWork.cancel("scrollTargetSettle"); -+ state.scheduledWork.cancel("scrollTargetSettleDeadline"); -+} -+function beginScrollTargetSettle(ctx, params) { -+ const state = ctx.state; -+ const { index, viewOffset, viewPosition } = params; -+ const { alignItemsAtEnd, data, maintainScrollAtEnd } = state.props; -+ const hasEndAnchoring = !!maintainScrollAtEnd || !!alignItemsAtEnd; -+ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; -+ if (index < 0 || index >= data.length || isEndAlignedLastItem && hasEndAnchoring) { -+ clearScrollTargetSettle(state); -+ return; -+ } -+ clearScrollTargetSettle(state); -+ const now = Date.now(); -+ state.scrollTargetSettle = { -+ corrections: 0, -+ deadline: now + SETTLE_MAX_MS, -+ id: getId(state, index), -+ measuredIndex: void 0, -+ quietPasses: 0, -+ viewOffset, -+ viewPosition -+ }; -+ state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline"); -+} -+function getSettleTargetOffset(ctx, settle, index, position) { -+ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; -+ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); -+} -+function applyScrollTargetCorrection(ctx, id) { -+ var _a3; -+ const state = ctx.state; -+ const settle = state.scrollTargetSettle; -+ if (!settle || settle.id !== id) { -+ return; -+ } -+ const index = state.indexByKey.get(id); -+ const position = index === void 0 ? void 0 : state.positions[index]; -+ if (index === void 0 || position === void 0 || Date.now() > settle.deadline) { -+ clearScrollTargetSettle(state); -+ return; -+ } -+ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { -+ clearScrollTargetSettle(state); -+ return; -+ } -+ settle.corrections++; -+ settle.measuredIndex = void 0; -+ scrollTo(ctx, { -+ animated: false, -+ index, -+ itemSize: getItemSizeAtIndex(ctx, index), -+ // Correcting where a scroll already landed is not a new imperative scroll: claiming the -+ // session would suppress the list's own handling of subsequent scroll events and re-arm -+ // this settle from inside its own correction. -+ noScrollingTo: true, -+ offset: position, -+ viewOffset: settle.viewOffset, -+ viewPosition: settle.viewPosition -+ }); -+ const scrollingTo = state.scrollingTo; -+ if (scrollingTo) { -+ scrollingTo.targetOffset = state.scrollPending; -+ scrollingTo.offset = position; -+ } -+ (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); -+} -+function settleScrollTarget(ctx, options) { -+ var _a3; -+ const state = ctx.state; -+ const settle = state.scrollTargetSettle; -+ if (!settle) { -+ return false; -+ } -+ const measured = options == null ? void 0 : options.minIndexSizeChanged; -+ if (measured !== void 0) { -+ settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); -+ } -+ if (options == null ? void 0 : options.isCompensating) { -+ if (Date.now() > settle.deadline) { -+ clearScrollTargetSettle(state); -+ return false; -+ } -+ settle.quietPasses = 0; -+ return false; -+ } -+ const index = state.indexByKey.get(settle.id); -+ const position = index === void 0 ? void 0 : state.positions[index]; -+ if (index === void 0 || position === void 0) { -+ clearScrollTargetSettle(state); -+ return false; -+ } -+ if (Date.now() > settle.deadline) { -+ clearScrollTargetSettle(state); -+ return false; -+ } -+ if (settle.measuredIndex === void 0 || settle.measuredIndex > index) { -+ return false; -+ } -+ const targetOffset = getSettleTargetOffset(ctx, settle, index, position); -+ if (Math.abs(targetOffset - state.scroll) <= SETTLE_POSITION_EPSILON) { -+ settle.measuredIndex = void 0; -+ settle.quietPasses++; -+ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { -+ clearScrollTargetSettle(state); -+ } -+ return false; -+ } -+ settle.quietPasses = 0; -+ state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); -+ return true; -+} -+ -+// src/core/cancelImperativeScroll.ts -+function cancelScrollCompletionChecks({ scheduledWork }) { -+ scheduledWork.cancel("checkFinishedScrollFrame"); -+ scheduledWork.cancel("checkFinishedScrollRetryFrame"); -+ scheduledWork.cancel("checkFinishedScrollFallback"); -+ scheduledWork.cancel("platformScrollCompletion"); -+} -+function settlePendingImperativeScroll(state) { -+ var _a3, _b; -+ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve; -+ state.pendingScrollResolve = void 0; -+ state.pendingScrollToEnd = void 0; -+ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); -+} -+function cancelImperativeScroll(state) { -+ cancelScrollCompletionChecks(state); -+ state.scheduledWork.cancel("imperativeScrollReady"); -+ state.scrollingTo = void 0; -+ state.scrollTargetPinnedRange = void 0; -+ clearScrollTargetSettle(state); -+ settlePendingImperativeScroll(state); -+} -+ -+// src/core/deferredPublicOnScroll.ts -+function withResolvedContentOffset(state, event, resolvedOffset) { -+ return { -+ ...event, -+ nativeEvent: { -+ ...event.nativeEvent, -+ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -+ } -+ }; -+} -+function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { -+ var _a3, _b, _c, _d; -+ const state = ctx.state; -+ const deferredEvent = state.deferredPublicOnScrollEvent; -+ state.deferredPublicOnScrollEvent = void 0; -+ if (deferredEvent) { -+ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call( -+ _c, -+ withResolvedContentOffset( -+ state, -+ deferredEvent, -+ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 -+ ) -+ ); -+ } -+} -+ -+// src/utils/setInitialRenderState.ts -+function resetInitialRenderState(ctx, { -+ resetLayout, -+ resetInitialScroll -+}) { -+ const { state } = ctx; -+ if (resetLayout) { -+ state.didContainersLayout = false; -+ state.queuedInitialLayout = false; -+ } -+ if (resetInitialScroll) { -+ state.didFinishInitialScroll = false; -+ } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); + set$(ctx, "readyToRender", false); + resetAdaptiveRender(ctx); -+} + } +- +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +function setInitialRenderState(ctx, { + didLayout, + didInitialScroll @@ -8046,10 +8493,29 @@ index 914d2da..ccb19f4 100644 + } = state; + if (didLayout) { + state.didContainersLayout = true; -+ } + } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } + if (didInitialScroll) { + state.didFinishInitialScroll = true; -+ } + } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); + if (isReadyToRender && !peek$(ctx, "readyToRender")) { + set$(ctx, "readyToRender", true); @@ -8062,17 +8528,36 @@ index 914d2da..ccb19f4 100644 + if (onLoad) { + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); + } -+ } -+ } -+} -+ + } + } +- return offset; + } + +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +- const state = ctx.state; +- const contentSize = getContentSize(ctx); +- let clampedOffset = offset; +- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { +- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); +- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; +- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; +- const maxOffset = baseMaxOffset + extraEndOffset; +- clampedOffset = Math.min(offset, maxOffset); +- } +- clampedOffset = Math.max(0, clampedOffset); +- return clampedOffset; +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; +function syncInitialScrollOffset(state, offset) { + state.scroll = offset; + state.scrollPending = offset; + state.scrollPrev = offset; -+} + } +- +-// src/core/finishScrollTo.ts +-function finishScrollTo(ctx) { +- var _a3, _b; +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -8084,32 +8569,134 @@ index 914d2da..ccb19f4 100644 +} +function supersedeInitialScroll(ctx) { + var _a3, _b, _c; -+ const state = ctx.state; + const state = ctx.state; +- if (state == null ? void 0 : state.scrollingTo) { +- cancelScrollCompletionChecks(state); +- const resolvePendingScroll = state.pendingScrollResolve; +- state.pendingScrollResolve = void 0; +- const scrollingTo = state.scrollingTo; +- state.scrollHistory.length = 0; +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- if (state.pendingTotalSize !== void 0) { +- addTotalSize(ctx, null, state.pendingTotalSize); +- } +- { +- state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); + const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; + if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { + if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { + cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -+ } + } +- if (scrollingTo.isInitialScroll || state.initialScroll) { +- const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; +- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; +- finishInitialScroll(ctx, { +- onFinished: () => { +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +- }, +- preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, +- recalculateItems: true, +- schedulePreservedTargetClear: shouldPreserveResizeTarget, +- syncObservedOffset: isOffsetSession, +- waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame +- }); +- return; + if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { + cancelScrollCompletionChecks(state); + state.scrollingTo = void 0; + state.scrollTargetPinnedRange = void 0; -+ } + } +- recalculateSettledScroll(ctx); +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); + initialScrollCompletion.resetFlags(state); + setInitialScrollSession(state, { bootstrap: null }); + finishInitialScroll(ctx); -+ } -+} + } + } +- +-// src/core/doScrollTo.ts +-var SCROLL_END_IDLE_MS = 80; +-var SCROLL_END_MAX_MS = 1500; +-var SMOOTH_SCROLL_DURATION_MS = 320; +-var SCROLL_END_TARGET_EPSILON = 1; +-function doScrollTo(ctx, params) { +- var _a3, _b; +function finishInitialScroll(ctx, options) { + var _a3, _b, _c; -+ const state = ctx.state; + const state = ctx.state; +- const { animated, horizontal, offset } = params; +- state.scheduledWork.cancel("platformScrollCompletion"); +- const scroller = state.refScroller.current; +- const node = scroller == null ? void 0 : scroller.getScrollableNode(); +- if (!scroller || !node) { +- return; +- } +- const isAnimated = !!animated; +- const isHorizontal = !!horizontal; +- const contentSize = isHorizontal ? getContentSize(ctx) : void 0; +- const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; +- const top = isHorizontal ? 0 : offset; +- scroller.scrollTo({ animated: isAnimated, x: left, y: top }); +- if (isAnimated) { +- const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; +- listenForScrollEnd(ctx, { +- readOffset: () => scroller.getCurrentScrollOffset(), +- target, +- targetOffset: offset +- }); +- } else { +- state.scroll = offset; +- const targetToken = state.scrollingTo; +- state.scheduledWork.timeout( +- () => { +- if (targetToken === state.scrollingTo) { +- finishScrollTo(ctx); +- } +- }, +- 100, +- "platformScrollCompletion" +- ); +- } +-} +-function listenForScrollEnd(ctx, params) { +- const { readOffset, target, targetOffset } = params; +- if (!target) { +- finishScrollTo(ctx); +- return; +- } +- const supportsScrollEnd = "onscrollend" in target; +- let idleTimeout; +- let settled = false; +- const { scheduledWork, scrollingTo: targetToken } = ctx.state; +- const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); +- const cleanup = () => { +- target.removeEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.removeEventListener("scrollend", onScrollEnd); +- } +- if (idleTimeout !== void 0) { +- clearTimeout(idleTimeout); +- } +- clearTimeout(maxTimeout); +- }; +- const cancel = () => { +- if (!settled) { +- settled = true; +- cleanup(); + if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { + syncInitialScrollOffset(state, options.resolvedOffset); + } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { + const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); + if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { + syncInitialScrollOffset(state, observedOffset); -+ } + } +- }; +- const finish = (reason) => { +- if (settled) return; +- if (targetToken !== ctx.state.scrollingTo) { +- scheduledWork.cancel("platformScrollCompletion"); +- return; + } + const complete = () => { + var _a4, _b2, _c2, _d, _e; @@ -8135,20 +8722,38 @@ index 914d2da..ccb19f4 100644 + } + } else { + clearPreservedInitialScrollTarget(state); -+ } + } +- const currentOffset = readOffset(); +- const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; +- if (reason === "scrollend" && !isNearTarget) { +- return; + if (options == null ? void 0 : options.recalculateItems) { + recalculateSettledScroll(ctx); -+ } + } +- scheduledWork.cancel("platformScrollCompletion"); +- finishScrollTo(ctx); +- }; +- const onScroll2 = () => { +- if (idleTimeout !== void 0) { +- clearTimeout(idleTimeout); + setInitialRenderState(ctx, { didInitialScroll: true }); + if (shouldReleaseDeferredPublicOnScroll) { + releaseDeferredPublicOnScroll(ctx, finalScrollOffset); -+ } + } +- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); + (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); -+ }; + }; +- const onScrollEnd = () => finish("scrollend"); +- target.addEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.addEventListener("scrollend", onScrollEnd); +- } else { +- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); + if (options == null ? void 0 : options.waitForCompletionFrame) { + requestAnimationFrame(complete); + return; -+ } + } +- scheduledWork.register("platformScrollCompletion", cancel); + complete(); +} + @@ -8156,178 +8761,89 @@ index 914d2da..ccb19f4 100644 +function calculateOffsetForIndex(ctx, index) { + const state = ctx.state; + return index !== void 0 ? state.positions[index] || 0 : 0; -+} -+ - // src/core/scrollToIndex.ts - function clampScrollIndex(index, dataLength) { - if (dataLength <= 0) { -@@ -4350,6 +4485,7 @@ function calculateItemsInView(ctx, params = {}) { - startIndex - }); - totalSize = getContentSize(ctx); -+ const minIndexSizeChangedThisPass = minIndexSizeChanged; - if (minIndexSizeChanged !== void 0) { - state.minIndexSizeChanged = void 0; + } + + // src/core/scrollRequestTracker.ts +@@ -1483,30 +1573,6 @@ function getScrollRequestTracker(ctx) { + return ctx.scrollRequestTracker; + } + +-// src/utils/requestAdjust.ts +-function requestAdjust(ctx, positionDiff, source) { +- const state = ctx.state; +- if (Math.abs(positionDiff) > 0.1) { +- const doit = () => { +- { +- state.scrollAdjustHandler.requestAdjust(positionDiff); +- if (state.adjustingFromInitialMount) { +- state.adjustingFromInitialMount--; +- } +- } +- }; +- state.scroll += positionDiff; +- state.scrollForNextCalculateItemsInView = void 0; +- const readyToRender = peek$(ctx, "readyToRender"); +- if (readyToRender) { +- doit(); +- } else { +- state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; +- requestAnimationFrame(doit); +- } +- } +-} +- + // src/core/doMaintainScrollAtEnd.ts + function finishMaintainScrollAtEnd(ctx) { + var _a3; +@@ -2141,7 +2207,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + } + } + function scrollTo(ctx, params) { +- var _a3, _b; ++ var _a3, _b, _c; + const state = ctx.state; + const { noScrollingTo, forceScroll, ...scrollTarget } = params; + const { +@@ -2173,6 +2239,15 @@ function scrollTo(ctx, params) { + }; + if (!isInitialScroll) { + pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); ++ if (scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { ++ beginScrollTargetSettle(ctx, { ++ index: scrollTarget.index, ++ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, ++ viewPosition: scrollTarget.viewPosition ++ }); ++ } else { ++ clearScrollTargetSettle(state); ++ } } -@@ -4367,8 +4503,18 @@ function calculateItemsInView(ctx, params = {}) { + } + state.scrollPending = targetOffset; +@@ -2180,7 +2255,7 @@ function scrollTo(ctx, params) { + if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { + if (animated) { + if (state.scrollTargetPinnedRange) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); + } + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); +@@ -4444,7 +4519,8 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); - const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); -- if (didMVCPAdjustScroll) { -+ const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; -+ const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); -+ const mvcp = state.props.maintainVisibleContentPosition; -+ const isUnanchoredDataChange = dataChanged && !mvcp.data; -+ const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; -+ if (!suppressInitialScrollSideEffects) { -+ settleScrollTarget(ctx, { -+ isCompensating, -+ minIndexSizeChanged: isUnanchoredDataChange ? 0 : minIndexSizeChangedThisPass -+ }); -+ } -+ if (didMVCPAdjust) { ++ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx); ++ const didMVCPAdjustScroll = didSettleScrollTarget || !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); + if (didMVCPAdjustScroll) { updateScroll2(state.scroll); updateScrollRange(); - } -@@ -6006,6 +6152,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll - // src/components/ListComponentScrollView.tsx - var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; - var SCROLL_END_FALLBACK_MS = 200; -+var SCROLL_EXTENT_EPSILON = 1; -+var REACHABLE_RETRY_FRAMES = 30; - var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; - function ensureScrollbarHiddenStyle() { - if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6094,6 +6242,26 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - } - return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; - }, [getMaxScrollOffset, horizontal, isWindowScroll]); -+ const reissueHandleRef = React3.useRef(0); -+ const scrollUntilReachable = React3.useCallback( -+ (offset, animated, run) => { -+ cancelAnimationFrame(reissueHandleRef.current); -+ let attempts = 0; -+ let previousMaxOffset = Number.NEGATIVE_INFINITY; -+ const attempt = () => { -+ const liveMaxOffset = getMaxScrollOffset(); -+ run(clampOffset(offset, liveMaxOffset)); -+ const isStillCommitting = liveMaxOffset > previousMaxOffset; -+ previousMaxOffset = liveMaxOffset; -+ if (!animated && isStillCommitting && Number.isFinite(offset) && offset - liveMaxOffset > SCROLL_EXTENT_EPSILON && ++attempts < REACHABLE_RETRY_FRAMES) { -+ reissueHandleRef.current = requestAnimationFrame(attempt); -+ } -+ }; -+ attempt(); -+ }, -+ [getMaxScrollOffset] -+ ); -+ React3.useEffect(() => () => cancelAnimationFrame(reissueHandleRef.current), []); - const scrollToLocalOffset = React3.useCallback( - (offset, animated) => { - const scrollElement = scrollRef.current; -@@ -6101,29 +6269,34 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - if (!target || typeof target.scrollTo !== "function") { - return; - } -- const maxOffset = getMaxScrollOffset(); -- const clampedOffset = clampOffset(offset, maxOffset); - const behavior = animated ? "smooth" : "auto"; - const options = { behavior }; - if (isWindowScroll) { - const scroll = getWindowScrollPosition(); - const listPos = getElementDocumentPosition(scrollElement, scroll); - const { left, top } = resolveWindowScrollTarget({ -- clampedOffset, -+ clampedOffset: clampOffset(offset, getMaxScrollOffset()), - horizontal, - listPos, - scroll - }); - options.left = left; - options.top = top; -- } else if (horizontal) { -- options.left = clampedOffset; -- } else { -- options.top = clampedOffset; -+ ctx.state.lastIssuedScrollOffset = clampOffset(offset, getMaxScrollOffset()); -+ target.scrollTo(options); -+ return; - } -- target.scrollTo(options); -+ scrollUntilReachable(offset, animated, (reachableOffset) => { -+ if (horizontal) { -+ options.left = reachableOffset; -+ } else { -+ options.top = reachableOffset; -+ } -+ ctx.state.lastIssuedScrollOffset = reachableOffset; -+ target.scrollTo(options); -+ }); - }, -- [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] -+ [ctx, getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable] - ); - React3.useImperativeHandle(ref, () => { - const api = { -@@ -6149,8 +6322,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView - }, - scrollToEnd: (options = {}) => { - const { animated = true } = options; -- const endOffset = getMaxScrollOffset(); -- scrollToLocalOffset(endOffset, animated); -+ scrollToLocalOffset(getMaxScrollOffset(), animated); - }, - scrollToOffset: (params) => { - const { offset, animated = true } = params; -@@ -6499,6 +6671,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize - return props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; - } - function setHeaderSize(ctx, size) { -+ var _a3; - const { state } = ctx; - const previousHeaderSize = peek$(ctx, "headerSize") || 0; - const didChange = previousHeaderSize !== size; -@@ -6508,6 +6681,8 @@ function setHeaderSize(ctx, size) { - updateContentMetricsState(ctx); - if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { - requestAdjust(ctx, size - previousHeaderSize); -+ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) { -+ doMaintainScrollAtEnd(ctx); - } - } - state.didMeasureHeader = true; -@@ -7570,13 +7745,14 @@ function getRenderedItem(ctx, key) { - - // src/utils/normalizeMaintainScrollAtEnd.ts - function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) { -- var _a3, _b, _c, _d; -+ var _a3, _b, _c, _d, _e; - return { - animated: false, - onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true, - onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true, -- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true, -- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true -+ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true, -+ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true, -+ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true - }; - } - function normalizeMaintainScrollAtEnd(value) { -@@ -8288,6 +8464,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded - onScrollBeginDrag: (event) => { - var _a4, _b2; - prepareReachedEdgeForNextUserScroll(ctx); -+ clearScrollTargetSettle(state); - (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); - }, - onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs -index 95465f2..4fb1e41 100644 +index a97be05..a1e2968 100644 --- a/node_modules/@legendapp/list/react.mjs +++ b/node_modules/@legendapp/list/react.mjs -@@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1; +@@ -409,171 +409,271 @@ var EDGE_POSITION_EPSILON = 1; var ENABLE_DEVMODE = IS_DEV && false; var ENABLE_DEBUG_VIEW = IS_DEV && false; @@ -8344,15 +8860,31 @@ index 95465f2..4fb1e41 100644 - state.pendingScrollResolve = void 0; - state.pendingScrollToEnd = void 0; - resolvePendingScroll == null ? void 0 : resolvePendingScroll(); --} ++// src/core/getStartOffsetAdjustment.ts ++function getStartOffsetAdjustment(ctx) { ++ const { state } = ctx; ++ const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; ++ return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); + } -function cancelImperativeScroll(state) { - cancelScrollCompletionChecks(state); - state.scheduledWork.cancel("imperativeScrollReady"); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; - settlePendingImperativeScroll(state); --} -- ++ ++// src/utils/getId.ts ++function getId(state, index) { ++ const { data, keyExtractor } = state.props; ++ if (!data) { ++ return ""; ++ } ++ const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; ++ const id = ret; ++ state.idCache[index] = id; ++ return id; + } + -// src/core/deferredPublicOnScroll.ts -function withResolvedContentOffset(state, event, resolvedOffset) { - return { @@ -8360,12 +8892,43 @@ index 95465f2..4fb1e41 100644 - nativeEvent: { - ...event.nativeEvent, - contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset } -- } ++// src/core/updateContentMetricsState.ts ++function getRawContentLength(ctx) { ++ var _a3, _b, _c; ++ const { state, values } = ctx; ++ return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); ++} ++function getAlignItemsAtEndPadding(ctx) { ++ const { state } = ctx; ++ const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; ++ return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; ++} ++function updateContentMetricsState(ctx) { ++ var _a3; ++ const { state } = ctx; ++ const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; ++ const nextPadding = getAlignItemsAtEndPadding(ctx); ++ if (previousPadding !== nextPadding) { ++ const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated"; ++ const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold")); ++ if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) { ++ state.scheduledWork.microtask(() => { ++ if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) { ++ set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx)); ++ } ++ }, "alignItemsAtEndPaddingFallback"); ++ } else if (!isMaintainExpected) { ++ set$(ctx, "alignItemsAtEndPadding", nextPadding); + } - }; --} ++ } + } -function releaseDeferredPublicOnScroll(ctx, resolvedOffset) { - var _a3, _b, _c, _d; -- const state = ctx.state; ++ ++// src/core/addTotalSize.ts ++function addTotalSize(ctx, key, add, notifyTotalSize = true) { + const state = ctx.state; - const deferredEvent = state.deferredPublicOnScrollEvent; - state.deferredPublicOnScrollEvent = void 0; - if (deferredEvent) { @@ -8377,18 +8940,54 @@ index 95465f2..4fb1e41 100644 - (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 - ) - ); -- } --} -- ++ const prevTotalSize = state.totalSize; ++ let totalSize = state.totalSize; ++ if (key === null) { ++ totalSize = add; ++ if (state.timeoutSetPaddingTop) { ++ clearTimeout(state.timeoutSetPaddingTop); ++ state.timeoutSetPaddingTop = void 0; ++ } ++ } else { ++ totalSize += add; ++ } ++ if (prevTotalSize !== totalSize) { ++ { ++ state.pendingTotalSize = void 0; ++ state.totalSize = totalSize; ++ if (notifyTotalSize) { ++ set$(ctx, "totalSize", totalSize); ++ } ++ updateContentMetricsState(ctx); ++ } ++ } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { ++ set$(ctx, "totalSize", totalSize); + } + } + -// src/core/initialScrollSession.ts -var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -function hasInitialScrollSessionCompletion(completion) { - return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); --} ++// src/core/setSize.ts ++function setSize(ctx, itemKey, size, notifyTotalSize = true) { ++ const state = ctx.state; ++ const { sizes } = state; ++ const previousSize = sizes.get(itemKey); ++ const diff = previousSize !== void 0 ? size - previousSize : size; ++ if (diff !== 0) { ++ addTotalSize(ctx, itemKey, diff, notifyTotalSize); ++ } ++ sizes.set(itemKey, size); + } -function clearInitialScrollSession(state) { - state.initialScrollSession = void 0; - return void 0; --} ++ ++// src/utils/helpers.ts ++function isFunction(obj) { ++ return typeof obj === "function"; + } -function createInitialScrollSession(options) { - const { bootstrap, completion, kind, previousDataLength } = options; - return kind === "offset" ? { @@ -8401,7 +9000,9 @@ index 95465f2..4fb1e41 100644 - kind, - previousDataLength - }; --} ++function isArray(obj) { ++ return Array.isArray(obj); + } -function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { - var _a4, _b2; - if (!state.initialScrollSession) { @@ -8417,10 +9018,15 @@ index 95465f2..4fb1e41 100644 - kind, - previousDataLength: state.initialScrollSession.previousDataLength - }); -- } ++var warned = /* @__PURE__ */ new Set(); ++function warnDevOnce(id, text) { ++ if (IS_DEV && !warned.has(id)) { ++ warned.add(id); ++ console.warn(`[legend-list] ${text}`); + } - (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; - return state.initialScrollSession.completion; --} + } -var initialScrollCompletion = { - didDispatchNativeScroll(state) { - var _a3, _b; @@ -8439,11 +9045,36 @@ index 95465f2..4fb1e41 100644 - resetFlags(state) { - if (!state.initialScrollSession) { - return; -- } ++function roundSize(size) { ++ return Math.floor(size * 8) / 8; ++} ++function isNullOrUndefined(value) { ++ return value === null || value === void 0; ++} ++function getPadding(s, type) { ++ var _a3, _b, _c; ++ const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; ++ return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; ++} ++function extractPadding(style, contentContainerStyle, type) { ++ return getPadding(style, type) + getPadding(contentContainerStyle, type); ++} ++function findContainerId(ctx, key) { ++ var _a3, _b; ++ const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); ++ if (directMatch !== void 0) { ++ return directMatch; ++ } ++ const numContainers = peek$(ctx, "numContainers"); ++ for (let i = 0; i < numContainers; i++) { ++ const itemKey = peek$(ctx, `containerItemKey${i}`); ++ if (itemKey === key) { ++ return i; + } - const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); - completion.didDispatchNativeScroll = void 0; - completion.didRetrySilentInitialScroll = void 0; -- } + } -}; -var initialScrollWatchdog = { - clear(state) { @@ -8467,16 +9098,46 @@ index 95465f2..4fb1e41 100644 - var _a3, _b; - if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { - return; -- } ++ return -1; ++} ++ ++// src/utils/getItemSize.ts ++function getKnownOrFixedSize(ctx, key, index, data, resolved) { ++ var _a3, _b; ++ const state = ctx.state; ++ const { getFixedItemSize, getItemType } = state.props; ++ let size = key ? state.sizesKnown.get(key) : void 0; ++ if (size === void 0 && key && getFixedItemSize) { ++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; ++ const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); ++ if (fixedSize !== void 0) { ++ size = fixedSize + ctx.scrollAxisGap; ++ state.sizesKnown.set(key, size); + } - const completion = ensureInitialScrollSessionCompletion(state); - completion.watchdog = watchdog ? { - startScroll: watchdog.startScroll, - targetOffset: watchdog.targetOffset - } : void 0; -- } + } -}; -function setInitialScrollSession(state, options = {}) { -- var _a3, _b, _c, _d; ++ return size; ++} ++function getKnownOrFixedItemSize(ctx, index) { ++ const key = getId(ctx.state, index); ++ return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); ++} ++function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { ++ for (let index = startIndex; index <= endIndex; index++) { ++ if (getKnownOrFixedItemSize(ctx, index) === void 0) { ++ return false; ++ } ++ } ++ return true; ++} ++function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { + var _a3, _b, _c, _d; - const existingSession = state.initialScrollSession; - const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; - const completion = existingSession == null ? void 0 : existingSession.completion; @@ -8484,10 +9145,26 @@ index 95465f2..4fb1e41 100644 - const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; - if (!kind) { - return clearInitialScrollSession(state); -- } ++ const state = ctx.state; ++ const { ++ sizes, ++ averageSizes, ++ props: { estimatedItemSize, getItemType }, ++ scrollingTo ++ } = state; ++ const sizeKnown = state.sizesKnown.get(key); ++ if (sizeKnown !== void 0) { ++ return sizeKnown; + } - if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { - return clearInitialScrollSession(state); -- } ++ let size; ++ const renderedSize = sizes.get(key); ++ if (preferCachedSize) { ++ if (renderedSize !== void 0) { ++ return renderedSize; ++ } + } - const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; - state.initialScrollSession = createInitialScrollSession({ - bootstrap, @@ -8496,256 +9173,133 @@ index 95465f2..4fb1e41 100644 - previousDataLength - }); - return state.initialScrollSession; ++ size = getKnownOrFixedSize(ctx, key, index, data, resolved); ++ if (size !== void 0) { ++ setSize(ctx, key, size, notifyTotalSize); ++ return size; ++ } ++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; ++ if (useAverageSize && !scrollingTo) { ++ const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; ++ if (averageSizeForType !== void 0) { ++ size = roundSize(averageSizeForType); ++ } ++ } ++ if (size === void 0 && renderedSize !== void 0) { ++ return renderedSize; ++ } ++ if (size === void 0 && useAverageSize && scrollingTo) { ++ const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; ++ if (averageSizeForType !== void 0) { ++ size = roundSize(averageSizeForType); ++ } ++ } ++ if (size === void 0) { ++ size = estimatedItemSize + ctx.scrollAxisGap; ++ } ++ setSize(ctx, key, size, notifyTotalSize); ++ return size; ++} ++function getItemSizeAtIndex(ctx, index) { ++ if (index === void 0 || index < 0) { ++ return void 0; ++ } ++ const targetId = getId(ctx.state, index); ++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); ++} ++ ++// src/core/calculateOffsetWithOffsetPosition.ts ++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { ++ var _a3; ++ const state = ctx.state; ++ const { index, viewOffset, viewPosition } = params; ++ let offset = offsetParam; ++ if (viewOffset) { ++ offset -= viewOffset; ++ } ++ if (index !== void 0) { ++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); ++ if (startOffsetAdjustment) { ++ offset += startOffsetAdjustment; ++ } ++ } ++ if (viewPosition !== void 0 && index !== void 0) { ++ const dataLength = state.props.data.length; ++ if (dataLength === 0) { ++ return offset; ++ } ++ const isOutOfBounds = index < 0 || index >= dataLength; ++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; ++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); ++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); ++ const trailingInset = getContentInsetEnd(ctx); ++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); ++ if (!isOutOfBounds && index === state.props.data.length - 1) { ++ const footerSize = peek$(ctx, "footerSize") || 0; ++ offset += footerSize; ++ } ++ } ++ return offset; ++} ++ ++// src/core/clampScrollOffset.ts ++function clampScrollOffset(ctx, offset, scrollTarget) { ++ const state = ctx.state; ++ const contentSize = getContentSize(ctx); ++ let clampedOffset = offset; ++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { ++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); ++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; ++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; ++ const maxOffset = baseMaxOffset + extraEndOffset; ++ clampedOffset = Math.min(offset, maxOffset); ++ } ++ clampedOffset = Math.max(0, clampedOffset); ++ return clampedOffset; + } + + // src/utils/checkThreshold.ts +@@ -796,626 +896,616 @@ function recalculateSettledScroll(ctx) { + } + checkThresholds(ctx); + } +-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; +-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; +-function clearAdaptiveRenderExitTimeout(ctx) { +- ctx.state.scheduledWork.cancel("adaptiveRender"); -} -- --// src/utils/checkThreshold.ts --var HYSTERESIS_MULTIPLIER = 1.3; --function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { -- const absDistance = Math.abs(distance); -- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; --} --var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { -- const absDistance = Math.abs(distance); -- const within = atThreshold || threshold > 0 && absDistance <= threshold; -- const updateSnapshot = () => { -- setSnapshot({ -- atThreshold, -- contentSize: context.contentSize, -- dataLength: context.dataLength, -- scrollPosition: context.scrollPosition -- }); -- }; -- if (!wasReached) { -- if (!within) { -- return false; -- } -- onReached(distance); -- updateSnapshot(); -- return true; -- } -- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); -- if (reset) { -- setSnapshot(void 0); -- return false; -- } -- if (within) { -- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; -- if (changed) { -- updateSnapshot(); -- } -- } -- return true; --}; -- --// src/utils/edgeReachedGate.ts --function resetEdgeLatch(ctx, edge) { +-function scheduleAdaptiveRenderExit(ctx, exitDelay) { - const state = ctx.state; -- if (edge === "start") { -- state.isStartReached = false; -- state.startReachedSnapshot = void 0; +- clearAdaptiveRenderExitTimeout(ctx); +- if (exitDelay <= 0) { +- setAdaptiveRender(ctx, "normal", "scroll"); - } else { -- state.isEndReached = false; -- state.endReachedSnapshot = void 0; +- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); - } -} --function resetSharedEdgeGateIfOutsideHysteresis(ctx) { -- const state = ctx.state; -- if (!state.edgeReachedGate) { -- return; -- } -- const contentSize = getContentSize(ctx); -- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); -- const isContentLess = contentSize < state.scrollLength; -- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; -- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; -- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); -- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); -- if (isOutsideStart && isOutsideEnd) { -- state.edgeReachedGate = void 0; +-function setAdaptiveRender(ctx, mode, reason) { +- var _a3, _b; +- const previousMode = peek$(ctx, "adaptiveRender"); +- if (previousMode !== mode) { +- set$(ctx, "adaptiveRender", mode); +- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); - } -} --function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { -- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; --} --function markReachedEdge(ctx) { -- ctx.state.edgeReachedGate = "closed"; --} --function prepareReachedEdgeForNextUserScroll(ctx) { -- if (ctx.state.edgeReachedGate) { -- ctx.state.edgeReachedGate = "prepared"; -- } --} --function beginReachedEdgeUserScroll(ctx, scrollDelta) { -- const state = ctx.state; -- if (state.edgeReachedGate !== "prepared") { -- return void 0; -- } -- const allowedEdge = scrollDelta < 0 ? "start" : "end"; -- state.edgeReachedGate = "closed"; -- resetEdgeLatch(ctx, allowedEdge); -- return allowedEdge; --} -- --// src/utils/hasActiveInitialScroll.ts --function hasActiveInitialScroll(state) { -- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; --} -- --// src/utils/checkAtBottom.ts --function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -- var _a3; -- const state = ctx.state; -- if (!state) { -- return; -- } -- const { -- queuedInitialLayout, -- scrollLength, -- scroll, -- maintainingScrollAtEnd, -- props: { maintainScrollAtEndThreshold, onEndReachedThreshold } -- } = state; -- const contentSize = getContentSize(ctx); -- resetSharedEdgeGateIfOutsideHysteresis(ctx); -- if (contentSize > 0 && queuedInitialLayout) { -- const insetEnd = getContentInsetEnd(ctx); -- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; -- const isContentLess = contentSize < scrollLength; -- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); -- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); -- set$( -- ctx, -- "isWithinMaintainScrollAtEndThreshold", -- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength -- ); -- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; -- if (!shouldSkipThresholdChecks) { -- state.isEndReached = checkThreshold( -- distanceFromEnd, -- isContentLess, -- onEndReachedThreshold * scrollLength, -- state.isEndReached, -- state.endReachedSnapshot, -- { -- contentSize, -- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, -- scrollPosition: scroll -- }, -- (distance) => { -- var _a4, _b; -- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { -- markReachedEdge(ctx); -- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); -- } -- }, -- (snapshot) => { -- state.endReachedSnapshot = snapshot; -- } -- ); -- } -- } --} -- --// src/utils/checkAtTop.ts --function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -- const state = ctx == null ? void 0 : ctx.state; -- if (!state) { -- return; -- } -- const { -- isStartReached, -- props: { data, onStartReachedThreshold }, -- scroll, -- scrollLength, -- startReachedSnapshot, -- totalSize -- } = state; -- const dataLength = data.length; -- const threshold = onStartReachedThreshold * scrollLength; -- resetSharedEdgeGateIfOutsideHysteresis(ctx); -- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { -- state.isStartReached = false; -- state.startReachedSnapshot = void 0; -- } -- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); -- set$(ctx, "isNearStart", scroll <= threshold); -- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; -- if (!shouldSkipThresholdChecks) { -- state.isStartReached = checkThreshold( -- scroll, -- false, -- threshold, -- state.isStartReached, -- startReachedSnapshot, -- { -- contentSize: totalSize, -- dataLength, -- scrollPosition: scroll -- }, -- (distance) => { -- var _a3, _b; -- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { -- markReachedEdge(ctx); -- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); -- } -- }, -- (snapshot) => { -- state.startReachedSnapshot = snapshot; -- } -- ); -- } --} -- --// src/utils/checkThresholds.ts --function checkThresholds(ctx, allowedEdge) { -- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; -- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); --} -- --// src/core/recalculateSettledScroll.ts --function recalculateSettledScroll(ctx) { -- var _a3, _b; -- const state = ctx.state; -- if ((_a3 = state.props) == null ? void 0 : _a3.data) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); -- } -- checkThresholds(ctx); --} --var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; --var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; --var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; --function clearAdaptiveRenderExitTimeout(ctx) { -- ctx.state.scheduledWork.cancel("adaptiveRender"); --} --function scheduleAdaptiveRenderExit(ctx, exitDelay) { -- const state = ctx.state; -- clearAdaptiveRenderExitTimeout(ctx); -- if (exitDelay <= 0) { -- setAdaptiveRender(ctx, "normal", "scroll"); -- } else { -- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); -- } --} --function setAdaptiveRender(ctx, mode, reason) { -- var _a3, _b; -- const previousMode = peek$(ctx, "adaptiveRender"); -- if (previousMode !== mode) { -- set$(ctx, "adaptiveRender", mode); -- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); -- } --} --function resetAdaptiveRender(ctx) { -- var _a3, _b; -- clearAdaptiveRenderExitTimeout(ctx); -- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; -- if (peek$(ctx, "adaptiveRender") !== mode) { -- setAdaptiveRender(ctx, mode, "initial"); +-function resetAdaptiveRender(ctx) { ++ ++// src/core/finishScrollTo.ts ++function finishScrollTo(ctx) { + var _a3, _b; +- clearAdaptiveRenderExitTimeout(ctx); +- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; +- if (peek$(ctx, "adaptiveRender") !== mode) { +- setAdaptiveRender(ctx, mode, "initial"); - } -} -function updateAdaptiveRender(ctx, scrollVelocity, options) { - var _a3, _b, _c; -- const state = ctx.state; + const state = ctx.state; - const adaptiveRender = state.props.adaptiveRender; - const currentMode = peek$(ctx, "adaptiveRender"); - if (peek$(ctx, "readyToRender")) { @@ -8766,10 +9320,40 @@ index 95465f2..4fb1e41 100644 - } - } else { - resetAdaptiveRender(ctx); -- } -- } --} -- ++ if (state == null ? void 0 : state.scrollingTo) { ++ cancelScrollCompletionChecks(state); ++ const resolvePendingScroll = state.pendingScrollResolve; ++ state.pendingScrollResolve = void 0; ++ const scrollingTo = state.scrollingTo; ++ state.scrollHistory.length = 0; ++ state.scrollingTo = void 0; ++ state.scrollTargetPinnedRange = void 0; ++ if (state.pendingTotalSize !== void 0) { ++ addTotalSize(ctx, null, state.pendingTotalSize); ++ } ++ { ++ state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); + } ++ if (scrollingTo.isInitialScroll || state.initialScroll) { ++ const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; ++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; ++ finishInitialScroll(ctx, { ++ onFinished: () => { ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); ++ }, ++ preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, ++ recalculateItems: true, ++ schedulePreservedTargetClear: shouldPreserveResizeTarget, ++ syncObservedOffset: isOffsetSession, ++ waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame ++ }); ++ return; ++ } ++ recalculateSettledScroll(ctx); ++ resolvePendingScroll == null ? void 0 : resolvePendingScroll(); + } + } + -// src/utils/getEffectiveDrawDistance.ts -var INITIAL_DRAW_DISTANCE = 50; -function getEffectiveDrawDistance(ctx, mode) { @@ -8783,8 +9367,21 @@ index 95465f2..4fb1e41 100644 -function scheduleFullDrawDistancePrewarm(ctx) { - const { state } = ctx; - if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { -- return; -- } ++// src/core/doScrollTo.ts ++var SCROLL_END_IDLE_MS = 80; ++var SCROLL_END_MAX_MS = 1500; ++var SMOOTH_SCROLL_DURATION_MS = 320; ++var SCROLL_END_TARGET_EPSILON = 1; ++function doScrollTo(ctx, params) { ++ var _a3, _b; ++ const state = ctx.state; ++ const { animated, horizontal, offset } = params; ++ state.scheduledWork.cancel("platformScrollCompletion"); ++ const scroller = state.refScroller.current; ++ const node = scroller == null ? void 0 : scroller.getScrollableNode(); ++ if (!scroller || !node) { + return; + } - state.scheduledWork.frame(() => { - var _a3; - return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); @@ -8803,10 +9400,35 @@ index 95465f2..4fb1e41 100644 - } - if (resetInitialScroll) { - state.didFinishInitialScroll = false; -- } ++ const isAnimated = !!animated; ++ const isHorizontal = !!horizontal; ++ const contentSize = isHorizontal ? getContentSize(ctx) : void 0; ++ const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; ++ const top = isHorizontal ? 0 : offset; ++ scroller.scrollTo({ animated: isAnimated, x: left, y: top }); ++ if (isAnimated) { ++ const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; ++ listenForScrollEnd(ctx, { ++ readOffset: () => scroller.getCurrentScrollOffset(), ++ target, ++ targetOffset: offset ++ }); ++ } else { ++ state.scroll = offset; ++ const targetToken = state.scrollingTo; ++ state.scheduledWork.timeout( ++ () => { ++ if (targetToken === state.scrollingTo) { ++ finishScrollTo(ctx); ++ } ++ }, ++ 100, ++ "platformScrollCompletion" ++ ); + } - set$(ctx, "readyToRender", false); - resetAdaptiveRender(ctx); --} + } -function setInitialRenderState(ctx, { - didLayout, - didInitialScroll @@ -8821,20 +9443,36 @@ index 95465f2..4fb1e41 100644 - } - if (didInitialScroll) { - state.didFinishInitialScroll = true; -- } ++function listenForScrollEnd(ctx, params) { ++ const { readOffset, target, targetOffset } = params; ++ if (!target) { ++ finishScrollTo(ctx); ++ return; + } - const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); - if (isReadyToRender && !peek$(ctx, "readyToRender")) { - set$(ctx, "readyToRender", true); - setAdaptiveRender(ctx, "normal", "ready"); - if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { - scheduleFullDrawDistancePrewarm(ctx); -- } ++ const supportsScrollEnd = "onscrollend" in target; ++ let idleTimeout; ++ let settled = false; ++ const { scheduledWork, scrollingTo: targetToken } = ctx.state; ++ const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); ++ const cleanup = () => { ++ target.removeEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.removeEventListener("scrollend", onScrollEnd); + } - if (!state.didLoad) { - state.didLoad = true; - if (onLoad) { - onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); - } -- } ++ if (idleTimeout !== void 0) { ++ clearTimeout(idleTimeout); + } - } -} - @@ -8861,12 +9499,24 @@ index 95465f2..4fb1e41 100644 - if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { - if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { - cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -- } ++ clearTimeout(maxTimeout); ++ }; ++ const cancel = () => { ++ if (!settled) { ++ settled = true; ++ cleanup(); + } - if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { - cancelScrollCompletionChecks(state); - state.scrollingTo = void 0; - state.scrollTargetPinnedRange = void 0; -- } ++ }; ++ const finish = (reason) => { ++ if (settled) return; ++ if (targetToken !== ctx.state.scrollingTo) { ++ scheduledWork.cancel("platformScrollCompletion"); ++ return; + } - initialScrollCompletion.resetFlags(state); - setInitialScrollSession(state, { bootstrap: null }); - finishInitialScroll(ctx); @@ -8881,758 +9531,141 @@ index 95465f2..4fb1e41 100644 - const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); - if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { - syncInitialScrollOffset(state, observedOffset); -- } -- } -- const complete = () => { -- var _a4, _b2, _c2, _d, _e; -- const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; -- const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0; -- initialScrollWatchdog.clear(state); -- if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) { -- state.clearPreservedInitialScrollOnNextFinish = void 0; -- setInitialScrollSession(state); -- clearPreservedInitialScrollTargetTimeout(state); -- if (options == null ? void 0 : options.schedulePreservedTargetClear) { -- state.scheduledWork.timeout( -- () => { -- var _a5; -- if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) { -- return; -- } -- clearPreservedInitialScrollTarget(state); -- }, -- PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, -- "preservedInitialScroll" -- ); -- } -- } else { -- clearPreservedInitialScrollTarget(state); -- } -- if (options == null ? void 0 : options.recalculateItems) { -- recalculateSettledScroll(ctx); -- } -- setInitialRenderState(ctx, { didInitialScroll: true }); -- if (shouldReleaseDeferredPublicOnScroll) { -- releaseDeferredPublicOnScroll(ctx, finalScrollOffset); -- } -- (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); -- }; -- if (options == null ? void 0 : options.waitForCompletionFrame) { -- requestAnimationFrame(complete); -- return; -- } -- complete(); --} -- --// src/core/calculateOffsetForIndex.ts --function calculateOffsetForIndex(ctx, index) { -- const state = ctx.state; -- return index !== void 0 ? state.positions[index] || 0 : 0; --} -- - // src/core/getStartOffsetAdjustment.ts - function getStartOffsetAdjustment(ctx) { - const { state } = ctx; -@@ -1213,58 +614,273 @@ function getItemSizeAtIndex(ctx, index) { - if (index === void 0 || index < 0) { - return void 0; - } -- const targetId = getId(ctx.state, index); -- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+ const targetId = getId(ctx.state, index); -+ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); -+} -+ -+// src/core/calculateOffsetWithOffsetPosition.ts -+function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -+ var _a3; -+ const state = ctx.state; -+ const { index, viewOffset, viewPosition } = params; -+ let offset = offsetParam; -+ if (viewOffset) { -+ offset -= viewOffset; -+ } -+ if (index !== void 0) { -+ const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -+ if (startOffsetAdjustment) { -+ offset += startOffsetAdjustment; -+ } -+ } -+ if (viewPosition !== void 0 && index !== void 0) { -+ const dataLength = state.props.data.length; -+ if (dataLength === 0) { -+ return offset; -+ } -+ const isOutOfBounds = index < 0 || index >= dataLength; -+ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -+ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -+ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -+ const trailingInset = getContentInsetEnd(ctx); -+ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -+ if (!isOutOfBounds && index === state.props.data.length - 1) { -+ const footerSize = peek$(ctx, "footerSize") || 0; -+ offset += footerSize; -+ } -+ } -+ return offset; -+} -+ -+// src/core/clampScrollOffset.ts -+function clampScrollOffset(ctx, offset, scrollTarget) { -+ const state = ctx.state; -+ const contentSize = getContentSize(ctx); -+ let clampedOffset = offset; -+ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { -+ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); -+ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; -+ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; -+ const maxOffset = baseMaxOffset + extraEndOffset; -+ clampedOffset = Math.min(offset, maxOffset); -+ } -+ clampedOffset = Math.max(0, clampedOffset); -+ return clampedOffset; -+} -+ -+// src/utils/checkThreshold.ts -+var HYSTERESIS_MULTIPLIER = 1.3; -+function isOutsideThresholdHysteresis(distance, atThreshold, threshold) { -+ const absDistance = Math.abs(distance); -+ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0; -+} -+var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => { -+ const absDistance = Math.abs(distance); -+ const within = atThreshold || threshold > 0 && absDistance <= threshold; -+ const updateSnapshot = () => { -+ setSnapshot({ -+ atThreshold, -+ contentSize: context.contentSize, -+ dataLength: context.dataLength, -+ scrollPosition: context.scrollPosition -+ }); ++ const currentOffset = readOffset(); ++ const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; ++ if (reason === "scrollend" && !isNearTarget) { ++ return; + } ++ scheduledWork.cancel("platformScrollCompletion"); ++ finishScrollTo(ctx); + }; -+ if (!wasReached) { -+ if (!within) { -+ return false; -+ } -+ onReached(distance); -+ updateSnapshot(); -+ return true; -+ } -+ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold); -+ if (reset) { -+ setSnapshot(void 0); -+ return false; -+ } -+ if (within) { -+ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength; -+ if (changed) { -+ updateSnapshot(); ++ const onScroll2 = () => { ++ if (idleTimeout !== void 0) { ++ clearTimeout(idleTimeout); + } -+ } -+ return true; -+}; -+ -+// src/utils/edgeReachedGate.ts -+function resetEdgeLatch(ctx, edge) { -+ const state = ctx.state; -+ if (edge === "start") { -+ state.isStartReached = false; -+ state.startReachedSnapshot = void 0; ++ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); ++ }; ++ const onScrollEnd = () => finish("scrollend"); ++ target.addEventListener("scroll", onScroll2); ++ if (supportsScrollEnd) { ++ target.addEventListener("scrollend", onScrollEnd); + } else { -+ state.isEndReached = false; -+ state.endReachedSnapshot = void 0; -+ } -+} -+function resetSharedEdgeGateIfOutsideHysteresis(ctx) { -+ const state = ctx.state; -+ if (!state.edgeReachedGate) { -+ return; -+ } -+ const contentSize = getContentSize(ctx); -+ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx); -+ const isContentLess = contentSize < state.scrollLength; -+ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength; -+ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength; -+ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold); -+ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold); -+ if (isOutsideStart && isOutsideEnd) { -+ state.edgeReachedGate = void 0; -+ } -+} -+function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) { -+ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck; -+} -+function markReachedEdge(ctx) { -+ ctx.state.edgeReachedGate = "closed"; -+} -+function prepareReachedEdgeForNextUserScroll(ctx) { -+ if (ctx.state.edgeReachedGate) { -+ ctx.state.edgeReachedGate = "prepared"; -+ } -+} -+function beginReachedEdgeUserScroll(ctx, scrollDelta) { -+ const state = ctx.state; -+ if (state.edgeReachedGate !== "prepared") { -+ return void 0; -+ } -+ const allowedEdge = scrollDelta < 0 ? "start" : "end"; -+ state.edgeReachedGate = "closed"; -+ resetEdgeLatch(ctx, allowedEdge); -+ return allowedEdge; -+} -+ -+// src/utils/hasActiveInitialScroll.ts -+function hasActiveInitialScroll(state) { -+ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll; -+} -+ -+// src/utils/checkAtBottom.ts -+function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -+ var _a3; -+ const state = ctx.state; -+ if (!state) { -+ return; -+ } -+ const { -+ queuedInitialLayout, -+ scrollLength, -+ scroll, -+ maintainingScrollAtEnd, -+ props: { maintainScrollAtEndThreshold, onEndReachedThreshold } -+ } = state; -+ const contentSize = getContentSize(ctx); -+ resetSharedEdgeGateIfOutsideHysteresis(ctx); -+ if (contentSize > 0 && queuedInitialLayout) { -+ const insetEnd = getContentInsetEnd(ctx); -+ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd; -+ const isContentLess = contentSize < scrollLength; -+ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON); -+ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength); -+ set$( -+ ctx, -+ "isWithinMaintainScrollAtEndThreshold", -+ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength -+ ); -+ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd; -+ if (!shouldSkipThresholdChecks) { -+ state.isEndReached = checkThreshold( -+ distanceFromEnd, -+ isContentLess, -+ onEndReachedThreshold * scrollLength, -+ state.isEndReached, -+ state.endReachedSnapshot, -+ { -+ contentSize, -+ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length, -+ scrollPosition: scroll -+ }, -+ (distance) => { -+ var _a4, _b; -+ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) { -+ markReachedEdge(ctx); -+ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance }); -+ } -+ }, -+ (snapshot) => { -+ state.endReachedSnapshot = snapshot; -+ } -+ ); -+ } -+ } -+} -+ -+// src/utils/checkAtTop.ts -+function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) { -+ const state = ctx == null ? void 0 : ctx.state; -+ if (!state) { -+ return; -+ } -+ const { -+ isStartReached, -+ props: { data, onStartReachedThreshold }, -+ scroll, -+ scrollLength, -+ startReachedSnapshot, -+ totalSize -+ } = state; -+ const dataLength = data.length; -+ const threshold = onStartReachedThreshold * scrollLength; -+ resetSharedEdgeGateIfOutsideHysteresis(ctx); -+ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) { -+ state.isStartReached = false; -+ state.startReachedSnapshot = void 0; -+ } -+ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON); -+ set$(ctx, "isNearStart", scroll <= threshold); -+ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo; -+ if (!shouldSkipThresholdChecks) { -+ state.isStartReached = checkThreshold( -+ scroll, -+ false, -+ threshold, -+ state.isStartReached, -+ startReachedSnapshot, -+ { -+ contentSize: totalSize, -+ dataLength, -+ scrollPosition: scroll -+ }, -+ (distance) => { -+ var _a3, _b; -+ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) { -+ markReachedEdge(ctx); -+ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance }); -+ } -+ }, -+ (snapshot) => { -+ state.startReachedSnapshot = snapshot; -+ } -+ ); -+ } - } - --// src/core/calculateOffsetWithOffsetPosition.ts --function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { -- var _a3; -- const state = ctx.state; -- const { index, viewOffset, viewPosition } = params; -- let offset = offsetParam; -- if (viewOffset) { -- offset -= viewOffset; -- } -- if (index !== void 0) { -- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); -- if (startOffsetAdjustment) { -- offset += startOffsetAdjustment; -- } -- } -- if (viewPosition !== void 0 && index !== void 0) { -- const dataLength = state.props.data.length; -- if (dataLength === 0) { -- return offset; -- } -- const isOutOfBounds = index < 0 || index >= dataLength; -- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; -- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); -- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); -- const trailingInset = getContentInsetEnd(ctx); -- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); -- if (!isOutOfBounds && index === state.props.data.length - 1) { -- const footerSize = peek$(ctx, "footerSize") || 0; -- offset += footerSize; -- } -- } -- return offset; -+// src/utils/checkThresholds.ts -+function checkThresholds(ctx, allowedEdge) { -+ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; -+ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); -+ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); - } - --// src/core/clampScrollOffset.ts --function clampScrollOffset(ctx, offset, scrollTarget) { -+// src/core/recalculateSettledScroll.ts -+function recalculateSettledScroll(ctx) { -+ var _a3, _b; - const state = ctx.state; -- const contentSize = getContentSize(ctx); -- let clampedOffset = offset; -- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { -- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); -- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; -- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; -- const maxOffset = baseMaxOffset + extraEndOffset; -- clampedOffset = Math.min(offset, maxOffset); -+ if ((_a3 = state.props) == null ? void 0 : _a3.data) { -+ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true }); - } -- clampedOffset = Math.max(0, clampedOffset); -- return clampedOffset; -+ checkThresholds(ctx); - } - - // src/core/finishScrollTo.ts -@@ -1404,6 +1020,181 @@ function listenForScrollEnd(ctx, params) { - scheduledWork.register("platformScrollCompletion", cancel); - } - -+// src/core/initialScrollSession.ts -+var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; -+function hasInitialScrollSessionCompletion(completion) { -+ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); -+} -+function clearInitialScrollSession(state) { -+ state.initialScrollSession = void 0; -+ return void 0; -+} -+function createInitialScrollSession(options) { -+ const { bootstrap, completion, kind, previousDataLength } = options; -+ return kind === "offset" ? { -+ completion, -+ kind, -+ previousDataLength -+ } : { -+ bootstrap, -+ completion, -+ kind, -+ previousDataLength -+ }; -+} -+function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { -+ var _a4, _b2; -+ if (!state.initialScrollSession) { -+ state.initialScrollSession = createInitialScrollSession({ -+ completion: {}, -+ kind, -+ previousDataLength: 0 -+ }); -+ } else if (state.initialScrollSession.kind !== kind) { -+ state.initialScrollSession = createInitialScrollSession({ -+ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, -+ completion: state.initialScrollSession.completion, -+ kind, -+ previousDataLength: state.initialScrollSession.previousDataLength -+ }); -+ } -+ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; -+ return state.initialScrollSession.completion; -+} -+var initialScrollCompletion = { -+ didDispatchNativeScroll(state) { -+ var _a3, _b; -+ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); -+ }, -+ didRetrySilentInitialScroll(state) { -+ var _a3, _b; -+ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); -+ }, -+ markInitialScrollNativeDispatch(state) { -+ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; -+ }, -+ markSilentInitialScrollRetry(state) { -+ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; -+ }, -+ resetFlags(state) { -+ if (!state.initialScrollSession) { -+ return; -+ } -+ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); -+ completion.didDispatchNativeScroll = void 0; -+ completion.didRetrySilentInitialScroll = void 0; -+ } -+}; -+var initialScrollWatchdog = { -+ clear(state) { -+ initialScrollWatchdog.set(state, void 0); -+ }, -+ didReachTarget(newScroll, watchdog) { -+ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); -+ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ get(state) { -+ var _a3, _b; -+ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; -+ }, -+ hasNonZeroTargetOffset(targetOffset) { -+ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ isAtZeroTargetOffset(targetOffset) { -+ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; -+ }, -+ set(state, watchdog) { -+ var _a3, _b; -+ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { -+ return; -+ } -+ const completion = ensureInitialScrollSessionCompletion(state); -+ completion.watchdog = watchdog ? { -+ startScroll: watchdog.startScroll, -+ targetOffset: watchdog.targetOffset -+ } : void 0; -+ } -+}; -+function setInitialScrollSession(state, options = {}) { -+ var _a3, _b, _c, _d; -+ const existingSession = state.initialScrollSession; -+ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; -+ const completion = existingSession == null ? void 0 : existingSession.completion; -+ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; -+ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; -+ if (!kind) { -+ return clearInitialScrollSession(state); -+ } -+ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { -+ return clearInitialScrollSession(state); -+ } -+ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; -+ state.initialScrollSession = createInitialScrollSession({ -+ bootstrap, -+ completion, -+ kind, -+ previousDataLength -+ }); -+ return state.initialScrollSession; -+} -+var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; -+var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; -+var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; -+function clearAdaptiveRenderExitTimeout(ctx) { -+ ctx.state.scheduledWork.cancel("adaptiveRender"); -+} -+function scheduleAdaptiveRenderExit(ctx, exitDelay) { -+ const state = ctx.state; -+ clearAdaptiveRenderExitTimeout(ctx); -+ if (exitDelay <= 0) { -+ setAdaptiveRender(ctx, "normal", "scroll"); -+ } else { -+ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); -+ } -+} -+function setAdaptiveRender(ctx, mode, reason) { -+ var _a3, _b; -+ const previousMode = peek$(ctx, "adaptiveRender"); -+ if (previousMode !== mode) { -+ set$(ctx, "adaptiveRender", mode); -+ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); -+ } -+} -+function resetAdaptiveRender(ctx) { -+ var _a3, _b; -+ clearAdaptiveRenderExitTimeout(ctx); -+ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; -+ if (peek$(ctx, "adaptiveRender") !== mode) { -+ setAdaptiveRender(ctx, mode, "initial"); -+ } -+} -+function updateAdaptiveRender(ctx, scrollVelocity, options) { -+ var _a3, _b, _c; -+ const state = ctx.state; -+ const adaptiveRender = state.props.adaptiveRender; -+ const currentMode = peek$(ctx, "adaptiveRender"); -+ if (peek$(ctx, "readyToRender")) { -+ if (adaptiveRender) { -+ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; -+ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; -+ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; -+ const threshold = currentMode === "light" ? exitVelocity : enterVelocity; -+ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; -+ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; -+ if (nextMode !== previousMode) { -+ if (nextMode === "light") { -+ setAdaptiveRender(ctx, "light", "scroll"); -+ scheduleAdaptiveRenderExit(ctx, exitDelay); -+ } else if (currentMode === "light") { -+ scheduleAdaptiveRenderExit(ctx, exitDelay); -+ } -+ } -+ } else { -+ resetAdaptiveRender(ctx); -+ } -+ } -+} -+ - // src/core/doMaintainScrollAtEnd.ts - function doMaintainScrollAtEnd(ctx) { - const state = ctx.state; -@@ -1430,9 +1221,12 @@ function doMaintainScrollAtEnd(ctx) { - const activeState = maintainScrollAtEnd.animated ? "animated" : "instant"; - const scrollAtRequest = state.scroll; - state.maintainingScrollAtEnd = pendingState; -+ clearScrollTargetSettle(state); - requestAnimationFrame(() => { - const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold"); -- const didScrollSinceRequest = state.scroll !== scrollAtRequest; -+ const lastIssued = state.lastIssuedScrollOffset; -+ const isSettlingOntoIssuedScroll = lastIssued !== void 0 && Math.abs(state.scroll - lastIssued) <= EDGE_POSITION_EPSILON; -+ const didScrollSinceRequest = state.scroll !== scrollAtRequest && !isSettlingOntoIssuedScroll; - if (isStillWithinThreshold || !didScrollSinceRequest) { - state.maintainingScrollAtEnd = activeState; - const scroller = refScroller.current; -@@ -1788,6 +1582,27 @@ function prepareMVCP(ctx, dataChanged) { - } - } - -+// src/utils/getEffectiveDrawDistance.ts -+var INITIAL_DRAW_DISTANCE = 50; -+function getEffectiveDrawDistance(ctx, mode) { -+ var _a3; -+ const drawDistance = ctx.state.props.drawDistance; -+ const initialScroll = ctx.state.initialScroll; -+ const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; -+ const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; -+ return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; -+} -+function scheduleFullDrawDistancePrewarm(ctx) { -+ const { state } = ctx; -+ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { -+ return; -+ } -+ state.scheduledWork.frame(() => { -+ var _a3; -+ return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); -+ }, "fullDrawDistancePrewarm"); -+} -+ - // src/utils/getScrollVelocity.ts - var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3; - var SCROLL_VELOCITY_HALF_LIFE_MS = 200; -@@ -2048,7 +1863,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { - } - } - function scrollTo(ctx, params) { -- var _a3, _b; -+ var _a3, _b, _c; - const state = ctx.state; - const { noScrollingTo, forceScroll, ...scrollTarget } = params; - const { -@@ -2070,6 +1885,7 @@ function scrollTo(ctx, params) { - if (!noScrollingTo) { - if (isInitialScroll) { - initialScrollCompletion.resetFlags(state); -+ clearScrollTargetSettle(state); - } - const averageSizeSnapshot = getAverageSizeSnapshot(state); - state.scrollingTo = { -@@ -2080,6 +1896,15 @@ function scrollTo(ctx, params) { - }; - if (!isInitialScroll) { - pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); -+ if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { -+ beginScrollTargetSettle(ctx, { -+ index: scrollTarget.index, -+ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, -+ viewPosition: scrollTarget.viewPosition -+ }); -+ } else { -+ clearScrollTargetSettle(state); -+ } - } - } - state.scrollPending = targetOffset; -@@ -2087,7 +1912,7 @@ function scrollTo(ctx, params) { - if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { - if (animated) { - if (state.scrollTargetPinnedRange) { -- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); -+ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); ++ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); + } +- const complete = () => { +- var _a4, _b2, _c2, _d, _e; +- const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; +- const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0; +- initialScrollWatchdog.clear(state); +- if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) { +- state.clearPreservedInitialScrollOnNextFinish = void 0; +- setInitialScrollSession(state); +- clearPreservedInitialScrollTargetTimeout(state); +- if (options == null ? void 0 : options.schedulePreservedTargetClear) { +- state.scheduledWork.timeout( +- () => { +- var _a5; +- if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) { +- return; +- } +- clearPreservedInitialScrollTarget(state); +- }, +- PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, +- "preservedInitialScroll" +- ); ++ scheduledWork.register("platformScrollCompletion", cancel); ++} ++ ++// src/utils/requestAdjust.ts ++function requestAdjust(ctx, positionDiff, source) { ++ const state = ctx.state; ++ if (Math.abs(positionDiff) > 0.1) { ++ const doit = () => { ++ { ++ state.scrollAdjustHandler.requestAdjust(positionDiff); ++ if (state.adjustingFromInitialMount) { ++ state.adjustingFromInitialMount--; ++ } } ++ }; ++ state.scroll += positionDiff; ++ state.scrollForNextCalculateItemsInView = void 0; ++ const readyToRender = peek$(ctx, "readyToRender"); ++ if (readyToRender) { ++ doit(); } else { - updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); -@@ -2100,6 +1925,316 @@ function scrollTo(ctx, params) { +- clearPreservedInitialScrollTarget(state); +- } +- if (options == null ? void 0 : options.recalculateItems) { +- recalculateSettledScroll(ctx); +- } +- setInitialRenderState(ctx, { didInitialScroll: true }); +- if (shouldReleaseDeferredPublicOnScroll) { +- releaseDeferredPublicOnScroll(ctx, finalScrollOffset); ++ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; ++ requestAnimationFrame(doit); + } +- (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); +- }; +- if (options == null ? void 0 : options.waitForCompletionFrame) { +- requestAnimationFrame(complete); +- return; } +- complete(); } +-// src/core/calculateOffsetForIndex.ts +-function calculateOffsetForIndex(ctx, index) { +// src/core/scrollTargetSettle.ts -+var SETTLE_POSITION_EPSILON = 1; ++var SETTLE_POSITION_EPSILON = 0.5; +var SETTLE_QUIET_PASSES_TO_RELEASE = 2; -+var SETTLE_MAX_MS = 1e3; -+var SETTLE_MAX_CORRECTIONS = 8; ++var SETTLE_TTL_MS = 500; ++function getSettleTargetOffset(ctx, settle, index, position) { ++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; ++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); ++} +function clearScrollTargetSettle(state) { + state.scrollTargetSettle = void 0; -+ state.scheduledWork.cancel("scrollTargetSettle"); -+ state.scheduledWork.cancel("scrollTargetSettleDeadline"); +} +function beginScrollTargetSettle(ctx, params) { -+ const state = ctx.state; + const state = ctx.state; +- return index !== void 0 ? state.positions[index] || 0 : 0; + const { index, viewOffset, viewPosition } = params; -+ const { alignItemsAtEnd, data, maintainScrollAtEnd } = state.props; -+ const hasEndAnchoring = !!maintainScrollAtEnd || !!alignItemsAtEnd; ++ const { data } = state.props; + const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1; -+ if (index < 0 || index >= data.length || isEndAlignedLastItem && hasEndAnchoring) { ++ if (index < 0 || index >= data.length || isEndAlignedLastItem) { + clearScrollTargetSettle(state); + return; + } -+ clearScrollTargetSettle(state); -+ const now = Date.now(); + state.scrollTargetSettle = { -+ corrections: 0, -+ deadline: now + SETTLE_MAX_MS, ++ expiresAt: Date.now() + SETTLE_TTL_MS, + id: getId(state, index), -+ measuredIndex: void 0, + quietPasses: 0, + viewOffset, + viewPosition + }; -+ state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline"); -+} -+function getSettleTargetOffset(ctx, settle, index, position) { -+ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition }; -+ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params); -+} -+function applyScrollTargetCorrection(ctx, id) { -+ var _a3; -+ const state = ctx.state; -+ const settle = state.scrollTargetSettle; -+ if (!settle || settle.id !== id) { -+ return; -+ } -+ const index = state.indexByKey.get(id); -+ const position = index === void 0 ? void 0 : state.positions[index]; -+ if (index === void 0 || position === void 0 || Date.now() > settle.deadline) { -+ clearScrollTargetSettle(state); -+ return; -+ } -+ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) { -+ clearScrollTargetSettle(state); -+ return; -+ } -+ settle.corrections++; -+ settle.measuredIndex = void 0; -+ scrollTo(ctx, { -+ animated: false, -+ index, -+ itemSize: getItemSizeAtIndex(ctx, index), -+ // Correcting where a scroll already landed is not a new imperative scroll: claiming the -+ // session would suppress the list's own handling of subsequent scroll events and re-arm -+ // this settle from inside its own correction. -+ noScrollingTo: true, -+ offset: position, -+ viewOffset: settle.viewOffset, -+ viewPosition: settle.viewPosition -+ }); -+ const scrollingTo = state.scrollingTo; -+ if (scrollingTo) { -+ scrollingTo.targetOffset = state.scrollPending; -+ scrollingTo.offset = position; -+ } -+ (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); +} -+function settleScrollTarget(ctx, options) { ++function settleScrollTarget(ctx) { + var _a3; + const state = ctx.state; + const settle = state.scrollTargetSettle; + if (!settle) { + return false; + } -+ const measured = options == null ? void 0 : options.minIndexSizeChanged; -+ if (measured !== void 0) { -+ settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured); -+ } -+ if (options == null ? void 0 : options.isCompensating) { -+ if (Date.now() > settle.deadline) { -+ clearScrollTargetSettle(state); -+ return false; -+ } -+ settle.quietPasses = 0; -+ return false; -+ } + const index = state.indexByKey.get(settle.id); + const position = index === void 0 ? void 0 : state.positions[index]; + if (index === void 0 || position === void 0) { + clearScrollTargetSettle(state); + return false; + } -+ if (Date.now() > settle.deadline) { ++ const now = Date.now(); ++ if (now > settle.expiresAt) { + clearScrollTargetSettle(state); + return false; + } -+ if (settle.measuredIndex === void 0 || settle.measuredIndex > index) { -+ return false; -+ } + const targetOffset = getSettleTargetOffset(ctx, settle, index, position); -+ if (Math.abs(targetOffset - state.scroll) <= SETTLE_POSITION_EPSILON) { -+ settle.measuredIndex = void 0; ++ const diff = targetOffset - state.scroll; ++ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) { + settle.quietPasses++; + if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) { + clearScrollTargetSettle(state); @@ -9640,10 +9673,20 @@ index 95465f2..4fb1e41 100644 + return false; + } + settle.quietPasses = 0; -+ state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle"); ++ settle.expiresAt = now + SETTLE_TTL_MS; ++ if (((_a3 = state.scrollingTo) == null ? void 0 : _a3.index) === index) { ++ state.scrollingTo.offset = position; ++ state.scrollingTo.targetOffset = targetOffset; ++ } ++ requestAdjust(ctx, diff); + return true; -+} -+ + } + +-// src/core/getStartOffsetAdjustment.ts +-function getStartOffsetAdjustment(ctx) { +- const { state } = ctx; +- const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0; +- return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0); +// src/core/cancelImperativeScroll.ts +function cancelScrollCompletionChecks({ scheduledWork }) { + scheduledWork.cancel("checkFinishedScrollFrame"); @@ -9665,8 +9708,13 @@ index 95465f2..4fb1e41 100644 + state.scrollTargetPinnedRange = void 0; + clearScrollTargetSettle(state); + settlePendingImperativeScroll(state); -+} -+ + } + +-// src/utils/getId.ts +-function getId(state, index) { +- const { data, keyExtractor } = state.props; +- if (!data) { +- return ""; +// src/core/deferredPublicOnScroll.ts +function withResolvedContentOffset(state, event, resolvedOffset) { + return { @@ -9691,8 +9739,370 @@ index 95465f2..4fb1e41 100644 + (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0 + ) + ); + } +- const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null; +- const id = ret; +- state.idCache[index] = id; +- return id; + } + +-// src/core/updateContentMetricsState.ts +-function getRawContentLength(ctx) { +- var _a3, _b, _c; +- const { state, values } = ctx; +- return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0); ++// src/core/initialScrollSession.ts ++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1; ++function hasInitialScrollSessionCompletion(completion) { ++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog)); + } +-function getAlignItemsAtEndPadding(ctx) { +- const { state } = ctx; +- const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0; +- return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0; ++function clearInitialScrollSession(state) { ++ state.initialScrollSession = void 0; ++ return void 0; + } +-function updateContentMetricsState(ctx) { +- var _a3; +- const { state } = ctx; +- const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0; +- const nextPadding = getAlignItemsAtEndPadding(ctx); +- if (previousPadding !== nextPadding) { +- const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated"; +- const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold")); +- if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) { +- state.scheduledWork.microtask(() => { +- if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) { +- set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx)); +- } +- }, "alignItemsAtEndPaddingFallback"); +- } else if (!isMaintainExpected) { +- set$(ctx, "alignItemsAtEndPadding", nextPadding); +- } ++function createInitialScrollSession(options) { ++ const { bootstrap, completion, kind, previousDataLength } = options; ++ return kind === "offset" ? { ++ completion, ++ kind, ++ previousDataLength ++ } : { ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }; ++} ++function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) { ++ var _a4, _b2; ++ if (!state.initialScrollSession) { ++ state.initialScrollSession = createInitialScrollSession({ ++ completion: {}, ++ kind, ++ previousDataLength: 0 ++ }); ++ } else if (state.initialScrollSession.kind !== kind) { ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0, ++ completion: state.initialScrollSession.completion, ++ kind, ++ previousDataLength: state.initialScrollSession.previousDataLength ++ }); + } ++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {}; ++ return state.initialScrollSession.completion; + } +- +-// src/core/addTotalSize.ts +-function addTotalSize(ctx, key, add, notifyTotalSize = true) { +- const state = ctx.state; +- const prevTotalSize = state.totalSize; +- let totalSize = state.totalSize; +- if (key === null) { +- totalSize = add; +- if (state.timeoutSetPaddingTop) { +- clearTimeout(state.timeoutSetPaddingTop); +- state.timeoutSetPaddingTop = void 0; ++var initialScrollCompletion = { ++ didDispatchNativeScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll); ++ }, ++ didRetrySilentInitialScroll(state) { ++ var _a3, _b; ++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll); ++ }, ++ markInitialScrollNativeDispatch(state) { ++ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true; ++ }, ++ markSilentInitialScrollRetry(state) { ++ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true; ++ }, ++ resetFlags(state) { ++ if (!state.initialScrollSession) { ++ return; + } +- } else { +- totalSize += add; ++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind); ++ completion.didDispatchNativeScroll = void 0; ++ completion.didRetrySilentInitialScroll = void 0; + } +- if (prevTotalSize !== totalSize) { +- { +- state.pendingTotalSize = void 0; +- state.totalSize = totalSize; +- if (notifyTotalSize) { +- set$(ctx, "totalSize", totalSize); +- } +- updateContentMetricsState(ctx); ++}; ++var initialScrollWatchdog = { ++ clear(state) { ++ initialScrollWatchdog.set(state, void 0); ++ }, ++ didReachTarget(newScroll, watchdog) { ++ const nextDistance = Math.abs(newScroll - watchdog.targetOffset); ++ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ get(state) { ++ var _a3, _b; ++ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog; ++ }, ++ hasNonZeroTargetOffset(targetOffset) { ++ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ isAtZeroTargetOffset(targetOffset) { ++ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET; ++ }, ++ set(state, watchdog) { ++ var _a3, _b; ++ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) { ++ return; + } +- } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) { +- set$(ctx, "totalSize", totalSize); ++ const completion = ensureInitialScrollSessionCompletion(state); ++ completion.watchdog = watchdog ? { ++ startScroll: watchdog.startScroll, ++ targetOffset: watchdog.targetOffset ++ } : void 0; ++ } ++}; ++function setInitialScrollSession(state, options = {}) { ++ var _a3, _b, _c, _d; ++ const existingSession = state.initialScrollSession; ++ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind; ++ const completion = existingSession == null ? void 0 : existingSession.completion; ++ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0; ++ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0; ++ if (!kind) { ++ return clearInitialScrollSession(state); + } +-} +- +-// src/core/setSize.ts +-function setSize(ctx, itemKey, size, notifyTotalSize = true) { +- const state = ctx.state; +- const { sizes } = state; +- const previousSize = sizes.get(itemKey); +- const diff = previousSize !== void 0 ? size - previousSize : size; +- if (diff !== 0) { +- addTotalSize(ctx, itemKey, diff, notifyTotalSize); ++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) { ++ return clearInitialScrollSession(state); + } +- sizes.set(itemKey, size); +-} +- +-// src/utils/helpers.ts +-function isFunction(obj) { +- return typeof obj === "function"; ++ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; ++ state.initialScrollSession = createInitialScrollSession({ ++ bootstrap, ++ completion, ++ kind, ++ previousDataLength ++ }); ++ return state.initialScrollSession; + } +-function isArray(obj) { +- return Array.isArray(obj); ++var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3; ++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250; ++function clearAdaptiveRenderExitTimeout(ctx) { ++ ctx.state.scheduledWork.cancel("adaptiveRender"); + } +-var warned = /* @__PURE__ */ new Set(); +-function warnDevOnce(id, text) { +- if (IS_DEV && !warned.has(id)) { +- warned.add(id); +- console.warn(`[legend-list] ${text}`); ++function scheduleAdaptiveRenderExit(ctx, exitDelay) { ++ const state = ctx.state; ++ clearAdaptiveRenderExitTimeout(ctx); ++ if (exitDelay <= 0) { ++ setAdaptiveRender(ctx, "normal", "scroll"); ++ } else { ++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); + } + } +-function roundSize(size) { +- return Math.floor(size * 8) / 8; +-} +-function isNullOrUndefined(value) { +- return value === null || value === void 0; +-} +-function getPadding(s, type) { +- var _a3, _b, _c; +- const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical; +- return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0; +-} +-function extractPadding(style, contentContainerStyle, type) { +- return getPadding(style, type) + getPadding(contentContainerStyle, type); +-} +-function findContainerId(ctx, key) { ++function setAdaptiveRender(ctx, mode, reason) { + var _a3, _b; +- const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key); +- if (directMatch !== void 0) { +- return directMatch; +- } +- const numContainers = peek$(ctx, "numContainers"); +- for (let i = 0; i < numContainers; i++) { +- const itemKey = peek$(ctx, `containerItemKey${i}`); +- if (itemKey === key) { +- return i; +- } ++ const previousMode = peek$(ctx, "adaptiveRender"); ++ if (previousMode !== mode) { ++ set$(ctx, "adaptiveRender", mode); ++ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); + } +- return -1; + } +- +-// src/utils/getItemSize.ts +-function getKnownOrFixedSize(ctx, key, index, data, resolved) { ++function resetAdaptiveRender(ctx) { + var _a3, _b; ++ clearAdaptiveRenderExitTimeout(ctx); ++ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal"; ++ if (peek$(ctx, "adaptiveRender") !== mode) { ++ setAdaptiveRender(ctx, mode, "initial"); + } +} ++function updateAdaptiveRender(ctx, scrollVelocity, options) { ++ var _a3, _b, _c; + const state = ctx.state; +- const { getFixedItemSize, getItemType } = state.props; +- let size = key ? state.sizesKnown.get(key) : void 0; +- if (size === void 0 && key && getFixedItemSize) { +- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; +- const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType); +- if (fixedSize !== void 0) { +- size = fixedSize + ctx.scrollAxisGap; +- state.sizesKnown.set(key, size); ++ const adaptiveRender = state.props.adaptiveRender; ++ const currentMode = peek$(ctx, "adaptiveRender"); ++ if (peek$(ctx, "readyToRender")) { ++ if (adaptiveRender) { ++ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ; ++ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ; ++ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ; ++ const threshold = currentMode === "light" ? exitVelocity : enterVelocity; ++ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal"; ++ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode; ++ if (nextMode !== previousMode) { ++ if (nextMode === "light") { ++ setAdaptiveRender(ctx, "light", "scroll"); ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } else if (currentMode === "light") { ++ scheduleAdaptiveRenderExit(ctx, exitDelay); ++ } ++ } ++ } else { ++ resetAdaptiveRender(ctx); + } + } +- return size; + } +-function getKnownOrFixedItemSize(ctx, index) { +- const key = getId(ctx.state, index); +- return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]); ++ ++// src/utils/getEffectiveDrawDistance.ts ++var INITIAL_DRAW_DISTANCE = 50; ++function getEffectiveDrawDistance(ctx, mode) { ++ var _a3; ++ const drawDistance = ctx.state.props.drawDistance; ++ const initialScroll = ctx.state.initialScroll; ++ const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0; ++ const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance; ++ return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance; + } +-function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) { +- for (let index = startIndex; index <= endIndex; index++) { +- if (getKnownOrFixedItemSize(ctx, index) === void 0) { +- return false; +- } ++function scheduleFullDrawDistancePrewarm(ctx) { ++ const { state } = ctx; ++ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) { ++ return; + } +- return true; ++ state.scheduledWork.frame(() => { ++ var _a3; ++ return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); ++ }, "fullDrawDistancePrewarm"); + } +-function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) { +- var _a3, _b, _c, _d; +- const state = ctx.state; +- const { +- sizes, +- averageSizes, +- props: { estimatedItemSize, getItemType }, +- scrollingTo +- } = state; +- const sizeKnown = state.sizesKnown.get(key); +- if (sizeKnown !== void 0) { +- return sizeKnown; +- } +- let size; +- const renderedSize = sizes.get(key); +- if (preferCachedSize) { +- if (renderedSize !== void 0) { +- return renderedSize; +- } +- } +- size = getKnownOrFixedSize(ctx, key, index, data, resolved); +- if (size !== void 0) { +- setSize(ctx, key, size, notifyTotalSize); +- return size; +- } +- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : ""; +- if (useAverageSize && !scrollingTo) { +- const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg; +- if (averageSizeForType !== void 0) { +- size = roundSize(averageSizeForType); +- } +- } +- if (size === void 0 && renderedSize !== void 0) { +- return renderedSize; +- } +- if (size === void 0 && useAverageSize && scrollingTo) { +- const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType]; +- if (averageSizeForType !== void 0) { +- size = roundSize(averageSizeForType); +- } +- } +- if (size === void 0) { +- size = estimatedItemSize + ctx.scrollAxisGap; + +// src/utils/setInitialRenderState.ts +function resetInitialRenderState(ctx, { @@ -9703,13 +10113,30 @@ index 95465f2..4fb1e41 100644 + if (resetLayout) { + state.didContainersLayout = false; + state.queuedInitialLayout = false; -+ } + } +- setSize(ctx, key, size, notifyTotalSize); +- return size; +-} +-function getItemSizeAtIndex(ctx, index) { +- if (index === void 0 || index < 0) { +- return void 0; + if (resetInitialScroll) { + state.didFinishInitialScroll = false; -+ } + } +- const targetId = getId(ctx.state, index); +- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]); + set$(ctx, "readyToRender", false); + resetAdaptiveRender(ctx); -+} + } +- +-// src/core/calculateOffsetWithOffsetPosition.ts +-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { +- var _a3; +- const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- let offset = offsetParam; +- if (viewOffset) { +- offset -= viewOffset; +function setInitialRenderState(ctx, { + didLayout, + didInitialScroll @@ -9721,10 +10148,29 @@ index 95465f2..4fb1e41 100644 + } = state; + if (didLayout) { + state.didContainersLayout = true; -+ } + } +- if (index !== void 0) { +- const startOffsetAdjustment = getStartOffsetAdjustment(ctx); +- if (startOffsetAdjustment) { +- offset += startOffsetAdjustment; +- } + if (didInitialScroll) { + state.didFinishInitialScroll = true; -+ } + } +- if (viewPosition !== void 0 && index !== void 0) { +- const dataLength = state.props.data.length; +- if (dataLength === 0) { +- return offset; +- } +- const isOutOfBounds = index < 0 || index >= dataLength; +- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0; +- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]); +- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap)); +- const trailingInset = getContentInsetEnd(ctx); +- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize); +- if (!isOutOfBounds && index === state.props.data.length - 1) { +- const footerSize = peek$(ctx, "footerSize") || 0; +- offset += footerSize; + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); + if (isReadyToRender && !peek$(ctx, "readyToRender")) { + set$(ctx, "readyToRender", true); @@ -9737,17 +10183,36 @@ index 95465f2..4fb1e41 100644 + if (onLoad) { + onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); + } -+ } -+ } -+} -+ + } + } +- return offset; + } + +-// src/core/clampScrollOffset.ts +-function clampScrollOffset(ctx, offset, scrollTarget) { +- const state = ctx.state; +- const contentSize = getContentSize(ctx); +- let clampedOffset = offset; +- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) { +- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); +- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; +- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; +- const maxOffset = baseMaxOffset + extraEndOffset; +- clampedOffset = Math.min(offset, maxOffset); +- } +- clampedOffset = Math.max(0, clampedOffset); +- return clampedOffset; +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; +function syncInitialScrollOffset(state, offset) { + state.scroll = offset; + state.scrollPending = offset; + state.scrollPrev = offset; -+} + } +- +-// src/core/finishScrollTo.ts +-function finishScrollTo(ctx) { +- var _a3, _b; +function clearPreservedInitialScrollTargetTimeout(state) { + state.scheduledWork.cancel("preservedInitialScroll"); +} @@ -9759,32 +10224,134 @@ index 95465f2..4fb1e41 100644 +} +function supersedeInitialScroll(ctx) { + var _a3, _b, _c; -+ const state = ctx.state; + const state = ctx.state; +- if (state == null ? void 0 : state.scrollingTo) { +- cancelScrollCompletionChecks(state); +- const resolvePendingScroll = state.pendingScrollResolve; +- state.pendingScrollResolve = void 0; +- const scrollingTo = state.scrollingTo; +- state.scrollHistory.length = 0; +- state.scrollingTo = void 0; +- state.scrollTargetPinnedRange = void 0; +- if (state.pendingTotalSize !== void 0) { +- addTotalSize(ctx, null, state.pendingTotalSize); +- } +- { +- state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); + const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; + if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) { + if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") { + cancelAnimationFrame(bootstrapInitialScroll.frameHandle); -+ } + } +- if (scrollingTo.isInitialScroll || state.initialScroll) { +- const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; +- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; +- finishInitialScroll(ctx, { +- onFinished: () => { +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); +- }, +- preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, +- recalculateItems: true, +- schedulePreservedTargetClear: shouldPreserveResizeTarget, +- syncObservedOffset: isOffsetSession, +- waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame +- }); +- return; + if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) { + cancelScrollCompletionChecks(state); + state.scrollingTo = void 0; + state.scrollTargetPinnedRange = void 0; -+ } + } +- recalculateSettledScroll(ctx); +- resolvePendingScroll == null ? void 0 : resolvePendingScroll(); + initialScrollCompletion.resetFlags(state); + setInitialScrollSession(state, { bootstrap: null }); + finishInitialScroll(ctx); -+ } -+} + } + } +- +-// src/core/doScrollTo.ts +-var SCROLL_END_IDLE_MS = 80; +-var SCROLL_END_MAX_MS = 1500; +-var SMOOTH_SCROLL_DURATION_MS = 320; +-var SCROLL_END_TARGET_EPSILON = 1; +-function doScrollTo(ctx, params) { +- var _a3, _b; +function finishInitialScroll(ctx, options) { + var _a3, _b, _c; -+ const state = ctx.state; + const state = ctx.state; +- const { animated, horizontal, offset } = params; +- state.scheduledWork.cancel("platformScrollCompletion"); +- const scroller = state.refScroller.current; +- const node = scroller == null ? void 0 : scroller.getScrollableNode(); +- if (!scroller || !node) { +- return; +- } +- const isAnimated = !!animated; +- const isHorizontal = !!horizontal; +- const contentSize = isHorizontal ? getContentSize(ctx) : void 0; +- const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0; +- const top = isHorizontal ? 0 : offset; +- scroller.scrollTo({ animated: isAnimated, x: left, y: top }); +- if (isAnimated) { +- const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null; +- listenForScrollEnd(ctx, { +- readOffset: () => scroller.getCurrentScrollOffset(), +- target, +- targetOffset: offset +- }); +- } else { +- state.scroll = offset; +- const targetToken = state.scrollingTo; +- state.scheduledWork.timeout( +- () => { +- if (targetToken === state.scrollingTo) { +- finishScrollTo(ctx); +- } +- }, +- 100, +- "platformScrollCompletion" +- ); +- } +-} +-function listenForScrollEnd(ctx, params) { +- const { readOffset, target, targetOffset } = params; +- if (!target) { +- finishScrollTo(ctx); +- return; +- } +- const supportsScrollEnd = "onscrollend" in target; +- let idleTimeout; +- let settled = false; +- const { scheduledWork, scrollingTo: targetToken } = ctx.state; +- const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS); +- const cleanup = () => { +- target.removeEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.removeEventListener("scrollend", onScrollEnd); +- } +- if (idleTimeout !== void 0) { +- clearTimeout(idleTimeout); +- } +- clearTimeout(maxTimeout); +- }; +- const cancel = () => { +- if (!settled) { +- settled = true; +- cleanup(); + if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { + syncInitialScrollOffset(state, options.resolvedOffset); + } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { + const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); + if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { + syncInitialScrollOffset(state, observedOffset); -+ } + } +- }; +- const finish = (reason) => { +- if (settled) return; +- if (targetToken !== ctx.state.scrollingTo) { +- scheduledWork.cancel("platformScrollCompletion"); +- return; + } + const complete = () => { + var _a4, _b2, _c2, _d, _e; @@ -9810,20 +10377,38 @@ index 95465f2..4fb1e41 100644 + } + } else { + clearPreservedInitialScrollTarget(state); -+ } + } +- const currentOffset = readOffset(); +- const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; +- if (reason === "scrollend" && !isNearTarget) { +- return; + if (options == null ? void 0 : options.recalculateItems) { + recalculateSettledScroll(ctx); -+ } + } +- scheduledWork.cancel("platformScrollCompletion"); +- finishScrollTo(ctx); +- }; +- const onScroll2 = () => { +- if (idleTimeout !== void 0) { +- clearTimeout(idleTimeout); + setInitialRenderState(ctx, { didInitialScroll: true }); + if (shouldReleaseDeferredPublicOnScroll) { + releaseDeferredPublicOnScroll(ctx, finalScrollOffset); -+ } + } +- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS); + (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); -+ }; + }; +- const onScrollEnd = () => finish("scrollend"); +- target.addEventListener("scroll", onScroll2); +- if (supportsScrollEnd) { +- target.addEventListener("scrollend", onScrollEnd); +- } else { +- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); + if (options == null ? void 0 : options.waitForCompletionFrame) { + requestAnimationFrame(complete); + return; -+ } + } +- scheduledWork.register("platformScrollCompletion", cancel); + complete(); +} + @@ -9831,204 +10416,81 @@ index 95465f2..4fb1e41 100644 +function calculateOffsetForIndex(ctx, index) { + const state = ctx.state; + return index !== void 0 ? state.positions[index] || 0 : 0; -+} -+ - // src/core/scrollToIndex.ts - function clampScrollIndex(index, dataLength) { - if (dataLength <= 0) { -@@ -4329,6 +4464,7 @@ function calculateItemsInView(ctx, params = {}) { - startIndex - }); - totalSize = getContentSize(ctx); -+ const minIndexSizeChangedThisPass = minIndexSizeChanged; - if (minIndexSizeChanged !== void 0) { - state.minIndexSizeChanged = void 0; + } + + // src/core/scrollRequestTracker.ts +@@ -1462,30 +1552,6 @@ function getScrollRequestTracker(ctx) { + return ctx.scrollRequestTracker; + } + +-// src/utils/requestAdjust.ts +-function requestAdjust(ctx, positionDiff, source) { +- const state = ctx.state; +- if (Math.abs(positionDiff) > 0.1) { +- const doit = () => { +- { +- state.scrollAdjustHandler.requestAdjust(positionDiff); +- if (state.adjustingFromInitialMount) { +- state.adjustingFromInitialMount--; +- } +- } +- }; +- state.scroll += positionDiff; +- state.scrollForNextCalculateItemsInView = void 0; +- const readyToRender = peek$(ctx, "readyToRender"); +- if (readyToRender) { +- doit(); +- } else { +- state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; +- requestAnimationFrame(doit); +- } +- } +-} +- + // src/core/doMaintainScrollAtEnd.ts + function finishMaintainScrollAtEnd(ctx) { + var _a3; +@@ -2120,7 +2186,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) { + } + } + function scrollTo(ctx, params) { +- var _a3, _b; ++ var _a3, _b, _c; + const state = ctx.state; + const { noScrollingTo, forceScroll, ...scrollTarget } = params; + const { +@@ -2152,6 +2218,15 @@ function scrollTo(ctx, params) { + }; + if (!isInitialScroll) { + pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index); ++ if (scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) { ++ beginScrollTargetSettle(ctx, { ++ index: scrollTarget.index, ++ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0, ++ viewPosition: scrollTarget.viewPosition ++ }); ++ } else { ++ clearScrollTargetSettle(state); ++ } } -@@ -4346,8 +4482,18 @@ function calculateItemsInView(ctx, params = {}) { + } + state.scrollPending = targetOffset; +@@ -2159,7 +2234,7 @@ function scrollTo(ctx, params) { + if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) { + if (animated) { + if (state.scrollTargetPinnedRange) { +- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state); ++ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state); + } + } else { + updateScroll(ctx, targetOffset, true, { markHasScrolled: false }); +@@ -4423,7 +4498,8 @@ function calculateItemsInView(ctx, params = {}) { const scrollBeforeMVCP = state.scroll; const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0; checkMVCP == null ? void 0 : checkMVCP(); - const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); -- if (didMVCPAdjustScroll) { -+ const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0; -+ const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP); -+ const mvcp = state.props.maintainVisibleContentPosition; -+ const isUnanchoredDataChange = dataChanged && !mvcp.data; -+ const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust; -+ if (!suppressInitialScrollSideEffects) { -+ settleScrollTarget(ctx, { -+ isCompensating, -+ minIndexSizeChanged: isUnanchoredDataChange ? 0 : minIndexSizeChangedThisPass -+ }); -+ } -+ if (didMVCPAdjust) { ++ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx); ++ const didMVCPAdjustScroll = didSettleScrollTarget || !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP); + if (didMVCPAdjustScroll) { updateScroll2(state.scroll); updateScrollRange(); - } -@@ -5985,6 +6131,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll - // src/components/ListComponentScrollView.tsx - var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style"; - var SCROLL_END_FALLBACK_MS = 200; -+var SCROLL_EXTENT_EPSILON = 1; -+var REACHABLE_RETRY_FRAMES = 30; - var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`; - function ensureScrollbarHiddenStyle() { - if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) { -@@ -6073,6 +6221,26 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - } - return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop; - }, [getMaxScrollOffset, horizontal, isWindowScroll]); -+ const reissueHandleRef = useRef(0); -+ const scrollUntilReachable = useCallback( -+ (offset, animated, run) => { -+ cancelAnimationFrame(reissueHandleRef.current); -+ let attempts = 0; -+ let previousMaxOffset = Number.NEGATIVE_INFINITY; -+ const attempt = () => { -+ const liveMaxOffset = getMaxScrollOffset(); -+ run(clampOffset(offset, liveMaxOffset)); -+ const isStillCommitting = liveMaxOffset > previousMaxOffset; -+ previousMaxOffset = liveMaxOffset; -+ if (!animated && isStillCommitting && Number.isFinite(offset) && offset - liveMaxOffset > SCROLL_EXTENT_EPSILON && ++attempts < REACHABLE_RETRY_FRAMES) { -+ reissueHandleRef.current = requestAnimationFrame(attempt); -+ } -+ }; -+ attempt(); -+ }, -+ [getMaxScrollOffset] -+ ); -+ useEffect(() => () => cancelAnimationFrame(reissueHandleRef.current), []); - const scrollToLocalOffset = useCallback( - (offset, animated) => { - const scrollElement = scrollRef.current; -@@ -6080,29 +6248,34 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - if (!target || typeof target.scrollTo !== "function") { - return; - } -- const maxOffset = getMaxScrollOffset(); -- const clampedOffset = clampOffset(offset, maxOffset); - const behavior = animated ? "smooth" : "auto"; - const options = { behavior }; - if (isWindowScroll) { - const scroll = getWindowScrollPosition(); - const listPos = getElementDocumentPosition(scrollElement, scroll); - const { left, top } = resolveWindowScrollTarget({ -- clampedOffset, -+ clampedOffset: clampOffset(offset, getMaxScrollOffset()), - horizontal, - listPos, - scroll - }); - options.left = left; - options.top = top; -- } else if (horizontal) { -- options.left = clampedOffset; -- } else { -- options.top = clampedOffset; -+ ctx.state.lastIssuedScrollOffset = clampOffset(offset, getMaxScrollOffset()); -+ target.scrollTo(options); -+ return; - } -- target.scrollTo(options); -+ scrollUntilReachable(offset, animated, (reachableOffset) => { -+ if (horizontal) { -+ options.left = reachableOffset; -+ } else { -+ options.top = reachableOffset; -+ } -+ ctx.state.lastIssuedScrollOffset = reachableOffset; -+ target.scrollTo(options); -+ }); - }, -- [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll] -+ [ctx, getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable] - ); - useImperativeHandle(ref, () => { - const api = { -@@ -6128,8 +6301,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({ - }, - scrollToEnd: (options = {}) => { - const { animated = true } = options; -- const endOffset = getMaxScrollOffset(); -- scrollToLocalOffset(endOffset, animated); -+ scrollToLocalOffset(getMaxScrollOffset(), animated); - }, - scrollToOffset: (params) => { - const { offset, animated = true } = params; -@@ -6478,6 +6650,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize - return props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; - } - function setHeaderSize(ctx, size) { -+ var _a3; - const { state } = ctx; - const previousHeaderSize = peek$(ctx, "headerSize") || 0; - const didChange = previousHeaderSize !== size; -@@ -6487,6 +6660,8 @@ function setHeaderSize(ctx, size) { - updateContentMetricsState(ctx); - if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { - requestAdjust(ctx, size - previousHeaderSize); -+ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) { -+ doMaintainScrollAtEnd(ctx); - } - } - state.didMeasureHeader = true; -@@ -7549,13 +7724,14 @@ function getRenderedItem(ctx, key) { - - // src/utils/normalizeMaintainScrollAtEnd.ts - function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) { -- var _a3, _b, _c, _d; -+ var _a3, _b, _c, _d, _e; - return { - animated: false, - onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true, - onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true, -- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true, -- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true -+ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true, -+ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true, -+ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true - }; - } - function normalizeMaintainScrollAtEnd(value) { -@@ -8267,6 +8443,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded - onScrollBeginDrag: (event) => { - var _a4, _b2; - prepareReachedEdgeForNextUserScroll(ctx); -+ clearScrollTargetSettle(state); - (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); - }, - onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx) -diff --git a/node_modules/@legendapp/list/reanimated.d.ts b/node_modules/@legendapp/list/reanimated.d.ts -index e504332..99337c1 100644 ---- a/node_modules/@legendapp/list/reanimated.d.ts -+++ b/node_modules/@legendapp/list/reanimated.d.ts -@@ -489,6 +489,12 @@ interface AlwaysRenderConfig { - interface MaintainScrollAtEndOnOptions { - dataChange?: boolean; - footerLayout?: boolean; -+ /** -+ * Keep the list pinned to the end when the header changes size. A header that measures larger -+ * than its estimate pushes every item down, which leaves a list that had already scrolled to -+ * its end short of it by the difference. -+ */ -+ headerLayout?: boolean; - itemLayout?: boolean; - layout?: boolean; - } -diff --git a/node_modules/@legendapp/list/section-list.d.ts b/node_modules/@legendapp/list/section-list.d.ts -index a790f7f..9a3c2ef 100644 ---- a/node_modules/@legendapp/list/section-list.d.ts -+++ b/node_modules/@legendapp/list/section-list.d.ts -@@ -545,6 +545,12 @@ interface AlwaysRenderConfig { - interface MaintainScrollAtEndOnOptions { - dataChange?: boolean; - footerLayout?: boolean; -+ /** -+ * Keep the list pinned to the end when the header changes size. A header that measures larger -+ * than its estimate pushes every item down, which leaves a list that had already scrolled to -+ * its end short of it by the difference. -+ */ -+ headerLayout?: boolean; - itemLayout?: boolean; - layout?: boolean; - } From 2529570d5eb50fdb60de50b28af636c0b319eb74 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 18 Aug 2026 21:59:03 -0400 Subject: [PATCH 31/38] fix(chat): let the list bootstrap own the first centered scroll An imperative scrollToItem on top of the list's own initialScrollIndex bootstrap is two authorities aiming at one target; measured on stock @legendapp/list 3.3.7 that fight is what makes a thread opened at a search hit land on the wrong message. Skip the call while the list has not yet rendered real content, which is exactly when the library still owns the target. --- shared/chat/conversation/list-area/index.tsx | 31 ++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/shared/chat/conversation/list-area/index.tsx b/shared/chat/conversation/list-area/index.tsx index 14194b30f21f..fdb7781d31b8 100644 --- a/shared/chat/conversation/list-area/index.tsx +++ b/shared/chat/conversation/list-area/index.tsx @@ -241,6 +241,21 @@ const useScrollToCentered = (p: { lastScrolledRef.current = undefined }, [datasetKey]) + // Has this mounted list ever committed real (ready, non-empty) message data before the current + // jump decision? That is the same question as "has the list already spent its own + // initialScrollIndex bootstrap": the library only re-arms that bootstrap on an empty -> non-empty + // data transition, which is exactly the transition that flips this ref. So when it is false the + // library is about to land the target itself (useInitialScrollIndex hands it {index, 0.5}) and an + // imperative scroll on top of it is a second authority aiming at the same target - measured on + // stock @legendapp/list 3.3.7, that fight is what makes an opened-at-a-hit thread land wrong. + // When it is true (jumping around inside a thread already on screen) the library will not re-aim + // and this call is the only thing that moves the list, so it has to stay. + // + // Deliberately NOT reset per dataset: every centered jump clears and refetches the thread, so + // resetting here would erase the very signal this reads. It is per mount, and the thread provider + // is keyed by conversation, so a new conversation gets a fresh one. + const hasRenderedNonEmptyRef = React.useRef(false) + React.useEffect(() => { if (!ready || centeredOrdinal === undefined) { lastScrolledRef.current = undefined @@ -251,8 +266,24 @@ const useScrollToCentered = (p: { return } lastScrolledRef.current = centeredOrdinal + if (!hasRenderedNonEmptyRef.current) { + // The list's own initialScrollIndex bootstrap owns this one. + return + } void listRef.current?.scrollToItem({animated: false, item: centeredOrdinal, viewPosition: 0.5}) }, [centeredOrdinal, datasetKey, listRef, messageOrdinals, ready]) + + // Declared AFTER the jump effect above, and that ordering is load-bearing: React runs one + // component's passive effects in declaration order, so the jump effect always reads this ref as + // it stood BEFORE the current commit. A thread's first content arriving in the same commit as its + // first centered target must not retroactively count as "already live" for that commit's own + // decision. Do not reorder these two, and do not hoist this into a layout effect, a render-phase + // assignment or a store flag. + React.useEffect(() => { + if (ready && messageOrdinals.length > 0) { + hasRenderedNonEmptyRef.current = true + } + }, [messageOrdinals, ready]) } const DesktopThreadWrapper = function DesktopThreadWrapper() { From d3ff0dcd6807679e04829afe636e029020eb3e79 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 18 Aug 2026 22:07:50 -0400 Subject: [PATCH 32/38] fix(chat): mirror the list's own non-empty-data predicate The centered-scroll guard tracked "has rendered content" with ready && length, but desktop passes ready: loaded while handing the list its data unconditionally. A live message landing after messagesClear and before the centered response flipped the library's hasHadNonEmptyData without flipping ours, so neither authority scrolled and the hit landed at the end of the thread. --- shared/chat/conversation/list-area/index.tsx | 25 +++++++++++++------- shared/chat/conversation/thread-context.tsx | 8 ++++--- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/shared/chat/conversation/list-area/index.tsx b/shared/chat/conversation/list-area/index.tsx index fdb7781d31b8..b81d0eb6e349 100644 --- a/shared/chat/conversation/list-area/index.tsx +++ b/shared/chat/conversation/list-area/index.tsx @@ -241,13 +241,13 @@ const useScrollToCentered = (p: { lastScrolledRef.current = undefined }, [datasetKey]) - // Has this mounted list ever committed real (ready, non-empty) message data before the current - // jump decision? That is the same question as "has the list already spent its own - // initialScrollIndex bootstrap": the library only re-arms that bootstrap on an empty -> non-empty - // data transition, which is exactly the transition that flips this ref. So when it is false the - // library is about to land the target itself (useInitialScrollIndex hands it {index, 0.5}) and an - // imperative scroll on top of it is a second authority aiming at the same target - measured on - // stock @legendapp/list 3.3.7, that fight is what makes an opened-at-a-hit thread land wrong. + // Has this mounted list ever committed non-empty message data before the current jump decision? + // That is the same question as "has the list already spent its own initialScrollIndex bootstrap": + // the library only re-arms that bootstrap on an empty -> non-empty data transition, which is + // exactly the transition that flips this ref. So when it is false the library is about to land the + // target itself (useInitialScrollIndex hands it {index, 0.5}) and an imperative scroll on top of + // it is a second authority aiming at the same target - measured on stock @legendapp/list 3.3.7, + // that fight is what makes an opened-at-a-hit thread land wrong. // When it is true (jumping around inside a thread already on screen) the library will not re-aim // and this call is the only thing that moves the list, so it has to stay. // @@ -279,11 +279,18 @@ const useScrollToCentered = (p: { // first centered target must not retroactively count as "already live" for that commit's own // decision. Do not reorder these two, and do not hoist this into a layout effect, a render-phase // assignment or a store flag. + // + // The predicate is deliberately the library's own (`dataLength > 0`, see its + // `shouldUseLatestInitialScroll`) and must stay in sync with it - do NOT add a condition the + // library does not have. `ready` in particular is wrong here: desktop passes `ready: loaded` but + // hands the list `data={messageOrdinals}` unconditionally, so a live message arriving after + // messagesClear but before the centered response would flip the library's own flag while leaving + // this one false, and then neither authority scrolls. React.useEffect(() => { - if (ready && messageOrdinals.length > 0) { + if (messageOrdinals.length > 0) { hasRenderedNonEmptyRef.current = true } - }, [messageOrdinals, ready]) + }, [messageOrdinals]) } const DesktopThreadWrapper = function DesktopThreadWrapper() { diff --git a/shared/chat/conversation/thread-context.tsx b/shared/chat/conversation/thread-context.tsx index 598b2c74fa11..9a30b23a5214 100644 --- a/shared/chat/conversation/thread-context.tsx +++ b/shared/chat/conversation/thread-context.tsx @@ -96,9 +96,11 @@ ConversationThreadIDContext.displayName = 'ConversationThreadIDContext' export type ConversationThreadState = { accountsInfoMap: Map - // Bumped on every messagesClear. The desktop list remounts on it: LegendList cannot recover - // from a non-empty -> empty -> non-empty data transition (it resets its layout state and waits - // for a container layout event that never comes), so the thread renders blank forever. + // Bumped on every messagesClear, and fed to the list as its dataKey (not as a React key - the + // list is not remounted). LegendList cannot recover from a non-empty -> empty -> non-empty data + // transition on its own (it resets its layout state and waits for a container layout event that + // never comes), so the thread renders blank forever; the dataKey change is what tells it this is + // a new dataset and makes it reset rather than wait. clearVersion: number explodingMode: number flipStatusMap: Map From cee1628bbc438cd77617d8a42f386d37f08a67ff Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 18 Aug 2026 22:58:28 -0400 Subject: [PATCH 33/38] fix(chat): rebuild the legend-list patch with the header end-anchor trigger Opening a thread landed 52px above its newest message on roughly half the opens of a given conversation. SpecialTopMessage renders at its minHeight 100 placeholder and measures 152 once the thread's load state resolves; when that measurement arrives after the at-end initial scroll has resolved its target, every message moves down by 52 and nothing brings the reader back. That case had a fork fix on 3.3.5 - a headerLayout trigger for maintainScrollAtEnd - and the fast-forward to 3.3.7 in b239d85835 dropped it. Nothing upstream replaced it: maintainVisibleContentPosition's size anchor declines while a scroll is in flight, which is exactly when a header first measures, and doMaintainScrollAtEnd's scroll is refused outright while another scroll is in flight and never replayed. The fork gets the trigger back, re-aiming the bootstrap initial scroll before asking the end anchor. The desktop list already passes maintainScrollAtEnd={true}, which enables every trigger, so no app change is needed. Measured: chat-thread-bottom failed 2 of 6 runs before and passed 6 of 6 after. --- shared/patches/@legendapp+list+3.3.7.patch | 694 +++++++++++++++------ 1 file changed, 506 insertions(+), 188 deletions(-) diff --git a/shared/patches/@legendapp+list+3.3.7.patch b/shared/patches/@legendapp+list+3.3.7.patch index 0afd47f5781a..7f7fdf1dcd1e 100644 --- a/shared/patches/@legendapp+list+3.3.7.patch +++ b/shared/patches/@legendapp+list+3.3.7.patch @@ -1,5 +1,39 @@ +diff --git a/node_modules/@legendapp/list/animated.d.ts b/node_modules/@legendapp/list/animated.d.ts +index 14beb98..56eefce 100644 +--- a/node_modules/@legendapp/list/animated.d.ts ++++ b/node_modules/@legendapp/list/animated.d.ts +@@ -488,6 +488,12 @@ interface AlwaysRenderConfig { + interface MaintainScrollAtEndOnOptions { + dataChange?: boolean; + footerLayout?: boolean; ++ /** ++ * Keep the list pinned to the end when the header changes size. A header that measures larger ++ * than its estimate pushes every item down, which leaves a list that had already scrolled to ++ * its end short of it by the difference. ++ */ ++ headerLayout?: boolean; + itemLayout?: boolean; + layout?: boolean; + } +diff --git a/node_modules/@legendapp/list/react-native.d.ts b/node_modules/@legendapp/list/react-native.d.ts +index ce1fe00..9a4311c 100644 +--- a/node_modules/@legendapp/list/react-native.d.ts ++++ b/node_modules/@legendapp/list/react-native.d.ts +@@ -488,6 +488,12 @@ interface AlwaysRenderConfig { + interface MaintainScrollAtEndOnOptions { + dataChange?: boolean; + footerLayout?: boolean; ++ /** ++ * Keep the list pinned to the end when the header changes size. A header that measures larger ++ * than its estimate pushes every item down, which leaves a list that had already scrolled to ++ * its end short of it by the difference. ++ */ ++ headerLayout?: boolean; + itemLayout?: boolean; + layout?: boolean; + } diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js -index 6f41da3..0d7357b 100644 +index 6f41da3..1108caf 100644 --- a/node_modules/@legendapp/list/react-native.js +++ b/node_modules/@legendapp/list/react-native.js @@ -458,171 +458,279 @@ var EDGE_POSITION_EPSILON = 1; @@ -468,25 +502,26 @@ index 6f41da3..0d7357b 100644 - setAdaptiveRender(ctx, "normal", "scroll"); - } else { - state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); -+ } -+ ); - } - } +- } +-} -function setAdaptiveRender(ctx, mode, reason) { - var _a3, _b; - const previousMode = peek$(ctx, "adaptiveRender"); - if (previousMode !== mode) { - set$(ctx, "adaptiveRender", mode); - (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); -- } ++ } ++ ); + } + } +-function resetAdaptiveRender(ctx) { + +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); - } --function resetAdaptiveRender(ctx) { ++} + +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { @@ -539,7 +574,7 @@ index 6f41da3..0d7357b 100644 + state.scrollTargetPinnedRange = void 0; + if (state.pendingTotalSize !== void 0) { + addTotalSize(ctx, null, state.pendingTotalSize); - } ++ } + if (PlatformAdjustBreaksScroll) { + state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); + } @@ -557,7 +592,7 @@ index 6f41da3..0d7357b 100644 + waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame + }); + return; -+ } + } + recalculateSettledScroll(ctx); + resolvePendingScroll == null ? void 0 : resolvePendingScroll(); } @@ -722,6 +757,12 @@ index 6f41da3..0d7357b 100644 - setAdaptiveRender(ctx, "normal", "ready"); - if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { - scheduleFullDrawDistancePrewarm(ctx); +- } +- if (!state.didLoad) { +- state.didLoad = true; +- if (onLoad) { +- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); +- } + const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; + state.initialScrollSession = createInitialScrollSession({ + bootstrap, @@ -743,12 +784,7 @@ index 6f41da3..0d7357b 100644 + if (options == null ? void 0 : options.onlyIfAligned) { + if (!(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) || scrollingTo.animated) { + return; - } -- if (!state.didLoad) { -- state.didLoad = true; -- if (onLoad) { -- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); -- } ++ } + if (!getResolvedScrollCompletionState(ctx, scrollingTo).isAtResolvedTarget) { + return; } @@ -1296,14 +1332,19 @@ index 6f41da3..0d7357b 100644 - let offset = offsetParam; - if (viewOffset) { - offset -= viewOffset; -- } ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ return false; + } - if (index !== void 0) { - const startOffsetAdjustment = getStartOffsetAdjustment(ctx); - if (startOffsetAdjustment) { - offset += startOffsetAdjustment; - } -+ const settle = state.scrollTargetSettle; -+ if (!settle) { ++ const index = state.indexByKey.get(settle.id); ++ const position = index === void 0 ? void 0 : state.positions[index]; ++ if (index === void 0 || position === void 0) { ++ clearScrollTargetSettle(state); + return false; } - if (viewPosition !== void 0 && index !== void 0) { @@ -1320,12 +1361,6 @@ index 6f41da3..0d7357b 100644 - if (!isOutOfBounds && index === state.props.data.length - 1) { - const footerSize = peek$(ctx, "footerSize") || 0; - offset += footerSize; -+ const index = state.indexByKey.get(settle.id); -+ const position = index === void 0 ? void 0 : state.positions[index]; -+ if (index === void 0 || position === void 0) { -+ clearScrollTargetSettle(state); -+ return false; -+ } + const now = Date.now(); + if (now > settle.expiresAt) { + clearScrollTargetSettle(state); @@ -1596,6 +1631,9 @@ index 6f41da3..0d7357b 100644 +}) { const { state } = ctx; - return !!((_a3 = state.scrollingTo) == null ? void 0 : _a3.isInitialScroll) && state.props.data.length > 0 && getContentSize(ctx) <= state.scrollLength && state.scrollPending <= INITIAL_SCROLL_ZERO_TARGET_EPSILON; +-} +-function isEndAlignedLastItemTarget(ctx, scrollingTo) { +- return scrollingTo.index === ctx.state.props.data.length - 1 && scrollingTo.viewPosition === 1; + const { + loadStartTime, + props: { onLoad } @@ -1621,8 +1659,12 @@ index 6f41da3..0d7357b 100644 + } + } } --function isEndAlignedLastItemTarget(ctx, scrollingTo) { -- return scrollingTo.index === ctx.state.props.data.length - 1 && scrollingTo.viewPosition === 1; +-function getCurrentTargetOffset(ctx, scrollingTo) { +- var _a3; +- const index = scrollingTo.index; +- const shouldRecomputeEndTarget = isEndAlignedLastItemTarget(ctx, scrollingTo); +- const requestedTargetOffset = shouldRecomputeEndTarget && index !== void 0 ? calculateOffsetWithOffsetPosition(ctx, calculateOffsetForIndex(ctx, index), scrollingTo) : (_a3 = scrollingTo.targetOffset) != null ? _a3 : clampScrollOffset(ctx, scrollingTo.offset - (scrollingTo.viewOffset || 0), scrollingTo); +- return clampScrollOffset(ctx, requestedTargetOffset, scrollingTo); + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -1631,15 +1673,6 @@ index 6f41da3..0d7357b 100644 + state.scrollPending = offset; + state.scrollPrev = offset; } --function getCurrentTargetOffset(ctx, scrollingTo) { -- var _a3; -- const index = scrollingTo.index; -- const shouldRecomputeEndTarget = isEndAlignedLastItemTarget(ctx, scrollingTo); -- const requestedTargetOffset = shouldRecomputeEndTarget && index !== void 0 ? calculateOffsetWithOffsetPosition(ctx, calculateOffsetForIndex(ctx, index), scrollingTo) : (_a3 = scrollingTo.targetOffset) != null ? _a3 : clampScrollOffset(ctx, scrollingTo.offset - (scrollingTo.viewOffset || 0), scrollingTo); -- return clampScrollOffset(ctx, requestedTargetOffset, scrollingTo); -+function clearPreservedInitialScrollTargetTimeout(state) { -+ state.scheduledWork.cancel("preservedInitialScroll"); - } -function getResolvedScrollCompletionState(ctx, scrollingTo) { - const { state } = ctx; - const scroll = state.scrollPending; @@ -1654,11 +1687,8 @@ index 6f41da3..0d7357b 100644 - clampedTargetOffset, - isAtResolvedTarget: Math.abs(scroll - maxOffset) < 1 && (diff1 < 1 || canUseAdjustedCompletion && diff2 < 1) - }; -+function clearPreservedInitialScrollTarget(state) { -+ clearPreservedInitialScrollTargetTimeout(state); -+ state.clearPreservedInitialScrollOnNextFinish = void 0; -+ state.initialScroll = void 0; -+ setInitialScrollSession(state); ++function clearPreservedInitialScrollTargetTimeout(state) { ++ state.scheduledWork.cancel("preservedInitialScroll"); } -function checkFinishedScrollFrame(ctx) { - const scrollingTo = ctx.state.scrollingTo; @@ -1672,6 +1702,20 @@ index 6f41da3..0d7357b 100644 - scrollingTo - })) { - finishScrollTo(ctx); +- } ++function clearPreservedInitialScrollTarget(state) { ++ clearPreservedInitialScrollTargetTimeout(state); ++ state.clearPreservedInitialScrollOnNextFinish = void 0; ++ state.initialScroll = void 0; ++ setInitialScrollSession(state); + } +-function scrollToFallbackOffset(ctx, offset) { +- var _a3; +- (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({ +- animated: false, +- x: ctx.state.props.horizontal ? offset : 0, +- y: ctx.state.props.horizontal ? 0 : offset +- }); +function supersedeInitialScroll(ctx) { + var _a3, _b, _c; + const state = ctx.state; @@ -1688,16 +1732,8 @@ index 6f41da3..0d7357b 100644 + initialScrollCompletion.resetFlags(state); + setInitialScrollSession(state, { bootstrap: null }); + finishInitialScroll(ctx); - } ++ } } --function scrollToFallbackOffset(ctx, offset) { -- var _a3; -- (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({ -- animated: false, -- x: ctx.state.props.horizontal ? offset : 0, -- y: ctx.state.props.horizontal ? 0 : offset -- }); --} -function checkFinishedScrollFallback(ctx) { +function finishInitialScroll(ctx, options) { + var _a3, _b, _c; @@ -1787,11 +1823,11 @@ index 6f41da3..0d7357b 100644 + } + if (options == null ? void 0 : options.recalculateItems) { + recalculateSettledScroll(ctx); - } ++ } + setInitialRenderState(ctx, { didInitialScroll: true }); + if (shouldReleaseDeferredPublicOnScroll) { + releaseDeferredPublicOnScroll(ctx, finalScrollOffset); -+ } + } + (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); }; - scheduleFallbackCheck(initialDelay); @@ -1936,8 +1972,44 @@ index 6f41da3..0d7357b 100644 if (didMVCPAdjustScroll) { updateScroll2(state.scroll); updateScrollRange(); +@@ -5940,6 +6016,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize + return Platform.OS === "web" && props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; + } + function setHeaderSize(ctx, size) { ++ var _a3; + const { state } = ctx; + const previousHeaderSize = peek$(ctx, "headerSize") || 0; + const didChange = previousHeaderSize !== size; +@@ -5949,6 +6026,9 @@ function setHeaderSize(ctx, size) { + updateContentMetricsState(ctx); + if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { + requestAdjust(ctx, size - previousHeaderSize); ++ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) { ++ handleBootstrapInitialScrollLayoutChange(ctx); ++ doMaintainScrollAtEnd(ctx); + } + } + state.didMeasureHeader = true; +@@ -7036,13 +7116,14 @@ function getRenderedItem(ctx, key, containerId) { + + // src/utils/normalizeMaintainScrollAtEnd.ts + function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) { +- var _a3, _b, _c, _d; ++ var _a3, _b, _c, _d, _e; + return { + animated: false, + onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true, + onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true, +- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true, +- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true ++ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true, ++ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true, ++ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true + }; + } + function normalizeMaintainScrollAtEnd(value) { diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs -index 4ad913c..73aea1f 100644 +index 4ad913c..8c313ee 100644 --- a/node_modules/@legendapp/list/react-native.mjs +++ b/node_modules/@legendapp/list/react-native.mjs @@ -437,171 +437,279 @@ var EDGE_POSITION_EPSILON = 1; @@ -2406,25 +2478,26 @@ index 4ad913c..73aea1f 100644 - setAdaptiveRender(ctx, "normal", "scroll"); - } else { - state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender"); -+ } -+ ); - } - } +- } +-} -function setAdaptiveRender(ctx, mode, reason) { - var _a3, _b; - const previousMode = peek$(ctx, "adaptiveRender"); - if (previousMode !== mode) { - set$(ctx, "adaptiveRender", mode); - (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason); -- } ++ } ++ ); + } + } +-function resetAdaptiveRender(ctx) { + +// src/utils/checkThresholds.ts +function checkThresholds(ctx, allowedEdge) { + const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate; + checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck); + checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck); - } --function resetAdaptiveRender(ctx) { ++} + +// src/core/recalculateSettledScroll.ts +function recalculateSettledScroll(ctx) { @@ -2477,7 +2550,7 @@ index 4ad913c..73aea1f 100644 + state.scrollTargetPinnedRange = void 0; + if (state.pendingTotalSize !== void 0) { + addTotalSize(ctx, null, state.pendingTotalSize); - } ++ } + if (PlatformAdjustBreaksScroll) { + state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); + } @@ -2495,7 +2568,7 @@ index 4ad913c..73aea1f 100644 + waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame + }); + return; -+ } + } + recalculateSettledScroll(ctx); + resolvePendingScroll == null ? void 0 : resolvePendingScroll(); } @@ -2660,6 +2733,12 @@ index 4ad913c..73aea1f 100644 - setAdaptiveRender(ctx, "normal", "ready"); - if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) { - scheduleFullDrawDistancePrewarm(ctx); +- } +- if (!state.didLoad) { +- state.didLoad = true; +- if (onLoad) { +- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); +- } + const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0; + state.initialScrollSession = createInitialScrollSession({ + bootstrap, @@ -2681,12 +2760,7 @@ index 4ad913c..73aea1f 100644 + if (options == null ? void 0 : options.onlyIfAligned) { + if (!(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) || scrollingTo.animated) { + return; - } -- if (!state.didLoad) { -- state.didLoad = true; -- if (onLoad) { -- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime }); -- } ++ } + if (!getResolvedScrollCompletionState(ctx, scrollingTo).isAtResolvedTarget) { + return; } @@ -3234,14 +3308,19 @@ index 4ad913c..73aea1f 100644 - let offset = offsetParam; - if (viewOffset) { - offset -= viewOffset; -- } ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ return false; + } - if (index !== void 0) { - const startOffsetAdjustment = getStartOffsetAdjustment(ctx); - if (startOffsetAdjustment) { - offset += startOffsetAdjustment; - } -+ const settle = state.scrollTargetSettle; -+ if (!settle) { ++ const index = state.indexByKey.get(settle.id); ++ const position = index === void 0 ? void 0 : state.positions[index]; ++ if (index === void 0 || position === void 0) { ++ clearScrollTargetSettle(state); + return false; } - if (viewPosition !== void 0 && index !== void 0) { @@ -3258,12 +3337,6 @@ index 4ad913c..73aea1f 100644 - if (!isOutOfBounds && index === state.props.data.length - 1) { - const footerSize = peek$(ctx, "footerSize") || 0; - offset += footerSize; -+ const index = state.indexByKey.get(settle.id); -+ const position = index === void 0 ? void 0 : state.positions[index]; -+ if (index === void 0 || position === void 0) { -+ clearScrollTargetSettle(state); -+ return false; -+ } + const now = Date.now(); + if (now > settle.expiresAt) { + clearScrollTargetSettle(state); @@ -3534,6 +3607,9 @@ index 4ad913c..73aea1f 100644 +}) { const { state } = ctx; - return !!((_a3 = state.scrollingTo) == null ? void 0 : _a3.isInitialScroll) && state.props.data.length > 0 && getContentSize(ctx) <= state.scrollLength && state.scrollPending <= INITIAL_SCROLL_ZERO_TARGET_EPSILON; +-} +-function isEndAlignedLastItemTarget(ctx, scrollingTo) { +- return scrollingTo.index === ctx.state.props.data.length - 1 && scrollingTo.viewPosition === 1; + const { + loadStartTime, + props: { onLoad } @@ -3559,8 +3635,12 @@ index 4ad913c..73aea1f 100644 + } + } } --function isEndAlignedLastItemTarget(ctx, scrollingTo) { -- return scrollingTo.index === ctx.state.props.data.length - 1 && scrollingTo.viewPosition === 1; +-function getCurrentTargetOffset(ctx, scrollingTo) { +- var _a3; +- const index = scrollingTo.index; +- const shouldRecomputeEndTarget = isEndAlignedLastItemTarget(ctx, scrollingTo); +- const requestedTargetOffset = shouldRecomputeEndTarget && index !== void 0 ? calculateOffsetWithOffsetPosition(ctx, calculateOffsetForIndex(ctx, index), scrollingTo) : (_a3 = scrollingTo.targetOffset) != null ? _a3 : clampScrollOffset(ctx, scrollingTo.offset - (scrollingTo.viewOffset || 0), scrollingTo); +- return clampScrollOffset(ctx, requestedTargetOffset, scrollingTo); + +// src/core/finishInitialScroll.ts +var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3; @@ -3569,15 +3649,6 @@ index 4ad913c..73aea1f 100644 + state.scrollPending = offset; + state.scrollPrev = offset; } --function getCurrentTargetOffset(ctx, scrollingTo) { -- var _a3; -- const index = scrollingTo.index; -- const shouldRecomputeEndTarget = isEndAlignedLastItemTarget(ctx, scrollingTo); -- const requestedTargetOffset = shouldRecomputeEndTarget && index !== void 0 ? calculateOffsetWithOffsetPosition(ctx, calculateOffsetForIndex(ctx, index), scrollingTo) : (_a3 = scrollingTo.targetOffset) != null ? _a3 : clampScrollOffset(ctx, scrollingTo.offset - (scrollingTo.viewOffset || 0), scrollingTo); -- return clampScrollOffset(ctx, requestedTargetOffset, scrollingTo); -+function clearPreservedInitialScrollTargetTimeout(state) { -+ state.scheduledWork.cancel("preservedInitialScroll"); - } -function getResolvedScrollCompletionState(ctx, scrollingTo) { - const { state } = ctx; - const scroll = state.scrollPending; @@ -3592,11 +3663,8 @@ index 4ad913c..73aea1f 100644 - clampedTargetOffset, - isAtResolvedTarget: Math.abs(scroll - maxOffset) < 1 && (diff1 < 1 || canUseAdjustedCompletion && diff2 < 1) - }; -+function clearPreservedInitialScrollTarget(state) { -+ clearPreservedInitialScrollTargetTimeout(state); -+ state.clearPreservedInitialScrollOnNextFinish = void 0; -+ state.initialScroll = void 0; -+ setInitialScrollSession(state); ++function clearPreservedInitialScrollTargetTimeout(state) { ++ state.scheduledWork.cancel("preservedInitialScroll"); } -function checkFinishedScrollFrame(ctx) { - const scrollingTo = ctx.state.scrollingTo; @@ -3610,6 +3678,20 @@ index 4ad913c..73aea1f 100644 - scrollingTo - })) { - finishScrollTo(ctx); +- } ++function clearPreservedInitialScrollTarget(state) { ++ clearPreservedInitialScrollTargetTimeout(state); ++ state.clearPreservedInitialScrollOnNextFinish = void 0; ++ state.initialScroll = void 0; ++ setInitialScrollSession(state); + } +-function scrollToFallbackOffset(ctx, offset) { +- var _a3; +- (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({ +- animated: false, +- x: ctx.state.props.horizontal ? offset : 0, +- y: ctx.state.props.horizontal ? 0 : offset +- }); +function supersedeInitialScroll(ctx) { + var _a3, _b, _c; + const state = ctx.state; @@ -3626,16 +3708,8 @@ index 4ad913c..73aea1f 100644 + initialScrollCompletion.resetFlags(state); + setInitialScrollSession(state, { bootstrap: null }); + finishInitialScroll(ctx); - } ++ } } --function scrollToFallbackOffset(ctx, offset) { -- var _a3; -- (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({ -- animated: false, -- x: ctx.state.props.horizontal ? offset : 0, -- y: ctx.state.props.horizontal ? 0 : offset -- }); --} -function checkFinishedScrollFallback(ctx) { +function finishInitialScroll(ctx, options) { + var _a3, _b, _c; @@ -3725,11 +3799,11 @@ index 4ad913c..73aea1f 100644 + } + if (options == null ? void 0 : options.recalculateItems) { + recalculateSettledScroll(ctx); - } ++ } + setInitialRenderState(ctx, { didInitialScroll: true }); + if (shouldReleaseDeferredPublicOnScroll) { + releaseDeferredPublicOnScroll(ctx, finalScrollOffset); -+ } + } + (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options); }; - scheduleFallbackCheck(initialDelay); @@ -3874,8 +3948,61 @@ index 4ad913c..73aea1f 100644 if (didMVCPAdjustScroll) { updateScroll2(state.scroll); updateScrollRange(); +@@ -5919,6 +5995,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize + return Platform.OS === "web" && props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; + } + function setHeaderSize(ctx, size) { ++ var _a3; + const { state } = ctx; + const previousHeaderSize = peek$(ctx, "headerSize") || 0; + const didChange = previousHeaderSize !== size; +@@ -5928,6 +6005,9 @@ function setHeaderSize(ctx, size) { + updateContentMetricsState(ctx); + if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { + requestAdjust(ctx, size - previousHeaderSize); ++ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) { ++ handleBootstrapInitialScrollLayoutChange(ctx); ++ doMaintainScrollAtEnd(ctx); + } + } + state.didMeasureHeader = true; +@@ -7015,13 +7095,14 @@ function getRenderedItem(ctx, key, containerId) { + + // src/utils/normalizeMaintainScrollAtEnd.ts + function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) { +- var _a3, _b, _c, _d; ++ var _a3, _b, _c, _d, _e; + return { + animated: false, + onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true, + onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true, +- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true, +- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true ++ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true, ++ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true, ++ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true + }; + } + function normalizeMaintainScrollAtEnd(value) { +diff --git a/node_modules/@legendapp/list/react-native.web.d.ts b/node_modules/@legendapp/list/react-native.web.d.ts +index b6c7481..ace847e 100644 +--- a/node_modules/@legendapp/list/react-native.web.d.ts ++++ b/node_modules/@legendapp/list/react-native.web.d.ts +@@ -511,6 +511,12 @@ interface AlwaysRenderConfig { + interface MaintainScrollAtEndOnOptions { + dataChange?: boolean; + footerLayout?: boolean; ++ /** ++ * Keep the list pinned to the end when the header changes size. A header that measures larger ++ * than its estimate pushes every item down, which leaves a list that had already scrolled to ++ * its end short of it by the difference. ++ */ ++ headerLayout?: boolean; + itemLayout?: boolean; + layout?: boolean; + } diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js -index b2240b5..69282b3 100644 +index b2240b5..cbff6eb 100644 --- a/node_modules/@legendapp/list/react-native.web.js +++ b/node_modules/@legendapp/list/react-native.web.js @@ -430,171 +430,271 @@ var EDGE_POSITION_EPSILON = 1; @@ -4368,7 +4495,7 @@ index b2240b5..69282b3 100644 + } + { + state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); - } ++ } + if (scrollingTo.isInitialScroll || state.initialScroll) { + const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; + const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; @@ -4383,7 +4510,7 @@ index b2240b5..69282b3 100644 + waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame + }); + return; -+ } + } + recalculateSettledScroll(ctx); + resolvePendingScroll == null ? void 0 : resolvePendingScroll(); } @@ -4555,22 +4682,11 @@ index b2240b5..69282b3 100644 - initialScrollCompletion.resetFlags(state); - setInitialScrollSession(state, { bootstrap: null }); - finishInitialScroll(ctx); -- } --} --function finishInitialScroll(ctx, options) { -- var _a3, _b, _c; -- const state = ctx.state; -- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { -- syncInitialScrollOffset(state, options.resolvedOffset); -- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { -- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); -- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { -- syncInitialScrollOffset(state, observedOffset); + const currentOffset = readOffset(); + const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; + if (reason === "scrollend" && !isNearTarget) { + return; - } ++ } + scheduledWork.cancel("platformScrollCompletion"); + finishScrollTo(ctx); + }; @@ -4587,6 +4703,22 @@ index b2240b5..69282b3 100644 + } else { + idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); } ++ scheduledWork.register("platformScrollCompletion", cancel); + } +-function finishInitialScroll(ctx, options) { +- var _a3, _b, _c; ++ ++// src/utils/requestAdjust.ts ++function requestAdjust(ctx, positionDiff, source) { + const state = ctx.state; +- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { +- syncInitialScrollOffset(state, options.resolvedOffset); +- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { +- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); +- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { +- syncInitialScrollOffset(state, observedOffset); +- } +- } - const complete = () => { - var _a4, _b2, _c2, _d, _e; - const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; @@ -4608,12 +4740,6 @@ index b2240b5..69282b3 100644 - PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, - "preservedInitialScroll" - ); -+ scheduledWork.register("platformScrollCompletion", cancel); -+} -+ -+// src/utils/requestAdjust.ts -+function requestAdjust(ctx, positionDiff, source) { -+ const state = ctx.state; + if (Math.abs(positionDiff) > 0.1) { + const doit = () => { + { @@ -5529,8 +5655,44 @@ index b2240b5..69282b3 100644 if (didMVCPAdjustScroll) { updateScroll2(state.scroll); updateScrollRange(); +@@ -6607,6 +6683,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize + return props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; + } + function setHeaderSize(ctx, size) { ++ var _a3; + const { state } = ctx; + const previousHeaderSize = peek$(ctx, "headerSize") || 0; + const didChange = previousHeaderSize !== size; +@@ -6616,6 +6693,9 @@ function setHeaderSize(ctx, size) { + updateContentMetricsState(ctx); + if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { + requestAdjust(ctx, size - previousHeaderSize); ++ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) { ++ handleBootstrapInitialScrollLayoutChange(ctx); ++ doMaintainScrollAtEnd(ctx); + } + } + state.didMeasureHeader = true; +@@ -7679,13 +7759,14 @@ function getRenderedItem(ctx, key, containerId) { + + // src/utils/normalizeMaintainScrollAtEnd.ts + function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) { +- var _a3, _b, _c, _d; ++ var _a3, _b, _c, _d, _e; + return { + animated: false, + onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true, + onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true, +- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true, +- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true ++ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true, ++ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true, ++ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true + }; + } + function normalizeMaintainScrollAtEnd(value) { diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs -index a97be05..a1e2968 100644 +index a97be05..83225ee 100644 --- a/node_modules/@legendapp/list/react-native.web.mjs +++ b/node_modules/@legendapp/list/react-native.web.mjs @@ -409,171 +409,271 @@ var EDGE_POSITION_EPSILON = 1; @@ -6023,7 +6185,7 @@ index a97be05..a1e2968 100644 + } + { + state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); - } ++ } + if (scrollingTo.isInitialScroll || state.initialScroll) { + const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; + const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; @@ -6038,7 +6200,7 @@ index a97be05..a1e2968 100644 + waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame + }); + return; -+ } + } + recalculateSettledScroll(ctx); + resolvePendingScroll == null ? void 0 : resolvePendingScroll(); } @@ -6210,22 +6372,11 @@ index a97be05..a1e2968 100644 - initialScrollCompletion.resetFlags(state); - setInitialScrollSession(state, { bootstrap: null }); - finishInitialScroll(ctx); -- } --} --function finishInitialScroll(ctx, options) { -- var _a3, _b, _c; -- const state = ctx.state; -- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { -- syncInitialScrollOffset(state, options.resolvedOffset); -- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { -- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); -- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { -- syncInitialScrollOffset(state, observedOffset); + const currentOffset = readOffset(); + const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; + if (reason === "scrollend" && !isNearTarget) { + return; - } ++ } + scheduledWork.cancel("platformScrollCompletion"); + finishScrollTo(ctx); + }; @@ -6242,6 +6393,22 @@ index a97be05..a1e2968 100644 + } else { + idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); } ++ scheduledWork.register("platformScrollCompletion", cancel); + } +-function finishInitialScroll(ctx, options) { +- var _a3, _b, _c; ++ ++// src/utils/requestAdjust.ts ++function requestAdjust(ctx, positionDiff, source) { + const state = ctx.state; +- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { +- syncInitialScrollOffset(state, options.resolvedOffset); +- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { +- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); +- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { +- syncInitialScrollOffset(state, observedOffset); +- } +- } - const complete = () => { - var _a4, _b2, _c2, _d, _e; - const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; @@ -6263,12 +6430,6 @@ index a97be05..a1e2968 100644 - PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, - "preservedInitialScroll" - ); -+ scheduledWork.register("platformScrollCompletion", cancel); -+} -+ -+// src/utils/requestAdjust.ts -+function requestAdjust(ctx, positionDiff, source) { -+ const state = ctx.state; + if (Math.abs(positionDiff) > 0.1) { + const doit = () => { + { @@ -7184,8 +7345,61 @@ index a97be05..a1e2968 100644 if (didMVCPAdjustScroll) { updateScroll2(state.scroll); updateScrollRange(); +@@ -6586,6 +6662,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize + return props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; + } + function setHeaderSize(ctx, size) { ++ var _a3; + const { state } = ctx; + const previousHeaderSize = peek$(ctx, "headerSize") || 0; + const didChange = previousHeaderSize !== size; +@@ -6595,6 +6672,9 @@ function setHeaderSize(ctx, size) { + updateContentMetricsState(ctx); + if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { + requestAdjust(ctx, size - previousHeaderSize); ++ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) { ++ handleBootstrapInitialScrollLayoutChange(ctx); ++ doMaintainScrollAtEnd(ctx); + } + } + state.didMeasureHeader = true; +@@ -7658,13 +7738,14 @@ function getRenderedItem(ctx, key, containerId) { + + // src/utils/normalizeMaintainScrollAtEnd.ts + function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) { +- var _a3, _b, _c, _d; ++ var _a3, _b, _c, _d, _e; + return { + animated: false, + onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true, + onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true, +- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true, +- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true ++ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true, ++ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true, ++ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true + }; + } + function normalizeMaintainScrollAtEnd(value) { +diff --git a/node_modules/@legendapp/list/react.d.ts b/node_modules/@legendapp/list/react.d.ts +index b6c7481..ace847e 100644 +--- a/node_modules/@legendapp/list/react.d.ts ++++ b/node_modules/@legendapp/list/react.d.ts +@@ -511,6 +511,12 @@ interface AlwaysRenderConfig { + interface MaintainScrollAtEndOnOptions { + dataChange?: boolean; + footerLayout?: boolean; ++ /** ++ * Keep the list pinned to the end when the header changes size. A header that measures larger ++ * than its estimate pushes every item down, which leaves a list that had already scrolled to ++ * its end short of it by the difference. ++ */ ++ headerLayout?: boolean; + itemLayout?: boolean; + layout?: boolean; + } diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js -index b2240b5..69282b3 100644 +index b2240b5..cbff6eb 100644 --- a/node_modules/@legendapp/list/react.js +++ b/node_modules/@legendapp/list/react.js @@ -430,171 +430,271 @@ var EDGE_POSITION_EPSILON = 1; @@ -7678,7 +7892,7 @@ index b2240b5..69282b3 100644 + } + { + state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); - } ++ } + if (scrollingTo.isInitialScroll || state.initialScroll) { + const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; + const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; @@ -7693,7 +7907,7 @@ index b2240b5..69282b3 100644 + waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame + }); + return; -+ } + } + recalculateSettledScroll(ctx); + resolvePendingScroll == null ? void 0 : resolvePendingScroll(); } @@ -7865,22 +8079,11 @@ index b2240b5..69282b3 100644 - initialScrollCompletion.resetFlags(state); - setInitialScrollSession(state, { bootstrap: null }); - finishInitialScroll(ctx); -- } --} --function finishInitialScroll(ctx, options) { -- var _a3, _b, _c; -- const state = ctx.state; -- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { -- syncInitialScrollOffset(state, options.resolvedOffset); -- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { -- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); -- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { -- syncInitialScrollOffset(state, observedOffset); + const currentOffset = readOffset(); + const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; + if (reason === "scrollend" && !isNearTarget) { + return; - } ++ } + scheduledWork.cancel("platformScrollCompletion"); + finishScrollTo(ctx); + }; @@ -7897,6 +8100,22 @@ index b2240b5..69282b3 100644 + } else { + idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); } ++ scheduledWork.register("platformScrollCompletion", cancel); + } +-function finishInitialScroll(ctx, options) { +- var _a3, _b, _c; ++ ++// src/utils/requestAdjust.ts ++function requestAdjust(ctx, positionDiff, source) { + const state = ctx.state; +- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { +- syncInitialScrollOffset(state, options.resolvedOffset); +- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { +- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); +- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { +- syncInitialScrollOffset(state, observedOffset); +- } +- } - const complete = () => { - var _a4, _b2, _c2, _d, _e; - const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; @@ -7918,12 +8137,6 @@ index b2240b5..69282b3 100644 - PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, - "preservedInitialScroll" - ); -+ scheduledWork.register("platformScrollCompletion", cancel); -+} -+ -+// src/utils/requestAdjust.ts -+function requestAdjust(ctx, positionDiff, source) { -+ const state = ctx.state; + if (Math.abs(positionDiff) > 0.1) { + const doit = () => { + { @@ -8839,8 +9052,44 @@ index b2240b5..69282b3 100644 if (didMVCPAdjustScroll) { updateScroll2(state.scroll); updateScrollRange(); +@@ -6607,6 +6683,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize + return props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; + } + function setHeaderSize(ctx, size) { ++ var _a3; + const { state } = ctx; + const previousHeaderSize = peek$(ctx, "headerSize") || 0; + const didChange = previousHeaderSize !== size; +@@ -6616,6 +6693,9 @@ function setHeaderSize(ctx, size) { + updateContentMetricsState(ctx); + if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { + requestAdjust(ctx, size - previousHeaderSize); ++ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) { ++ handleBootstrapInitialScrollLayoutChange(ctx); ++ doMaintainScrollAtEnd(ctx); + } + } + state.didMeasureHeader = true; +@@ -7679,13 +7759,14 @@ function getRenderedItem(ctx, key, containerId) { + + // src/utils/normalizeMaintainScrollAtEnd.ts + function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) { +- var _a3, _b, _c, _d; ++ var _a3, _b, _c, _d, _e; + return { + animated: false, + onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true, + onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true, +- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true, +- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true ++ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true, ++ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true, ++ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true + }; + } + function normalizeMaintainScrollAtEnd(value) { diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs -index a97be05..a1e2968 100644 +index a97be05..83225ee 100644 --- a/node_modules/@legendapp/list/react.mjs +++ b/node_modules/@legendapp/list/react.mjs @@ -409,171 +409,271 @@ var EDGE_POSITION_EPSILON = 1; @@ -9333,7 +9582,7 @@ index a97be05..a1e2968 100644 + } + { + state.scrollAdjustHandler.commitPendingAdjust(scrollingTo); - } ++ } + if (scrollingTo.isInitialScroll || state.initialScroll) { + const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset"; + const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1; @@ -9348,7 +9597,7 @@ index a97be05..a1e2968 100644 + waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame + }); + return; -+ } + } + recalculateSettledScroll(ctx); + resolvePendingScroll == null ? void 0 : resolvePendingScroll(); } @@ -9520,22 +9769,11 @@ index a97be05..a1e2968 100644 - initialScrollCompletion.resetFlags(state); - setInitialScrollSession(state, { bootstrap: null }); - finishInitialScroll(ctx); -- } --} --function finishInitialScroll(ctx, options) { -- var _a3, _b, _c; -- const state = ctx.state; -- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { -- syncInitialScrollOffset(state, options.resolvedOffset); -- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { -- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); -- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { -- syncInitialScrollOffset(state, observedOffset); + const currentOffset = readOffset(); + const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON; + if (reason === "scrollend" && !isNearTarget) { + return; - } ++ } + scheduledWork.cancel("platformScrollCompletion"); + finishScrollTo(ctx); + }; @@ -9552,6 +9790,22 @@ index a97be05..a1e2968 100644 + } else { + idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS); } ++ scheduledWork.register("platformScrollCompletion", cancel); + } +-function finishInitialScroll(ctx, options) { +- var _a3, _b, _c; ++ ++// src/utils/requestAdjust.ts ++function requestAdjust(ctx, positionDiff, source) { + const state = ctx.state; +- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) { +- syncInitialScrollOffset(state, options.resolvedOffset); +- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") { +- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b); +- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) { +- syncInitialScrollOffset(state, observedOffset); +- } +- } - const complete = () => { - var _a4, _b2, _c2, _d, _e; - const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap"; @@ -9573,12 +9827,6 @@ index a97be05..a1e2968 100644 - PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS, - "preservedInitialScroll" - ); -+ scheduledWork.register("platformScrollCompletion", cancel); -+} -+ -+// src/utils/requestAdjust.ts -+function requestAdjust(ctx, positionDiff, source) { -+ const state = ctx.state; + if (Math.abs(positionDiff) > 0.1) { + const doit = () => { + { @@ -10494,3 +10742,73 @@ index a97be05..a1e2968 100644 if (didMVCPAdjustScroll) { updateScroll2(state.scroll); updateScrollRange(); +@@ -6586,6 +6662,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize + return props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; + } + function setHeaderSize(ctx, size) { ++ var _a3; + const { state } = ctx; + const previousHeaderSize = peek$(ctx, "headerSize") || 0; + const didChange = previousHeaderSize !== size; +@@ -6595,6 +6672,9 @@ function setHeaderSize(ctx, size) { + updateContentMetricsState(ctx); + if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { + requestAdjust(ctx, size - previousHeaderSize); ++ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) { ++ handleBootstrapInitialScrollLayoutChange(ctx); ++ doMaintainScrollAtEnd(ctx); + } + } + state.didMeasureHeader = true; +@@ -7658,13 +7738,14 @@ function getRenderedItem(ctx, key, containerId) { + + // src/utils/normalizeMaintainScrollAtEnd.ts + function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) { +- var _a3, _b, _c, _d; ++ var _a3, _b, _c, _d, _e; + return { + animated: false, + onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true, + onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true, +- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true, +- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true ++ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true, ++ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true, ++ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true + }; + } + function normalizeMaintainScrollAtEnd(value) { +diff --git a/node_modules/@legendapp/list/reanimated.d.ts b/node_modules/@legendapp/list/reanimated.d.ts +index e504332..99337c1 100644 +--- a/node_modules/@legendapp/list/reanimated.d.ts ++++ b/node_modules/@legendapp/list/reanimated.d.ts +@@ -489,6 +489,12 @@ interface AlwaysRenderConfig { + interface MaintainScrollAtEndOnOptions { + dataChange?: boolean; + footerLayout?: boolean; ++ /** ++ * Keep the list pinned to the end when the header changes size. A header that measures larger ++ * than its estimate pushes every item down, which leaves a list that had already scrolled to ++ * its end short of it by the difference. ++ */ ++ headerLayout?: boolean; + itemLayout?: boolean; + layout?: boolean; + } +diff --git a/node_modules/@legendapp/list/section-list.d.ts b/node_modules/@legendapp/list/section-list.d.ts +index a790f7f..9a3c2ef 100644 +--- a/node_modules/@legendapp/list/section-list.d.ts ++++ b/node_modules/@legendapp/list/section-list.d.ts +@@ -545,6 +545,12 @@ interface AlwaysRenderConfig { + interface MaintainScrollAtEndOnOptions { + dataChange?: boolean; + footerLayout?: boolean; ++ /** ++ * Keep the list pinned to the end when the header changes size. A header that measures larger ++ * than its estimate pushes every item down, which leaves a list that had already scrolled to ++ * its end short of it by the difference. ++ */ ++ headerLayout?: boolean; + itemLayout?: boolean; + layout?: boolean; + } From d1c2a2ed9d66b7e44eb5f85207729d47b2ab99ac Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Tue, 18 Aug 2026 23:28:35 -0400 Subject: [PATCH 34/38] test(e2e): cover opening a conversation onto a linked message The cold path - a thread mounting with a centred target already pending - had no coverage. chat-search-hit only exercises the warm one. This drives it the way a reader does: copy a link to a message in one conversation, paste and send it in another, click it. The test currently FAILS on this branch. With the hasRenderedNonEmptyRef guard in useScrollToCentered removed (pre-2529570d5e behaviour) it passes; with the guard in place the thread lands on its newest message and the linked one is never shown. Instrumented renders show why: the list mounts with initialScrollIndex undefined and initialScrollAtEnd true, and the centred ordinal only arrives on the next render, so the library's own bootstrap has nothing to aim at and the guard has already declined the imperative scroll. Adds a testID to the per-message "..." button, the only way into "Copy a link to this message" and icon-only, so there is no text to match. --- .../conversation/messages/wrapper/wrapper.tsx | 6 +- .../e2e/electron/flows/chat-link-jump.test.ts | 205 ++++++++++++++++++ shared/tests/e2e/shared/test-ids.ts | 3 + 3 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 shared/tests/e2e/electron/flows/chat-link-jump.test.ts diff --git a/shared/chat/conversation/messages/wrapper/wrapper.tsx b/shared/chat/conversation/messages/wrapper/wrapper.tsx index a339e2e9108f..e6b237cb58dc 100644 --- a/shared/chat/conversation/messages/wrapper/wrapper.tsx +++ b/shared/chat/conversation/messages/wrapper/wrapper.tsx @@ -906,7 +906,11 @@ function RightSide(p: RProps) { )} > - + ) diff --git a/shared/tests/e2e/electron/flows/chat-link-jump.test.ts b/shared/tests/e2e/electron/flows/chat-link-jump.test.ts new file mode 100644 index 000000000000..b05ae3de5c6b --- /dev/null +++ b/shared/tests/e2e/electron/flows/chat-link-jump.test.ts @@ -0,0 +1,205 @@ +import type {Locator, Page} from '@playwright/test' +import {test, expect} from '@/tests/e2e/electron/helpers/fixtures' +import {navigateToChat} from '@/tests/e2e/electron/helpers/navigate' +import * as T from '@/tests/e2e/shared/test-ids' + +// Opening a conversation directly onto one of its messages — the cold path. +// +// chat-search-hit covers the warm one: it opens a thread and then searches inside it, so the list +// has always rendered content by the time anything asks it to centre. The branch this covers is the +// other one — a thread mounting with a centred target already pending, which is what a permalink +// does. It is reached the way a reader reaches it, through the app's own affordances: copy a link to +// a message in conversation B, paste and send it in conversation A, click it. That routes through +// handleKeybaseLink -> previewConversation -> navigateToThread(..., highlightMessageID), which on +// desktop resets the chat root params and mounts a fresh thread. No OS-level deeplink needed. +// +// The link must point at a genuinely different conversation: navigateToThread takes a +// sameVisibleThread branch when the target is already on screen, and that branch does not remount, +// which is the warm path again. + +// A row has to be visible by more than a hairline to count as landed on, matching chat-search-hit +// and the iOS flow. +// No retries. The second attempt runs against a thread this test has already fetched once, so a +// pass on retry would not be a pass on the path the first attempt took - the same reason +// chat-thread-bottom turns them off. +test.describe.configure({retries: 0}) + +const MIN_VISIBLE_HEIGHT = 24 +// Centring is checked loosely on purpose. Sub-pixel precision is not the point — landing on the +// right message rather than at one end of the thread is. A third of the viewport either side of the +// middle still fails every way this can go wrong (top of the loaded window, bottom of the thread, +// off screen entirely). +const CENTRE_BAND_FRACTION = 1 / 3 +// The target has to sit well above the newest message, or a thread that simply stayed at its end +// would pass without anything having jumped. +const MIN_DISTANCE_FROM_END = 1_200 +// A conversation with less scrollable history than this cannot show the difference. +const MIN_SCROLLABLE_OVERFLOW = 1_500 + +// The scroller is the element LegendList renders inside the wrapper carrying the testID. Reached +// through the wrapper's first child, the same way chat-thread-bottom does it. +const distanceFromEnd = async (page: Page): Promise => + page.evaluate((testID: string) => { + type Scroller = {clientHeight: number; scrollHeight: number; scrollTop: number} + const doc = ( + globalThis as unknown as { + document?: {querySelector: (selector: string) => {firstElementChild?: Scroller | null} | null} + } + ).document + const scroller = doc?.querySelector(`[data-testid="${testID}"]`)?.firstElementChild + if (!scroller) return undefined + return Math.round(scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop) + }, T.CHAT_MESSAGE_LIST) + +const scrollableOverflow = async (page: Page): Promise => + page.evaluate((testID: string) => { + type Scroller = {clientHeight: number; scrollHeight: number} + const doc = ( + globalThis as unknown as { + document?: {querySelector: (selector: string) => {firstElementChild?: Scroller | null} | null} + } + ).document + const scroller = doc?.querySelector(`[data-testid="${testID}"]`)?.firstElementChild + if (!scroller) return undefined + return Math.round(scroller.scrollHeight - scroller.clientHeight) + }, T.CHAT_MESSAGE_LIST) + +// Inbox rows carry the conversation name on their first line. Matched exactly rather than with +// hasText: a row's second line is the latest message, and this flow sends a message that contains +// another conversation's name, so a substring match picks the wrong row from the second run on. +const rowName = async (row: Locator): Promise => + (await row.innerText()).split('\n')[0]?.trim() ?? '' + +const openConversationNamed = async (page: Page, rows: Locator, name: string): Promise => { + const count = await rows.count() + for (let i = 0; i < count; i++) { + const row = rows.nth(i) + if ((await rowName(row)) !== name) continue + await row.click({force: true, timeout: 10_000}) + await page.waitForSelector(`[data-testid="${T.CHAT_MESSAGE_LIST}"]`, {timeout: 10_000}) + await page.waitForTimeout(2_500) + return + } + throw new Error(`no inbox row named "${name}"`) +} + +test('opens a conversation onto the message a link points at', async ({page}) => { + test.setTimeout(180_000) + const smokeUser = process.env['KB_SMOKE_USER'] + expect(smokeUser, 'KB_SMOKE_USER is not set').toBeTruthy() + + await navigateToChat(page) + const rows = page.locator( + `[data-testid="${T.CHAT_INBOX_ROW}"], [data-testid="${T.CHAT_INBOX_CHANNEL_ROW}"]` + ) + + // B is discovered rather than hard-coded — a real username in a committed test would pin the suite + // to one machine — but it is pinned by the name it turns out to have for the rest of the run, so + // the second visit is provably the same conversation as the first. + let convB = '' + const rowCount = Math.min(await rows.count(), 12) + for (let i = 0; i < rowCount && !convB; i++) { + const name = await rowName(rows.nth(i)) + // A is the smoke account's own conversation: the one message this flow sends goes there. + if (!name || name === smokeUser) continue + await openConversationNamed(page, rows, name) + const overflow = await scrollableOverflow(page) + if (overflow !== undefined && overflow >= MIN_SCROLLABLE_OVERFLOW) convB = name + } + expect(convB, 'no conversation had enough history to jump within, so nothing was checked').toBeTruthy() + + // Scroll back through B until the newest message is far enough away, then take a row that is + // wholly on screen and small enough to measure. `data-ordinal` is the app's own per-message + // attribute; there is no testID for "a message row", and the ordinal is what the copied link + // encodes, so it is also how the landing is checked at the other end. + const listBox = await page.getByTestId(T.CHAT_MESSAGE_LIST).first().boundingBox() + expect(listBox, 'the message list has no box').not.toBeNull() + await page.mouse.move(listBox!.x + listBox!.width / 2, listBox!.y + listBox!.height / 2) + + let targetOrdinal = '' + let targetRow: Locator | undefined + for (let attempt = 0; attempt < 30 && !targetRow; attempt++) { + await page.mouse.wheel(0, -600) + await page.waitForTimeout(250) + if (((await distanceFromEnd(page)) ?? 0) < MIN_DISTANCE_FROM_END) continue + await page.waitForTimeout(500) + const candidates = page.locator('[data-ordinal]') + const n = await candidates.count() + for (let i = 0; i < n; i++) { + const candidate = candidates.nth(i) + const box = await candidate.boundingBox() + if (!box) continue + const text = (await candidate.innerText()).trim() + const wholly = box.y >= listBox!.y + 20 && box.y + box.height <= listBox!.y + listBox!.height - 20 + // Media rows are taller than the viewport's usable band and often have no text to identify + // them by, so a short text row is the honest handle here. + if (box.height > 250 || !wholly || text.length < 6) continue + targetOrdinal = (await candidate.getAttribute('data-ordinal')) ?? '' + targetRow = candidate + break + } + } + expect(targetRow, `no message in "${convB}" was far enough above its newest one to link to`).toBeTruthy() + + await targetRow!.hover() + await targetRow!.getByTestId(T.CHAT_MESSAGE_MENU_BUTTON).first().click({force: true, timeout: 10_000}) + await page.getByText('Copy a link to this message').first().click({timeout: 10_000}) + await page.waitForTimeout(700) + + // Pasting rather than typing the link out: the copy above put it on the real clipboard and there + // is no way to read that back from the renderer (clipboard-read permission is denied in the app), + // so the paste both delivers it and reveals what was copied. A plain Meta+V is a synthetic key + // event the renderer ignores; the editing command has to be attached to it. + await openConversationNamed(page, rows, smokeUser!) + const input = page.getByTestId(T.CHAT_INPUT) + await input.click() + await input.fill('') + const cdp = await page.context().newCDPSession(page) + const key = {code: 'KeyV', key: 'v', modifiers: 4, nativeVirtualKeyCode: 86, windowsVirtualKeyCode: 86} + await cdp.send('Input.dispatchKeyEvent', {...key, commands: ['paste'], type: 'keyDown'}) + await cdp.send('Input.dispatchKeyEvent', {...key, type: 'keyUp'}) + await page.waitForTimeout(600) + const link = await input.inputValue() + expect(link, 'the message menu did not put a keybase chat link on the clipboard').toMatch( + new RegExp(`^keybase://chat/.+/${targetOrdinal}$`) + ) + await input.press('Enter') + + const sentLink = page.locator(`[data-testid="${T.CHAT_MESSAGE_LIST}"]`).getByText(link, {exact: true}) + await expect(sentLink.last()).toBeVisible({timeout: 15_000}) + await sentLink.last().click() + + // The thread this lands in is a fresh mount with a centred target already pending. Poll rather + // than sleep once: the centred fetch clears and refetches the thread, so the row arrives well + // after the navigation does. + const hit = page.getByTestId(T.CHAT_SEARCH_HIT).first() + await expect( + hit, + `clicking the link to ${link} never landed on the message: the thread settled ${await distanceFromEnd(page)}px from its end without the target on screen` + ).toBeVisible({timeout: 20_000}) + // Let any late row measurement settle before reading positions, the same way the search flow does. + await page.waitForTimeout(2_000) + + // We really did leave A: the link message is A's newest and would still be rendered if we had not. + await expect(sentLink).toHaveCount(0) + + const hitBox = await hit.boundingBox({timeout: 5_000}) + const listAfter = await page.getByTestId(T.CHAT_MESSAGE_LIST).first().boundingBox() + expect(hitBox, 'the linked message was highlighted but had no box, so nothing was measured').not.toBeNull() + expect(listAfter, 'the message list has no box').not.toBeNull() + + const visibleHeight = + Math.min(hitBox!.y + hitBox!.height, listAfter!.y + listAfter!.height) - Math.max(hitBox!.y, listAfter!.y) + expect( + visibleHeight >= Math.min(hitBox!.height, MIN_VISIBLE_HEIGHT), + `the linked message is outside the list: hit y=${Math.round(hitBox!.y)} h=${Math.round(hitBox!.height)}, list y=${Math.round(listAfter!.y)} h=${Math.round(listAfter!.height)}` + ).toBe(true) + + const offCentre = Math.abs( + hitBox!.y + hitBox!.height / 2 - (listAfter!.y + listAfter!.height / 2) + ) + expect( + offCentre, + `the linked message landed ${Math.round(offCentre)}px from the middle of the list (viewport ${Math.round(listAfter!.height)}px)` + ).toBeLessThanOrEqual(listAfter!.height * CENTRE_BAND_FRACTION) +}) diff --git a/shared/tests/e2e/shared/test-ids.ts b/shared/tests/e2e/shared/test-ids.ts index c01658038632..eb1003bf10e6 100644 --- a/shared/tests/e2e/shared/test-ids.ts +++ b/shared/tests/e2e/shared/test-ids.ts @@ -46,6 +46,9 @@ export const CHAT_THREAD_SEARCH_NEXT = 'chat-thread-search-next' // messages, reply jumps and permalinks highlight one too, so this only means "search hit" inside a // search flow. Present only while the row is highlighted. export const CHAT_SEARCH_HIT = 'chat-search-hit' +// The per-message "..." actions button (desktop only, revealed on hover). Icon-only, so there is +// no text to match on, and it is the only way into "Copy a link to this message". +export const CHAT_MESSAGE_MENU_BUTTON = 'chat-message-menu-button' // The header above the oldest loaded message. Mounted in every state - loading, more to load, start // of the conversation - so its position is readable throughout, which is how a test sees a page of // older messages arrive. From 2d428e54b5d36734a24efd528bd02553b69d95dd Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 19 Aug 2026 07:21:23 -0400 Subject: [PATCH 35/38] fix(chat): always issue the centred scroll The guard added in 2529570d5e stood the imperative scrollToItem down while the list's own initialScrollIndex bootstrap looked like it owned the target. Measured against the app, the premise does not hold on the permalink path: a thread opened from a message link mounts with no centred ordinal, so the list is built with initialScrollAtEnd, and the bootstrap it re-arms when the centred dataset lands does not move it. The thread settles at its end and the linked message is never shown. Correcting the predicate to the real question - is the bootstrap armed at THIS target - does not help. Instrumented renders show it is true on exactly that path: by the time the centred dataset commits, centeredOrdinal has arrived and initialScrollIndex resolves, so the corrected guard skips too. A warm in-thread jump reaches this hook in the same shape, and what separates them lives in the library's own initial-scroll state, which the app cannot see. So the imperative call has to be the one authority that always fires. chat-search-hit, the flow the guard was added for, passes without it. --- shared/chat/conversation/list-area/index.tsx | 46 ++++---------------- 1 file changed, 8 insertions(+), 38 deletions(-) diff --git a/shared/chat/conversation/list-area/index.tsx b/shared/chat/conversation/list-area/index.tsx index b81d0eb6e349..1144ff142f30 100644 --- a/shared/chat/conversation/list-area/index.tsx +++ b/shared/chat/conversation/list-area/index.tsx @@ -241,21 +241,14 @@ const useScrollToCentered = (p: { lastScrolledRef.current = undefined }, [datasetKey]) - // Has this mounted list ever committed non-empty message data before the current jump decision? - // That is the same question as "has the list already spent its own initialScrollIndex bootstrap": - // the library only re-arms that bootstrap on an empty -> non-empty data transition, which is - // exactly the transition that flips this ref. So when it is false the library is about to land the - // target itself (useInitialScrollIndex hands it {index, 0.5}) and an imperative scroll on top of - // it is a second authority aiming at the same target - measured on stock @legendapp/list 3.3.7, - // that fight is what makes an opened-at-a-hit thread land wrong. - // When it is true (jumping around inside a thread already on screen) the library will not re-aim - // and this call is the only thing that moves the list, so it has to stay. - // - // Deliberately NOT reset per dataset: every centered jump clears and refetches the thread, so - // resetting here would erase the very signal this reads. It is per mount, and the thread provider - // is keyed by conversation, so a new conversation gets a fresh one. - const hasRenderedNonEmptyRef = React.useRef(false) - + // Unconditional on purpose. A guard that stood this call down while the list's own + // initialScrollIndex bootstrap looked like it owned the target was tried and measured against the + // app, and there is no version of it that is safe: on the permalink path the thread mounts with no + // centred target, so the list is built with initialScrollAtEnd, and the bootstrap it re-arms when + // the centred dataset lands does not move it - the thread settles at its end with the target never + // shown. That path is indistinguishable from a warm in-thread jump by anything visible here (both + // arrive as "dataset with a resolvable initialScrollIndex"), so this call has to be the one + // authority that always fires. React.useEffect(() => { if (!ready || centeredOrdinal === undefined) { lastScrolledRef.current = undefined @@ -266,31 +259,8 @@ const useScrollToCentered = (p: { return } lastScrolledRef.current = centeredOrdinal - if (!hasRenderedNonEmptyRef.current) { - // The list's own initialScrollIndex bootstrap owns this one. - return - } void listRef.current?.scrollToItem({animated: false, item: centeredOrdinal, viewPosition: 0.5}) }, [centeredOrdinal, datasetKey, listRef, messageOrdinals, ready]) - - // Declared AFTER the jump effect above, and that ordering is load-bearing: React runs one - // component's passive effects in declaration order, so the jump effect always reads this ref as - // it stood BEFORE the current commit. A thread's first content arriving in the same commit as its - // first centered target must not retroactively count as "already live" for that commit's own - // decision. Do not reorder these two, and do not hoist this into a layout effect, a render-phase - // assignment or a store flag. - // - // The predicate is deliberately the library's own (`dataLength > 0`, see its - // `shouldUseLatestInitialScroll`) and must stay in sync with it - do NOT add a condition the - // library does not have. `ready` in particular is wrong here: desktop passes `ready: loaded` but - // hands the list `data={messageOrdinals}` unconditionally, so a live message arriving after - // messagesClear but before the centered response would flip the library's own flag while leaving - // this one false, and then neither authority scrolls. - React.useEffect(() => { - if (messageOrdinals.length > 0) { - hasRenderedNonEmptyRef.current = true - } - }, [messageOrdinals]) } const DesktopThreadWrapper = function DesktopThreadWrapper() { From dbe74c1e8b75895bbaedfb6f776ab8293d5be7a6 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 19 Aug 2026 07:50:38 -0400 Subject: [PATCH 36/38] test(e2e): reuse an existing link message in chat-link-jump chat-link-jump.test.ts sent a fresh keybase://chat link message into the smoke user's own conversation on every run, polluting real chat history. Look for a link an earlier run already left there first, validate its target is still far enough from that conversation's newest message to prove a jump happened, and only fall back to sending a new one when no usable link is found. --- .../e2e/electron/flows/chat-link-jump.test.ts | 250 +++++++++++++----- 1 file changed, 184 insertions(+), 66 deletions(-) diff --git a/shared/tests/e2e/electron/flows/chat-link-jump.test.ts b/shared/tests/e2e/electron/flows/chat-link-jump.test.ts index b05ae3de5c6b..3060a9483e1c 100644 --- a/shared/tests/e2e/electron/flows/chat-link-jump.test.ts +++ b/shared/tests/e2e/electron/flows/chat-link-jump.test.ts @@ -16,6 +16,12 @@ import * as T from '@/tests/e2e/shared/test-ids' // The link must point at a genuinely different conversation: navigateToThread takes a // sameVisibleThread branch when the target is already on screen, and that branch does not remount, // which is the warm path again. +// +// Sending a link message is real data in the smoke user's real chat history, so every run that +// sends one leaves it behind permanently. Only one such message is ever needed - the app doesn't +// care who authored the link it's clicking, or when. So this test looks for one an earlier run +// already left in conversation A before resorting to sending a new one, and only falls back to +// sending when no leftover link is usable. // A row has to be visible by more than a hairline to count as landed on, matching chat-search-hit // and the iOS flow. @@ -31,7 +37,9 @@ const MIN_VISIBLE_HEIGHT = 24 // off screen entirely). const CENTRE_BAND_FRACTION = 1 / 3 // The target has to sit well above the newest message, or a thread that simply stayed at its end -// would pass without anything having jumped. +// would pass without anything having jumped. This also gates link reuse: an old link is only worth +// clicking if its target is still this far from B's current newest message, or a wrong landing +// (staying put) would go undetected. const MIN_DISTANCE_FROM_END = 1_200 // A conversation with less scrollable history than this cannot show the difference. const MIN_SCROLLABLE_OVERFLOW = 1_500 @@ -70,11 +78,27 @@ const scrollableOverflow = async (page: Page): Promise => const rowName = async (row: Locator): Promise => (await row.innerText()).split('\n')[0]?.trim() ?? '' +// A row's displayed name isn't always the conversation's full label. A team's default "general" +// channel shows as a small-team row with just the team name; any other channel shows as its own +// row with just "# channelname" (the team name isn't repeated there). getConversationLabel, which +// is what a copied link encodes, always writes "team#channel" though - so a link's conv name needs +// both forms accepted, not just an exact match against what a row shows. +const rowMatchesConvName = async (row: Locator, convName: string): Promise => { + const text = await rowName(row) + if (text === convName) return true + const hashIndex = convName.indexOf('#') + if (hashIndex === -1) return false + const team = convName.slice(0, hashIndex) + const channel = convName.slice(hashIndex + 1) + if (channel === 'general' && text === team) return true + return text.replace(/^#\s*/, '') === channel +} + const openConversationNamed = async (page: Page, rows: Locator, name: string): Promise => { const count = await rows.count() for (let i = 0; i < count; i++) { const row = rows.nth(i) - if ((await rowName(row)) !== name) continue + if (!(await rowMatchesConvName(row, name))) continue await row.click({force: true, timeout: 10_000}) await page.waitForSelector(`[data-testid="${T.CHAT_MESSAGE_LIST}"]`, {timeout: 10_000}) await page.waitForTimeout(2_500) @@ -83,6 +107,57 @@ const openConversationNamed = async (page: Page, rows: Locator, name: string): P throw new Error(`no inbox row named "${name}"`) } +// What "Copy a link to this message" puts on the clipboard: constants/deeplinks.tsx's +// linkFromConvAndMessage(conv, messageID) - conv is the conversation's display label (what +// rowName also reads), messageID is what the app renders as data-ordinal once a message has sent. +const LINK_PATTERN = /^keybase:\/\/chat\/(.+)\/(\d+)$/ + +// Looks for a message already sitting in conversation A whose text is a link from an earlier run +// of this test. Recent messages are checked first without scrolling - every prior run that fell +// back to sending left one near the bottom, so the common case needs no scroll at all - and a +// bounded scroll back covers the case where other messages have since pushed all of them out of +// view. +const findExistingLink = async (page: Page): Promise<{convName: string; ordinal: string; text: string} | undefined> => { + for (let attempt = 0; attempt < 6; attempt++) { + const candidates = page.locator('[data-ordinal]') + const n = await candidates.count() + for (let i = n - 1; i >= 0; i--) { + const text = (await candidates.nth(i).innerText()).trim() + const match = LINK_PATTERN.exec(text) + if (match) return {convName: match[1]!, ordinal: match[2]!, text} + } + await page.mouse.wheel(0, -600) + await page.waitForTimeout(250) + } + return undefined +} + +// Scrolls conversation B upward looking for the reused link's target row. If it turns up wholly on +// screen at a point at least MIN_DISTANCE_FROM_END from B's current newest message, the reused link +// is still good. If the scroll runs out first - the target has scrolled closer to the end since the +// link was made, or is no longer reachable at all - the caller falls back to sending a fresh link +// instead: asserting a landing that could not distinguish "jumped" from "already there" would prove +// nothing. +const findReusedTargetRow = async ( + page: Page, + listBox: {x: number; y: number; width: number; height: number}, + ordinal: string +): Promise => { + for (let attempt = 0; attempt < 30; attempt++) { + await page.mouse.wheel(0, -600) + await page.waitForTimeout(250) + if (((await distanceFromEnd(page)) ?? 0) < MIN_DISTANCE_FROM_END) continue + await page.waitForTimeout(500) + const candidate = page.locator(`[data-ordinal="${ordinal}"]`).first() + if ((await candidate.count()) === 0) continue + const box = await candidate.boundingBox() + if (!box) continue + const wholly = box.y >= listBox.y + 20 && box.y + box.height <= listBox.y + listBox.height - 20 + if (wholly) return candidate + } + return undefined +} + test('opens a conversation onto the message a link points at', async ({page}) => { test.setTimeout(180_000) const smokeUser = process.env['KB_SMOKE_USER'] @@ -93,78 +168,121 @@ test('opens a conversation onto the message a link points at', async ({page}) => `[data-testid="${T.CHAT_INBOX_ROW}"], [data-testid="${T.CHAT_INBOX_CHANNEL_ROW}"]` ) - // B is discovered rather than hard-coded — a real username in a committed test would pin the suite - // to one machine — but it is pinned by the name it turns out to have for the rest of the run, so - // the second visit is provably the same conversation as the first. let convB = '' - const rowCount = Math.min(await rows.count(), 12) - for (let i = 0; i < rowCount && !convB; i++) { - const name = await rowName(rows.nth(i)) - // A is the smoke account's own conversation: the one message this flow sends goes there. - if (!name || name === smokeUser) continue - await openConversationNamed(page, rows, name) - const overflow = await scrollableOverflow(page) - if (overflow !== undefined && overflow >= MIN_SCROLLABLE_OVERFLOW) convB = name + let targetOrdinal = '' + let link = '' + + // ---- Try to reuse a link an earlier run already left in conversation A. ---- + await openConversationNamed(page, rows, smokeUser!) + const existing = await findExistingLink(page) + if (existing) { + try { + await openConversationNamed(page, rows, existing.convName) + const overflow = await scrollableOverflow(page) + const listBox = + overflow !== undefined && overflow >= MIN_SCROLLABLE_OVERFLOW + ? await page.getByTestId(T.CHAT_MESSAGE_LIST).first().boundingBox() + : null + if (listBox) { + await page.mouse.move(listBox.x + listBox.width / 2, listBox.y + listBox.height / 2) + const row = await findReusedTargetRow(page, listBox, existing.ordinal) + if (row) { + convB = existing.convName + targetOrdinal = existing.ordinal + link = existing.text + } + } + } catch { + // existing.convName no longer names an inbox row (conversation renamed or gone) - fall + // through to sending a fresh link below. + } } - expect(convB, 'no conversation had enough history to jump within, so nothing was checked').toBeTruthy() + console.log( + link + ? `[chat-link-jump] reusing an existing link: ${link}` + : '[chat-link-jump] no reusable link found; sending a new one' + ) - // Scroll back through B until the newest message is far enough away, then take a row that is - // wholly on screen and small enough to measure. `data-ordinal` is the app's own per-message - // attribute; there is no testID for "a message row", and the ordinal is what the copied link - // encodes, so it is also how the landing is checked at the other end. - const listBox = await page.getByTestId(T.CHAT_MESSAGE_LIST).first().boundingBox() - expect(listBox, 'the message list has no box').not.toBeNull() - await page.mouse.move(listBox!.x + listBox!.width / 2, listBox!.y + listBox!.height / 2) + // ---- Fall back to the original flow: discover a conversation and send a fresh link. ---- + if (!link) { + // B is discovered rather than hard-coded — a real username in a committed test would pin the + // suite to one machine — but it is pinned by the name it turns out to have for the rest of the + // run, so the second visit is provably the same conversation as the first. + const rowCount = Math.min(await rows.count(), 12) + for (let i = 0; i < rowCount && !convB; i++) { + const name = await rowName(rows.nth(i)) + // A is the smoke account's own conversation: the one message this flow sends goes there. + if (!name || name === smokeUser) continue + await openConversationNamed(page, rows, name) + const overflow = await scrollableOverflow(page) + if (overflow !== undefined && overflow >= MIN_SCROLLABLE_OVERFLOW) convB = name + } + expect( + convB, + 'no conversation had enough history to jump within, so nothing was checked' + ).toBeTruthy() - let targetOrdinal = '' - let targetRow: Locator | undefined - for (let attempt = 0; attempt < 30 && !targetRow; attempt++) { - await page.mouse.wheel(0, -600) - await page.waitForTimeout(250) - if (((await distanceFromEnd(page)) ?? 0) < MIN_DISTANCE_FROM_END) continue - await page.waitForTimeout(500) - const candidates = page.locator('[data-ordinal]') - const n = await candidates.count() - for (let i = 0; i < n; i++) { - const candidate = candidates.nth(i) - const box = await candidate.boundingBox() - if (!box) continue - const text = (await candidate.innerText()).trim() - const wholly = box.y >= listBox!.y + 20 && box.y + box.height <= listBox!.y + listBox!.height - 20 - // Media rows are taller than the viewport's usable band and often have no text to identify - // them by, so a short text row is the honest handle here. - if (box.height > 250 || !wholly || text.length < 6) continue - targetOrdinal = (await candidate.getAttribute('data-ordinal')) ?? '' - targetRow = candidate - break + // Scroll back through B until the newest message is far enough away, then take a row that is + // wholly on screen and small enough to measure. `data-ordinal` is the app's own per-message + // attribute; there is no testID for "a message row", and the ordinal is what the copied link + // encodes, so it is also how the landing is checked at the other end. + const listBox = await page.getByTestId(T.CHAT_MESSAGE_LIST).first().boundingBox() + expect(listBox, 'the message list has no box').not.toBeNull() + await page.mouse.move(listBox!.x + listBox!.width / 2, listBox!.y + listBox!.height / 2) + + let targetRow: Locator | undefined + for (let attempt = 0; attempt < 30 && !targetRow; attempt++) { + await page.mouse.wheel(0, -600) + await page.waitForTimeout(250) + if (((await distanceFromEnd(page)) ?? 0) < MIN_DISTANCE_FROM_END) continue + await page.waitForTimeout(500) + const candidates = page.locator('[data-ordinal]') + const n = await candidates.count() + for (let i = 0; i < n; i++) { + const candidate = candidates.nth(i) + const box = await candidate.boundingBox() + if (!box) continue + const text = (await candidate.innerText()).trim() + const wholly = box.y >= listBox!.y + 20 && box.y + box.height <= listBox!.y + listBox!.height - 20 + // Media rows are taller than the viewport's usable band and often have no text to identify + // them by, so a short text row is the honest handle here. + if (box.height > 250 || !wholly || text.length < 6) continue + targetOrdinal = (await candidate.getAttribute('data-ordinal')) ?? '' + targetRow = candidate + break + } } - } - expect(targetRow, `no message in "${convB}" was far enough above its newest one to link to`).toBeTruthy() + expect( + targetRow, + `no message in "${convB}" was far enough above its newest one to link to` + ).toBeTruthy() - await targetRow!.hover() - await targetRow!.getByTestId(T.CHAT_MESSAGE_MENU_BUTTON).first().click({force: true, timeout: 10_000}) - await page.getByText('Copy a link to this message').first().click({timeout: 10_000}) - await page.waitForTimeout(700) + await targetRow!.hover() + await targetRow!.getByTestId(T.CHAT_MESSAGE_MENU_BUTTON).first().click({force: true, timeout: 10_000}) + await page.getByText('Copy a link to this message').first().click({timeout: 10_000}) + await page.waitForTimeout(700) - // Pasting rather than typing the link out: the copy above put it on the real clipboard and there - // is no way to read that back from the renderer (clipboard-read permission is denied in the app), - // so the paste both delivers it and reveals what was copied. A plain Meta+V is a synthetic key - // event the renderer ignores; the editing command has to be attached to it. - await openConversationNamed(page, rows, smokeUser!) - const input = page.getByTestId(T.CHAT_INPUT) - await input.click() - await input.fill('') - const cdp = await page.context().newCDPSession(page) - const key = {code: 'KeyV', key: 'v', modifiers: 4, nativeVirtualKeyCode: 86, windowsVirtualKeyCode: 86} - await cdp.send('Input.dispatchKeyEvent', {...key, commands: ['paste'], type: 'keyDown'}) - await cdp.send('Input.dispatchKeyEvent', {...key, type: 'keyUp'}) - await page.waitForTimeout(600) - const link = await input.inputValue() - expect(link, 'the message menu did not put a keybase chat link on the clipboard').toMatch( - new RegExp(`^keybase://chat/.+/${targetOrdinal}$`) - ) - await input.press('Enter') + // Pasting rather than typing the link out: the copy above put it on the real clipboard and + // there is no way to read that back from the renderer (clipboard-read permission is denied in + // the app), so the paste both delivers it and reveals what was copied. A plain Meta+V is a + // synthetic key event the renderer ignores; the editing command has to be attached to it. + await openConversationNamed(page, rows, smokeUser!) + const input = page.getByTestId(T.CHAT_INPUT) + await input.click() + await input.fill('') + const cdp = await page.context().newCDPSession(page) + const key = {code: 'KeyV', key: 'v', modifiers: 4, nativeVirtualKeyCode: 86, windowsVirtualKeyCode: 86} + await cdp.send('Input.dispatchKeyEvent', {...key, commands: ['paste'], type: 'keyDown'}) + await cdp.send('Input.dispatchKeyEvent', {...key, type: 'keyUp'}) + await page.waitForTimeout(600) + link = await input.inputValue() + expect(link, 'the message menu did not put a keybase chat link on the clipboard').toMatch( + new RegExp(`^keybase://chat/.+/${targetOrdinal}$`) + ) + await input.press('Enter') + } + await openConversationNamed(page, rows, smokeUser!) const sentLink = page.locator(`[data-testid="${T.CHAT_MESSAGE_LIST}"]`).getByText(link, {exact: true}) await expect(sentLink.last()).toBeVisible({timeout: 15_000}) await sentLink.last().click() From 96345f7c277aa042c59c254029a8e1d4c1f49184 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 19 Aug 2026 08:04:30 -0400 Subject: [PATCH 37/38] test(e2e): open each conversation for real before measuring its end The inbox row of the conversation already on screen has no click handler, so clicking it measured whatever state an earlier flow left the thread in - a search hit scrolled back into history, 6351px above the newest message - as if this test had just opened it. Click a neighbour first so every measured open is an open. --- .../e2e/electron/flows/chat-thread-bottom.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/shared/tests/e2e/electron/flows/chat-thread-bottom.test.ts b/shared/tests/e2e/electron/flows/chat-thread-bottom.test.ts index 667eab4b2d25..0e94c5f0d0f7 100644 --- a/shared/tests/e2e/electron/flows/chat-thread-bottom.test.ts +++ b/shared/tests/e2e/electron/flows/chat-thread-bottom.test.ts @@ -71,6 +71,19 @@ test('opens every conversation on its newest message', async ({page}) => { for (let i = 0; i < rowCount && checked.length < 5; i++) { const row = rows.nth(i) const name = (await row.innerText().catch(() => '')).split('\n')[0] ?? `row ${i}` + // Open a neighbour first so the click below is a real open. The inbox row of the conversation + // already on screen has no click handler at all (chat/inbox/row/small-team: onSelectConversation + // is undefined when isSelected), so clicking it changes nothing - and a thread left parked in + // its history by an earlier flow would be measured as if this test had just opened it. That is + // what made this fail only when a search flow ran first and left its conversation open. + if (rowCount > 1) { + await rows + .nth((i + 1) % rowCount) + .click({force: true, timeout: 10_000}) + .catch(() => {}) + await page.waitForSelector(`[data-testid="${T.CHAT_MESSAGE_LIST}"]`, {timeout: 10_000}) + await page.waitForTimeout(500) + } await row.click({force: true, timeout: 10_000}).catch(() => {}) await page.waitForSelector(`[data-testid="${T.CHAT_MESSAGE_LIST}"]`, {timeout: 10_000}) // Long enough for the delayed images to commit and for anything following the end to react. From 459587cc62cc58ddc4608bcf661278b580c071c4 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 19 Aug 2026 10:07:34 -0400 Subject: [PATCH 38/38] fix(chat): pass maintainVisibleContentPosition as the documented boolean Object and boolean normalize identically today (normalizeMaintainVisibleContentPosition fills size ?? true), but a partial config opts out of whatever it does not name, which is how maintainScrollAtEnd's {on: {...}} list once lost a trigger. The boolean also avoids rebuilding a config object every render, which is why native needed a module level const for it. --- shared/chat/conversation/list-area/index.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/shared/chat/conversation/list-area/index.tsx b/shared/chat/conversation/list-area/index.tsx index 1144ff142f30..765cfa4f9678 100644 --- a/shared/chat/conversation/list-area/index.tsx +++ b/shared/chat/conversation/list-area/index.tsx @@ -47,10 +47,6 @@ const noOrdinals: ReadonlyArray = [] // Stable config so it doesn't churn props each render. Empty = enable adaptive render with defaults. const adaptiveRenderConfig = {} -// Stable MVCP config (anchor visible rows across data prepends). Referenced by native; desktop -// inlines an equivalent. -const mvcpData = {data: true} as const - const keyExtractor = (ordinal: ItemType) => String(ordinal) // Item type for list recycling pool separation. A message that leads its author group renders an @@ -514,7 +510,10 @@ const DesktopThreadWrapper = function DesktopThreadWrapper() { } // Stays on while centered: the full thread response lands after the cached one and // re-measures rows above the target, which slides it out of view unless anchored. - maintainVisibleContentPosition={{data: true}} + // The documented boolean form, which enables both anchors. A partial config object opts + // out of whatever it does not name — `data` defaults to false — so naming keys here would + // silently narrow this the way maintainScrollAtEnd's {on: {...}} list once did. + maintainVisibleContentPosition={true} onLoad={onLoad} onScroll={onScroll as unknown as (e: unknown) => void} onStartReached={onStartReached} @@ -734,7 +733,7 @@ const NativeConversationList = function NativeConversationList() { // above the target and rows above it swap estimatedItemSize for their measured height, // which slides the target out of view unless it stays anchored. Toggling this prop off // and back on also makes the list jump, so it is mounted with one config throughout. - maintainVisibleContentPosition={mvcpData} + maintainVisibleContentPosition={true} onStartReached={onStartReached} onStartReachedThreshold={2} onEndReached={onEndReached}