diff --git a/shared/chat/conversation/list-area/index.tsx b/shared/chat/conversation/list-area/index.tsx index dda21755fdf5..541474f66544 100644 --- a/shared/chat/conversation/list-area/index.tsx +++ b/shared/chat/conversation/list-area/index.tsx @@ -138,7 +138,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 @@ -212,6 +211,60 @@ 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 (patches/@legendapp+list scrollTargetSettle), 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]) + + // Unconditional on purpose, and safe to be: every imperative scroll on the ref calls + // supersedeInitialScroll synchronously inside runScrollWithPromise (see + // node_modules/@legendapp/list/react.mjs), which cancels the list's own initialScrollIndex + // bootstrap before the scroll is even queued. So this call cannot race the bootstrap - it + // supersedes it by construction and always wins. A library bump that broke that guarantee is the + // one thing that would make this regress. + // + // A guard that stood this call down while the bootstrap looked like it owned the target was tried + // and cannot be made 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 leaves it at the end, 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 + 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}) + // datasetKey is a dependency without being read: the layout effect above clears the latch on a + // new dataset, and this effect has to re-run afterwards to scroll again for the same ordinal. + }, [centeredOrdinal, datasetKey, listRef, messageOrdinals, ready]) +} + const DesktopThreadWrapper = function DesktopThreadWrapper() { const desktopStyles = useDesktopStyles() const editingOrdinal = InputState.useConversationInput(s => s.editing) @@ -221,7 +274,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) @@ -319,111 +374,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. - const lastScrolledCenteredRef = React.useRef(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) - } else if (lastScrolledCenteredRef.current !== undefined) { - lastScrolledCenteredRef.current = undefined - abortCentering() - if (containsLatestMessage) { - void listRef.current?.scrollToEnd({animated: false}) - } - } - }, [abortCentering, centeredOrdinal, loaded, containsLatestMessage, messageOrdinals]) + useScrollToCentered({centeredOrdinal, datasetKey, listRef, messageOrdinals, ready: loaded}) // Scroll to the message being edited const lastEditingOrdinalRef = React.useRef(undefined) @@ -527,12 +478,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 (
void} onStartReached={onStartReached} diff --git a/shared/chat/conversation/messages/special-top-message.tsx b/shared/chat/conversation/messages/special-top-message.tsx index 6b6c4bc021eb..c55e71e379d5 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 42cb9dd9ef89..2a10f1064503 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 @@ -905,7 +906,7 @@ function RightSide(p: RProps) { )} > - + ) @@ -1018,7 +1019,13 @@ export function WrapperMessage(p: WrapperMessageProps) { const messageContext = {isHighlighted: showCenteredHighlight, ordinal} const row = ( - + - // 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 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)`} > - + { ) : null return ( - + 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; ++ ++// 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) { +- (_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 { 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" ? { +- completion, +- kind, +- previousDataLength +- } : { +- bootstrap, +- completion, +- 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) { +- 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 +- }); ++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; +- 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; ++ ++// 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) { +- 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; ++ 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 = {}) { ++ 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; +- 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); ++ 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, +- completion, +- kind, +- 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" || 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; +- } +- ); +- } +-} +- +-// 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) { ++ ++// 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; +- 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; ++ ++// 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")) { +- 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); ++ 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, { +- resetLayout, +- resetInitialScroll +-}) { +- const { state } = ctx; +- 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); +-} +-function setInitialRenderState(ctx, { +- didLayout, +- didInitialScroll +-}) { +- const { state } = ctx; +- const { +- loadStartTime, +- props: { onLoad } +- } = 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); +- } +- 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, ++ 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 (!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; +-function syncInitialScrollOffset(state, offset) { +- 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; +- 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 isNativeInitialNonZeroTarget(state) { ++ const targetOffset = getInitialScrollWatchdogTargetOffset(state); ++ return !state.didFinishInitialScroll && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); + } +-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); +- } ++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"; +- 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; ++ if (((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap") { ++ return false; + } +- complete(); +-} +- +-// src/core/calculateOffsetForIndex.ts +-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) { ++function shouldFinishInitialZeroTargetScroll(ctx) { ++ var _a3; + 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); ++ 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 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) ++ }; + } +-function updateContentMetricsState(ctx) { +- var _a3; ++function checkFinishedScrollFrame(ctx) { ++ const scrollingTo = ctx.state.scrollingTo; ++ if (!scrollingTo) { ++ return; ++ } + 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 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; + } +- 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 ++ ); ++ 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; + } +- 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--; ++ } ++ } ++ }; ++ 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) { ++function settleScrollTarget(ctx) { + var _a3; + const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- 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 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) { +- 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 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; ++ 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/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; +- 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); ++ 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; + } + +-// 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/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); ++ 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 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; ++ 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); ++ } ++ } + } +-function getInitialScrollWatchdogTargetOffset(state) { ++ ++// src/utils/getEffectiveDrawDistance.ts ++var INITIAL_DRAW_DISTANCE = 50; ++function getEffectiveDrawDistance(ctx, mode) { + 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/setInitialRenderState.ts ++function resetInitialRenderState(ctx, { ++ resetLayout, ++ resetInitialScroll ++}) { ++ const { state } = ctx; ++ if (resetLayout) { ++ state.didContainersLayout = false; ++ state.queuedInitialLayout = false; + } +- 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); + } +-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; +-} +-function isEndAlignedLastItemTarget(ctx, scrollingTo) { +- return scrollingTo.index === ctx.state.props.data.length - 1 && scrollingTo.viewPosition === 1; ++ 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 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; ++function syncInitialScrollOffset(state, offset) { ++ state.scroll = offset; ++ state.scrollPending = offset; ++ state.scrollPrev = offset; + } +-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 clearPreservedInitialScrollTargetTimeout(state) { ++ state.scheduledWork.cancel("preservedInitialScroll"); + } +-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 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; ++ 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 checkFinishedScrollFallback(ctx) { ++function finishInitialScroll(ctx, options) { ++ var _a3, _b, _c; + 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") { ++ 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 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"; ++ 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" + ); +- 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; + } +- 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/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); ++ } + } + } + 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); ++ 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(); +@@ -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..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; + 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"); ++// 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 { +- ...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; ++ ++// 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) { +- (_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 { 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" ? { +- completion, +- kind, +- previousDataLength +- } : { +- bootstrap, +- completion, +- 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) { +- 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 +- }); ++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; +- 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; ++ ++// 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) { +- 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; ++ 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 = {}) { ++ 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; +- 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); ++ 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, +- completion, +- kind, +- 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" || 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; +- } +- ); +- } +-} +- +-// 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) { ++ ++// 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; +- 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; ++ ++// 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")) { +- 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); ++ 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, { +- resetLayout, +- resetInitialScroll +-}) { +- const { state } = ctx; +- 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); +-} +-function setInitialRenderState(ctx, { +- didLayout, +- didInitialScroll +-}) { +- const { state } = ctx; +- const { +- loadStartTime, +- props: { onLoad } +- } = 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); +- } +- 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, ++ 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 (!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; +-function syncInitialScrollOffset(state, offset) { +- 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; +- 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 isNativeInitialNonZeroTarget(state) { ++ const targetOffset = getInitialScrollWatchdogTargetOffset(state); ++ return !state.didFinishInitialScroll && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset); + } +-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); +- } ++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"; +- 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; ++ if (((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap") { ++ return false; + } +- complete(); +-} +- +-// src/core/calculateOffsetForIndex.ts +-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) { ++function shouldFinishInitialZeroTargetScroll(ctx) { ++ var _a3; + 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); ++ 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 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) ++ }; + } +-function updateContentMetricsState(ctx) { +- var _a3; ++function checkFinishedScrollFrame(ctx) { ++ const scrollingTo = ctx.state.scrollingTo; ++ if (!scrollingTo) { ++ return; ++ } + 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 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; + } +- 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 ++ ); ++ 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; + } +- 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--; ++ } ++ } ++ }; ++ 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) { ++function settleScrollTarget(ctx) { + var _a3; + const state = ctx.state; +- const { index, viewOffset, viewPosition } = params; +- 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 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) { +- 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 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; ++ 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/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; +- 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); ++ 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; + } + +-// 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/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); ++ 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 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; ++ 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); ++ } ++ } + } +-function getInitialScrollWatchdogTargetOffset(state) { ++ ++// src/utils/getEffectiveDrawDistance.ts ++var INITIAL_DRAW_DISTANCE = 50; ++function getEffectiveDrawDistance(ctx, mode) { + 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/setInitialRenderState.ts ++function resetInitialRenderState(ctx, { ++ resetLayout, ++ resetInitialScroll ++}) { ++ const { state } = ctx; ++ if (resetLayout) { ++ state.didContainersLayout = false; ++ state.queuedInitialLayout = false; + } +- 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); + } +-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; +-} +-function isEndAlignedLastItemTarget(ctx, scrollingTo) { +- return scrollingTo.index === ctx.state.props.data.length - 1 && scrollingTo.viewPosition === 1; ++ 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 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; ++function syncInitialScrollOffset(state, offset) { ++ state.scroll = offset; ++ state.scrollPending = offset; ++ state.scrollPrev = offset; + } +-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 clearPreservedInitialScrollTargetTimeout(state) { ++ state.scheduledWork.cancel("preservedInitialScroll"); + } +-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 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; ++ 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 checkFinishedScrollFallback(ctx) { ++function finishInitialScroll(ctx, options) { ++ var _a3, _b, _c; + 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") { ++ 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 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"; ++ 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" + ); +- 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; + } +- 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/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); ++ } + } + } + 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); ++ 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(); +@@ -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..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; + 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(); ++// 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 { +- ...event, +- 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; ++ ++// 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) { +- (_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 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" ? { +- completion, +- kind, +- previousDataLength +- } : { +- bootstrap, +- completion, +- 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) { +- 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 +- }); ++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; +- 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; ++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) { +- 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; ++ 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 = {}) { ++ 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; +- 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); ++ 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, +- completion, +- kind, +- 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"); +-} +-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/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; ++ 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); ++ } ++ }, ++ 100, ++ "platformScrollCompletion" ++ ); + } +- 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; ++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); + } +- } +-} +- +-// 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); ++ 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); ++ 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); + } +-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"; +- 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" +- ); ++ 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) { +- 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 = 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; +- return index !== void 0 ? state.positions[index] || 0 : 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; ++ } ++ state.scrollTargetSettle = { ++ expiresAt: Date.now() + SETTLE_TTL_MS, ++ id: getId(state, index), ++ quietPasses: 0, ++ viewOffset, ++ viewPosition ++ }; ++} ++function settleScrollTarget(ctx) { ++ var _a3; ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ 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) { ++ 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; ++ } ++ 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 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/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 } ++ } ++ }; ++} ++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/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, { ++ 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); + } +- +-// 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 }); ++ } + } + } +- 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"); ++} ++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; +- 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 { 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; ++ 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 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(); ++} ++ ++// src/core/calculateOffsetForIndex.ts ++function calculateOffsetForIndex(ctx, index) { ++ const state = ctx.state; ++ return index !== void 0 ? state.positions[index] || 0 : 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); ++ } + } + } + 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); ++ 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(); +@@ -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..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; + 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(); ++// 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 { +- ...event, +- 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; ++ ++// 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) { +- (_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 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" ? { +- completion, +- kind, +- previousDataLength +- } : { +- bootstrap, +- completion, +- 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) { +- 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 +- }); ++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; +- 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; ++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) { +- 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; ++ 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 = {}) { ++ 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; +- 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); ++ 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, +- completion, +- kind, +- 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"); +-} +-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/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; ++ 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); ++ } ++ }, ++ 100, ++ "platformScrollCompletion" ++ ); + } +- 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; ++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); + } +- } +-} +- +-// 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); ++ 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); ++ 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); + } +-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"; +- 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" +- ); ++ 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) { +- 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 = 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; +- return index !== void 0 ? state.positions[index] || 0 : 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; ++ } ++ state.scrollTargetSettle = { ++ expiresAt: Date.now() + SETTLE_TTL_MS, ++ id: getId(state, index), ++ quietPasses: 0, ++ viewOffset, ++ viewPosition ++ }; ++} ++function settleScrollTarget(ctx) { ++ var _a3; ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ 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) { ++ 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; ++ } ++ 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 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/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 } ++ } ++ }; ++} ++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/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, { ++ 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); + } +- +-// 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 }); ++ } + } + } +- 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"); ++} ++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; +- 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 { 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; ++ 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 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(); ++} ++ ++// src/core/calculateOffsetForIndex.ts ++function calculateOffsetForIndex(ctx, index) { ++ const state = ctx.state; ++ return index !== void 0 ? state.positions[index] || 0 : 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); ++ } + } + } + 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(); +@@ -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..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; + 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(); ++// 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 { +- ...event, +- 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; ++ ++// 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) { +- (_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 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" ? { +- completion, +- kind, +- previousDataLength +- } : { +- bootstrap, +- completion, +- 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) { +- 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 +- }); ++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; +- 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; ++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) { +- 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; ++ 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 = {}) { ++ 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; +- 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); ++ 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, +- completion, +- kind, +- 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"); +-} +-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/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; ++ 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); ++ } ++ }, ++ 100, ++ "platformScrollCompletion" ++ ); + } +- 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; ++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); + } +- } +-} +- +-// 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); ++ 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); ++ 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); + } +-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"; +- 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" +- ); ++ 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) { +- 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 = 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; +- return index !== void 0 ? state.positions[index] || 0 : 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; ++ } ++ state.scrollTargetSettle = { ++ expiresAt: Date.now() + SETTLE_TTL_MS, ++ id: getId(state, index), ++ quietPasses: 0, ++ viewOffset, ++ viewPosition ++ }; ++} ++function settleScrollTarget(ctx) { ++ var _a3; ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ 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) { ++ 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; ++ } ++ 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 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/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 } ++ } ++ }; ++} ++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/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, { ++ 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); + } +- +-// 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 }); ++ } + } + } +- 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"); ++} ++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; +- 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 { 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; ++ 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 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(); ++} ++ ++// src/core/calculateOffsetForIndex.ts ++function calculateOffsetForIndex(ctx, index) { ++ const state = ctx.state; ++ return index !== void 0 ? state.positions[index] || 0 : 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); ++ } + } + } + 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); ++ 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(); +@@ -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..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; + 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(); ++// 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 { +- ...event, +- 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; ++ ++// 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) { +- (_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 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" ? { +- completion, +- kind, +- previousDataLength +- } : { +- bootstrap, +- completion, +- 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) { +- 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 +- }); ++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; +- 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; ++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) { +- 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; ++ 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 = {}) { ++ 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; +- 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); ++ 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, +- completion, +- kind, +- 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"); +-} +-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/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; ++ 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); ++ } ++ }, ++ 100, ++ "platformScrollCompletion" ++ ); + } +- 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; ++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); + } +- } +-} +- +-// 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); ++ 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); ++ 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); + } +-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"; +- 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" +- ); ++ 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) { +- 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 = 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; +- return index !== void 0 ? state.positions[index] || 0 : 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; ++ } ++ state.scrollTargetSettle = { ++ expiresAt: Date.now() + SETTLE_TTL_MS, ++ id: getId(state, index), ++ quietPasses: 0, ++ viewOffset, ++ viewPosition ++ }; ++} ++function settleScrollTarget(ctx) { ++ var _a3; ++ const state = ctx.state; ++ const settle = state.scrollTargetSettle; ++ if (!settle) { ++ 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) { ++ 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; ++ } ++ 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 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/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 } ++ } ++ }; ++} ++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/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, { ++ 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); + } +- +-// 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 }); ++ } + } + } +- 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"); ++} ++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; +- 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 { 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; ++ 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 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(); ++} ++ ++// src/core/calculateOffsetForIndex.ts ++function calculateOffsetForIndex(ctx, index) { ++ const state = ctx.state; ++ return index !== void 0 ? state.positions[index] || 0 : 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); ++ } + } + } + 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(); +@@ -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; + } 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..5b9294e87d08 --- /dev/null +++ b/shared/tests/e2e/electron/flows/chat-link-jump.test.ts @@ -0,0 +1,348 @@ +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. +// +// 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. +// No retries. This flow is timing-dependent end to end - it scrolls, waits for fetches to land and +// measures where rows settle - so a retry would mostly re-roll the timing rather than re-test the +// behaviour, and a flake would be hidden instead of reported. 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. 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 + +// 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() ?? '' + +// 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 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) + return + } + 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. +// Matched per line (`m`), not against the whole element: the [data-ordinal] element is +// HighlightableRow's wrapper, so its innerText also carries the separator and, when the message +// leads an author group, an author header - "someone\n12:34 PM\nkeybase://chat/...". Anchoring +// without `m` therefore never matches a real row. The anchors still matter: they require the link +// to be a whole line, so a link quoted inside a longer sentence is not mistaken for one of ours. +const LINK_PATTERN = /^keybase:\/\/chat\/(.+)\/(\d+)$/m + +// How many of the currently rendered rows in conversation A are link messages this flow left +// behind. Logged so a run's output shows whether the reuse path is holding: the count must not grow +// from one run to the next, because a growing count is exactly the permanent chat-history litter +// the reuse path exists to prevent. +const countLinkMessages = async (page: Page): Promise => { + const candidates = page.locator('[data-ordinal]') + const n = await candidates.count() + let found = 0 + for (let i = 0; i < n; i++) { + if (LINK_PATTERN.test(await candidates.nth(i).innerText())) found++ + } + return found +} + +// 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> => { + // The pointer has to be over the message list before any wheel event: the last click was on an + // inbox row, so without this the wheel scrolls the inbox and the message list never moves. + const listBox = await page.getByTestId(T.CHAT_MESSAGE_LIST).first().boundingBox() + if (!listBox) return undefined + await page.mouse.move(listBox.x + listBox.width / 2, listBox.y + listBox.height / 2) + 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 match = LINK_PATTERN.exec(await candidates.nth(i).innerText()) + if (match) return {convName: match[1]!, ordinal: match[2]!, text: match[0]} + } + 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'] + 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}"]` + ) + + let convB = '' + let targetOrdinal = '' + let link = '' + + // ---- Try to reuse a link an earlier run already left in conversation A. ---- + await openConversationNamed(page, rows, smokeUser!) + console.log(`[chat-link-jump] link messages rendered in A before this run: ${await countLinkMessages(page)}`) + 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. + } + } + console.log( + link + ? `[chat-link-jump] reusing an existing link: ${link}` + : '[chat-link-jump] no reusable link found; sending a new one' + ) + + // ---- 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() + + // 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() + + 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) + 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() + + // 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/electron/flows/chat-search-hit.test.ts b/shared/tests/e2e/electron/flows/chat-search-hit.test.ts new file mode 100644 index 000000000000..f44c27d4846a --- /dev/null +++ b/shared/tests/e2e/electron/flows/chat-search-hit.test.ts @@ -0,0 +1,111 @@ +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' + +// Searching a thread has to land on the hit, and then leave the thread alone. Both were manual +// checks until now: the list was landing with the hit far below the viewport, and after that was +// fixed a thread the reader had scrolled away from could still pull itself back. +// +// 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 + // 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. + 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++ + // 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)})` + ).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. + // 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) + 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') + } 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') + } +}) diff --git a/shared/tests/e2e/electron/flows/chat-thread-bottom.test.ts b/shared/tests/e2e/electron/flows/chat-thread-bottom.test.ts new file mode 100644 index 000000000000..0e94c5f0d0f7 --- /dev/null +++ b/shared/tests/e2e/electron/flows/chat-thread-bottom.test.ts @@ -0,0 +1,108 @@ +import type {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 has to leave the reader on its newest message. It stopped doing that for +// threads whose rows grow a frame or two after the list has already landed - a link preview +// committing, an image measuring, the full thread response replacing the cached one. The list +// anchored to the end it could see, the content then grew past it, and the thread sat hundreds of +// pixels above the newest message. Measured in the app: it landed on an extent of 6891, the content +// committed 7224, and the thread stayed 432px short of the end. +// +// Image responses are held back so that growth lands after the initial scroll rather than before it, +// which is what makes this able to fail rather than passing on whatever the disk cache happened to +// have warm. +const IMAGE_DELAY_MS = 700 +// A couple of pixels of sub-pixel residue is fine; anything more is a reader looking at old +// messages with the newest one off screen. +const MAX_DISTANCE_FROM_END = 8 +// Threads shorter than their viewport cannot be short of their end, so they prove nothing. +const MIN_SCROLLABLE_OVERFLOW = 200 + +type ListMetrics = {clientHeight: number; distanceFromEnd: number; scrollHeight: number} | null + +// The scroller is the list element LegendList renders inside the wrapper that carries the testID. +// Reached through globalThis because this suite's tsconfig has no DOM lib, the same way the app's +// desktop-only code does it. +const readListMetrics = 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 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}` + // 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. + 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 bafe3dbeb78f..803e9a96e131 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' @@ -31,6 +32,18 @@ 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' +// 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 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. +export const CHAT_THREAD_TOP = 'chat-thread-top' // Files export const FILES_BROWSER = 'files-browser'