From a33fa2f0f9d95c1b83d87584e241a8485c37b07b Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 19 Aug 2026 10:11:46 -0400 Subject: [PATCH 1/5] fix(chat): patch legend-list for jump-to-message targeting Two library fixes the app cannot get at from outside, generated from fix/jump-to-item-scroll-targeting off v3.3.7. scrollTargetSettle: a scrollToIndex/scrollToItem with a viewPosition is aimed at positions computed from estimatedItemSize, so it lands wherever the estimates above the target were wrong and never comes back. The list now holds the index + viewPosition target across the measurement settle and re-aims as rows report their real heights, which is what makes a search hit or a permalink land on the message instead of near it. headerLayout trigger for maintainScrollAtEnd: a header that lays out taller than the size it was rendered with pushes every item down by the difference, so a list that had already reached its end is short of it by that much with nothing to bring it back. Measured opening a one-to-one on desktop: the at-end initial scroll dispatched to 449 against 1228px of content, setHeaderSize reported 100 -> 152 in the same tick, and the thread sat 52px above its newest message permanently, on roughly half of opens. With the trigger, 30 of 30 opens landed on the newest message. Both are load-bearing: removing either makes one of the desktop flows added here fail. --- shared/patches/@legendapp+list+3.3.7.patch | 10814 +++++++++++++++++++ 1 file changed, 10814 insertions(+) create mode 100644 shared/patches/@legendapp+list+3.3.7.patch diff --git a/shared/patches/@legendapp+list+3.3.7.patch b/shared/patches/@legendapp+list+3.3.7.patch new file mode 100644 index 000000000000..7f7fdf1dcd1e --- /dev/null +++ b/shared/patches/@legendapp+list+3.3.7.patch @@ -0,0 +1,10814 @@ +diff --git a/node_modules/@legendapp/list/animated.d.ts b/node_modules/@legendapp/list/animated.d.ts +index 14beb98..56eefce 100644 +--- a/node_modules/@legendapp/list/animated.d.ts ++++ b/node_modules/@legendapp/list/animated.d.ts +@@ -488,6 +488,12 @@ interface AlwaysRenderConfig { + interface MaintainScrollAtEndOnOptions { + dataChange?: boolean; + footerLayout?: boolean; ++ /** ++ * Keep the list pinned to the end when the header changes size. A header that measures larger ++ * than its estimate pushes every item down, which leaves a list that had already scrolled to ++ * its end short of it by the difference. ++ */ ++ headerLayout?: boolean; + itemLayout?: boolean; + layout?: boolean; + } +diff --git a/node_modules/@legendapp/list/react-native.d.ts b/node_modules/@legendapp/list/react-native.d.ts +index ce1fe00..9a4311c 100644 +--- a/node_modules/@legendapp/list/react-native.d.ts ++++ b/node_modules/@legendapp/list/react-native.d.ts +@@ -488,6 +488,12 @@ interface AlwaysRenderConfig { + interface MaintainScrollAtEndOnOptions { + dataChange?: boolean; + footerLayout?: boolean; ++ /** ++ * Keep the list pinned to the end when the header changes size. A header that measures larger ++ * than its estimate pushes every item down, which leaves a list that had already scrolled to ++ * its end short of it by the difference. ++ */ ++ headerLayout?: boolean; + itemLayout?: boolean; + layout?: boolean; + } +diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js +index 6f41da3..1108caf 100644 +--- a/node_modules/@legendapp/list/react-native.js ++++ b/node_modules/@legendapp/list/react-native.js +@@ -458,171 +458,279 @@ var EDGE_POSITION_EPSILON = 1; + 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 +@@ -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; + } From 008763ec0fafef539729d67abfbe52d16cb4d426 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 19 Aug 2026 10:13:03 -0400 Subject: [PATCH 2/5] fix(chat): hand the centred scroll to the list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The desktop thread centred a search hit, a reply-quote jump or a permalink with a closed loop of its own: measure the target row's offset from the viewport centre, ask for a corrected scroll offset, repeat for up to 3s, and abort on a wheel event. It was compensating for the library aiming at positions built from estimatedItemSize, and it compensated badly — it fought maintainVisibleContentPosition for the same scroll offset, it re-centred a reader who had scrolled away while the loop was still running, and near either end of a thread it spun against a clamp. With scrollTargetSettle in the legend-list patch the list holds the target across the measurement settle itself, so the app's job shrinks to issuing one scroll per target and getting out of the way. useScrollToCentered does exactly that: wait for the target to be present in messageOrdinals (a centred load clears the thread first, so it is briefly absent), scroll once, and reset per dataset rather than per conversation. The scroll is unconditional rather than deferring to the list's own initialScrollIndex bootstrap. A permalink mounts the thread with no centred target, so the list is built with initialScrollAtEnd and the bootstrap it re-arms when the centred dataset lands does not move it — the thread settles at its end and the target is never shown. That path is indistinguishable here from a warm in-thread jump, so this has to be the one authority that always fires. maintainVisibleContentPosition also becomes the documented boolean. A partial config object opts out of whatever it does not name, and `data` defaults to false, so {data: true} was quietly narrowing it the way maintainScrollAtEnd's {on: {...}} list once did. Native is untouched: it is still the FlatList thread master ships. --- shared/chat/conversation/list-area/index.tsx | 170 ++++++------------- shared/chat/conversation/thread-context.tsx | 8 +- 2 files changed, 60 insertions(+), 118 deletions(-) diff --git a/shared/chat/conversation/list-area/index.tsx b/shared/chat/conversation/list-area/index.tsx index dda21755fdf5..72474da99115 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,53 @@ 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. A guard that stood this call down while the list's own + // initialScrollIndex bootstrap looked like it owned the target was tried and measured against the + // app, and there is no version of it that is safe: on the permalink path the thread mounts with no + // centred target, so the list is built with initialScrollAtEnd, and the bootstrap it re-arms when + // the centred dataset lands does not move it - the thread settles at its end with the target never + // shown. That path is indistinguishable from a warm in-thread jump by anything visible here (both + // arrive as "dataset with a resolvable initialScrollIndex"), so this call has to be the one + // authority that always fires. + React.useEffect(() => { + if (!ready || centeredOrdinal === undefined) { + lastScrolledRef.current = undefined + return + } + if (lastScrolledRef.current === centeredOrdinal) return + if (sortedIndexOf(messageOrdinals as unknown as number[], centeredOrdinal as unknown as number) < 0) { + return + } + lastScrolledRef.current = centeredOrdinal + void listRef.current?.scrollToItem({animated: false, item: centeredOrdinal, viewPosition: 0.5}) + }, [centeredOrdinal, datasetKey, listRef, messageOrdinals, ready]) +} + const DesktopThreadWrapper = function DesktopThreadWrapper() { const desktopStyles = useDesktopStyles() const editingOrdinal = InputState.useConversationInput(s => s.editing) @@ -221,7 +267,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 +367,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 +471,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/thread-context.tsx b/shared/chat/conversation/thread-context.tsx index 598b2c74fa11..9a30b23a5214 100644 --- a/shared/chat/conversation/thread-context.tsx +++ b/shared/chat/conversation/thread-context.tsx @@ -96,9 +96,11 @@ ConversationThreadIDContext.displayName = 'ConversationThreadIDContext' export type ConversationThreadState = { accountsInfoMap: Map - // Bumped on every messagesClear. The desktop list remounts on it: LegendList cannot recover - // from a non-empty -> empty -> non-empty data transition (it resets its layout state and waits - // for a container layout event that never comes), so the thread renders blank forever. + // Bumped on every messagesClear, and fed to the list as its dataKey (not as a React key - the + // list is not remounted). LegendList cannot recover from a non-empty -> empty -> non-empty data + // transition on its own (it resets its layout state and waits for a container layout event that + // never comes), so the thread renders blank forever; the dataKey change is what tells it this is + // a new dataset and makes it reset rather than wait. clearVersion: number explodingMode: number flipStatusMap: Map From 64d4b5ee5b6c405db6d6ad8211c694c3326f9c55 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 19 Aug 2026 10:14:49 -0400 Subject: [PATCH 3/5] test(e2e): cover jumping to a message on desktop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three flows, all of which were manual checks: chat-search-hit opens a named conversation (the inbox is ordered by recency, so "the first row" is a different thread from one run to the next), searches it, and steps through hits with Enter — the wrap-around at the end is the jump that used to land off screen. Then it scrolls away and watches: a thread the reader has moved must not pull itself back once the rows scrolled past finish measuring. chat-thread-bottom opens conversations and measures how far each settled from its newest message. Image responses are delayed so the row 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 had warm. MAX_DISTANCE_FROM_END is 8px and retries are off on purpose: the growth is timing dependent, and a retry that passes hides the regression the flow exists for. chat-link-jump covers the cold path the other two do not — a thread mounting with a centred target already pending, the way a permalink arrives. It reaches it through the app's own affordances (copy a link to a message in B, paste and send it in A, click it), reuses a link an earlier run left behind rather than writing new data into the smoke user's real history each time, and checks the target both landed on screen and near the middle. The testIDs they need are added alongside: the desktop header search button, the centre-highlighted row, the per-message "..." menu, the big-team channel inbox row, and the header above the oldest loaded message. --- .../messages/special-top-message.tsx | 3 +- .../conversation/messages/wrapper/wrapper.tsx | 10 +- shared/chat/inbox-and-conversation-header.tsx | 8 +- shared/chat/inbox/row/big-team-channel.tsx | 9 +- .../e2e/electron/flows/chat-link-jump.test.ts | 323 ++++++++++++++++++ .../electron/flows/chat-search-hit.test.ts | 111 ++++++ .../electron/flows/chat-thread-bottom.test.ts | 108 ++++++ shared/tests/e2e/shared/test-ids.ts | 13 + 8 files changed, 580 insertions(+), 5 deletions(-) create mode 100644 shared/tests/e2e/electron/flows/chat-link-jump.test.ts create mode 100644 shared/tests/e2e/electron/flows/chat-search-hit.test.ts create mode 100644 shared/tests/e2e/electron/flows/chat-thread-bottom.test.ts diff --git a/shared/chat/conversation/messages/special-top-message.tsx b/shared/chat/conversation/messages/special-top-message.tsx index 6b6c4bc021eb..e6a1a2f17687 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,7 @@ 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..421769c5471e 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,12 @@ export function WrapperMessage(p: WrapperMessageProps) { const messageContext = {isHighlighted: showCenteredHighlight, ordinal} const row = ( - + { direction="vertical" tooltip={`Search in this chat (${C.shortcutSymbol}F)`} > - + { ) : null return ( - + 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. The second attempt runs against a thread this test has already fetched once, so a +// pass on retry would not be a pass on the path the first attempt took - the same reason +// chat-thread-bottom turns them off. +test.describe.configure({retries: 0}) + +const MIN_VISIBLE_HEIGHT = 24 +// Centring is checked loosely on purpose. Sub-pixel precision is not the point — landing on the +// right message rather than at one end of the thread is. A third of the viewport either side of the +// middle still fails every way this can go wrong (top of the loaded window, bottom of the thread, +// off screen entirely). +const CENTRE_BAND_FRACTION = 1 / 3 +// The target has to sit well above the newest message, or a thread that simply stayed at its end +// would pass without anything having jumped. 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. +const LINK_PATTERN = /^keybase:\/\/chat\/(.+)\/(\d+)$/ + +// Looks for a message already sitting in conversation A whose text is a link from an earlier run +// of this test. Recent messages are checked first without scrolling - every prior run that fell +// back to sending left one near the bottom, so the common case needs no scroll at all - and a +// bounded scroll back covers the case where other messages have since pushed all of them out of +// view. +const findExistingLink = async (page: Page): Promise<{convName: string; ordinal: string; text: string} | undefined> => { + for (let attempt = 0; attempt < 6; attempt++) { + const candidates = page.locator('[data-ordinal]') + const n = await candidates.count() + for (let i = n - 1; i >= 0; i--) { + const text = (await candidates.nth(i).innerText()).trim() + const match = LINK_PATTERN.exec(text) + if (match) return {convName: match[1]!, ordinal: match[2]!, text} + } + await page.mouse.wheel(0, -600) + await page.waitForTimeout(250) + } + return undefined +} + +// Scrolls conversation B upward looking for the reused link's target row. If it turns up wholly on +// screen at a point at least MIN_DISTANCE_FROM_END from B's current newest message, the reused link +// is still good. If the scroll runs out first - the target has scrolled closer to the end since the +// link was made, or is no longer reachable at all - the caller falls back to sending a fresh link +// instead: asserting a landing that could not distinguish "jumped" from "already there" would prove +// nothing. +const findReusedTargetRow = async ( + page: Page, + listBox: {x: number; y: number; width: number; height: number}, + ordinal: string +): Promise => { + for (let attempt = 0; attempt < 30; attempt++) { + await page.mouse.wheel(0, -600) + await page.waitForTimeout(250) + if (((await distanceFromEnd(page)) ?? 0) < MIN_DISTANCE_FROM_END) continue + await page.waitForTimeout(500) + const candidate = page.locator(`[data-ordinal="${ordinal}"]`).first() + if ((await candidate.count()) === 0) continue + const box = await candidate.boundingBox() + if (!box) continue + const wholly = box.y >= listBox.y + 20 && box.y + box.height <= listBox.y + listBox.height - 20 + if (wholly) return candidate + } + return undefined +} + +test('opens a conversation onto the message a link points at', async ({page}) => { + test.setTimeout(180_000) + const smokeUser = process.env['KB_SMOKE_USER'] + 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!) + 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' From 8a77edca2a28c9caa9961814c0456bacd993d3be Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 19 Aug 2026 10:54:09 -0400 Subject: [PATCH 4/5] test(e2e): make the link-jump flow actually reuse an old link The reuse path never engaged: LINK_PATTERN was anchored without the m flag but matched against the [data-ordinal] wrapper's innerText, which also carries the separator and an author header, and the scroll-back wheeled the inbox because the pointer was never moved into the message list. Every run therefore sent a fresh link into the smoke account's real chat history - twice per full suite run, since two projects match flows/**. Also log the number of leftover link messages so a run shows the count holding, and give retries: 0 its real reason. fix(chat): say what actually keeps the centred scroll authoritative The comment credited the weaker guarantee. Every imperative scroll calls supersedeInitialScroll synchronously inside runScrollWithPromise, so the app's call cancels the list bootstrap by construction; note the datasetKey dep too. --- shared/chat/conversation/list-area/index.tsx | 23 +++++++---- .../e2e/electron/flows/chat-link-jump.test.ts | 39 +++++++++++++++---- 2 files changed, 47 insertions(+), 15 deletions(-) diff --git a/shared/chat/conversation/list-area/index.tsx b/shared/chat/conversation/list-area/index.tsx index 72474da99115..541474f66544 100644 --- a/shared/chat/conversation/list-area/index.tsx +++ b/shared/chat/conversation/list-area/index.tsx @@ -236,14 +236,19 @@ const useScrollToCentered = (p: { lastScrolledRef.current = undefined }, [datasetKey]) - // Unconditional on purpose. A guard that stood this call down while the list's own - // initialScrollIndex bootstrap looked like it owned the target was tried and measured against the - // app, and there is no version of it that is safe: on the permalink path the thread mounts with no - // centred target, so the list is built with initialScrollAtEnd, and the bootstrap it re-arms when - // the centred dataset lands does not move it - the thread settles at its end with the target never - // shown. That path is indistinguishable from a warm in-thread jump by anything visible here (both - // arrive as "dataset with a resolvable initialScrollIndex"), so this call has to be the one - // authority that always fires. + // 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 @@ -255,6 +260,8 @@ const useScrollToCentered = (p: { } 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]) } diff --git a/shared/tests/e2e/electron/flows/chat-link-jump.test.ts b/shared/tests/e2e/electron/flows/chat-link-jump.test.ts index 3060a9483e1c..5b9294e87d08 100644 --- a/shared/tests/e2e/electron/flows/chat-link-jump.test.ts +++ b/shared/tests/e2e/electron/flows/chat-link-jump.test.ts @@ -25,9 +25,10 @@ import * as T from '@/tests/e2e/shared/test-ids' // A row has to be visible by more than a hairline to count as landed on, matching chat-search-hit // and the iOS flow. -// No retries. The second attempt runs against a thread this test has already fetched once, so a -// pass on retry would not be a pass on the path the first attempt took - the same reason -// chat-thread-bottom turns them off. +// 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 @@ -110,7 +111,26 @@ const openConversationNamed = async (page: Page, rows: Locator, name: string): P // What "Copy a link to this message" puts on the clipboard: constants/deeplinks.tsx's // linkFromConvAndMessage(conv, messageID) - conv is the conversation's display label (what // rowName also reads), messageID is what the app renders as data-ordinal once a message has sent. -const LINK_PATTERN = /^keybase:\/\/chat\/(.+)\/(\d+)$/ +// 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 @@ -118,13 +138,17 @@ const LINK_PATTERN = /^keybase:\/\/chat\/(.+)\/(\d+)$/ // 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 text = (await candidates.nth(i).innerText()).trim() - const match = LINK_PATTERN.exec(text) - if (match) return {convName: match[1]!, ordinal: match[2]!, text} + 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) @@ -174,6 +198,7 @@ test('opens a conversation onto the message a link points at', async ({page}) => // ---- 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 { From 061e76e5d10e227e3f65637986328a1324973524 Mon Sep 17 00:00:00 2001 From: chrisnojima Date: Wed, 19 Aug 2026 11:08:42 -0400 Subject: [PATCH 5/5] fix(chat): keep collapsable={false} on the testID'd wrappers Fabric view-flattening drops a Box2 that carries nothing but a testID, which takes the testID with it and makes the row unfindable to the native e2e drivers. Desktop ignores the prop, so carrying it costs nothing there. --- shared/chat/conversation/messages/special-top-message.tsx | 8 +++++++- shared/chat/conversation/messages/wrapper/wrapper.tsx | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/shared/chat/conversation/messages/special-top-message.tsx b/shared/chat/conversation/messages/special-top-message.tsx index e6a1a2f17687..c55e71e379d5 100644 --- a/shared/chat/conversation/messages/special-top-message.tsx +++ b/shared/chat/conversation/messages/special-top-message.tsx @@ -154,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 421769c5471e..2a10f1064503 100644 --- a/shared/chat/conversation/messages/wrapper/wrapper.tsx +++ b/shared/chat/conversation/messages/wrapper/wrapper.tsx @@ -1023,6 +1023,7 @@ export function WrapperMessage(p: WrapperMessageProps) { direction="vertical" relative={true} fullWidth={true} + collapsable={false} testID={showCenteredHighlight ? TestIDs.CHAT_SEARCH_HIT : undefined} >