authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-07 10:10:00-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-07 10:10:00-07:00
log8d7f78121660eb8599d4c5ff40cb4394159abfab
tree8ba5afb19748b089e5b8249b7264053e6a09566f
parent357b8d5217cd62826767b6ce6d7c47915f2777d2
signature Signed by PGP key B5690EEEBB952194

fix(mobile): wait for native thread scroll before reveal (#10486)


3 files changed, 357 insertions(+), 125 deletions(-)

patches/@legendapp__list@3.3.5.patch+138-120
...@@ -241,7 +241,7 @@ index ce1fe00001c9e5aee6c6ea8bb2d4757d4586d002..3ccf6f16067152dfcb0c143371e2ec6a...@@ -241,7 +241,7 @@ index ce1fe00001c9e5aee6c6ea8bb2d4757d4586d002..3ccf6f16067152dfcb0c143371e2ec6a
241 * Number of columns to render items in.241 * Number of columns to render items in.
242 * @default 1242 * @default 1
243diff --git a/react-native.js b/react-native.js243diff --git a/react-native.js b/react-native.js
244index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac0047a8ead3 100644244index b3c5a306b293f797a8b338adfca3060c0f6db22b..5ac6bbe8fe40bf252fefa6b885c2196d0677d75f 100644
245--- a/react-native.js245--- a/react-native.js
246+++ b/react-native.js246+++ b/react-native.js
247@@ -717,6 +717,15 @@ function hasActiveInitialScroll(state) {247@@ -717,6 +717,15 @@ function hasActiveInitialScroll(state) {
...@@ -269,16 +269,25 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -269,16 +269,25 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
269 set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength);269 set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength);
270 set$(270 set$(
271 ctx,271 ctx,
272@@ -954,7 +963,7 @@ function setInitialRenderState(ctx, {272@@ -954,7 +963,16 @@ function setInitialRenderState(ctx, {
273 if (didInitialScroll) {273 if (didInitialScroll) {
274 state.didFinishInitialScroll = true;274 state.didFinishInitialScroll = true;
275 }275 }
276- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll);276- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll);
277+ // Native contentOffset can seed an end target without dispatchInitialScroll.
278+ // Both overflow completion paths must wait for the native end landing.
279+ // Underflow needs no scroll: preserve UIKit's untouched resting position.
280+ if (state.didContainersLayout && state.didFinishInitialScroll && state.initialScroll && state.initialScroll.viewPosition === 1 && state.initialScroll.index === state.props.data.length - 1 && state.props.data.length > 0) {
281+ const insetStartAdjustment = getContentInsetStartAdjustment(ctx);
282+ if (insetStartAdjustment > 0 && getContentSize(ctx) > state.scrollLength - insetStartAdjustment + 1) {
283+ startInsetEndSettleWatchdog(ctx);
284+ }
285+ }
277+ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll && !state.insetEndRevealHold);286+ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll && !state.insetEndRevealHold);
278 if (isReadyToRender && !peek$(ctx, "readyToRender")) {287 if (isReadyToRender && !peek$(ctx, "readyToRender")) {
279 set$(ctx, "readyToRender", true);288 set$(ctx, "readyToRender", true);
280 setAdaptiveRender(ctx, "normal", "ready");289 setAdaptiveRender(ctx, "normal", "ready");
281@@ -1090,7 +1099,7 @@ function getRawContentLength(ctx) {290@@ -1090,7 +1108,7 @@ function getRawContentLength(ctx) {
282 function getAlignItemsAtEndPadding(ctx) {291 function getAlignItemsAtEndPadding(ctx) {
283 const { state } = ctx;292 const { state } = ctx;
284 const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0;293 const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0;
...@@ -287,7 +296,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -287,7 +296,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
287 }296 }
288 function updateContentMetricsState(ctx) {297 function updateContentMetricsState(ctx) {
289 const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0;298 const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0;
290@@ -1115,6 +1124,10 @@ function addTotalSize(ctx, key, add, notifyTotalSize = true) {299@@ -1115,6 +1133,10 @@ function addTotalSize(ctx, key, add, notifyTotalSize = true) {
291 totalSize += add;300 totalSize += add;
292 }301 }
293 if (prevTotalSize !== totalSize) {302 if (prevTotalSize !== totalSize) {
...@@ -298,7 +307,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -298,7 +307,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
298 if (!IsNewArchitecture && state.initialScroll && totalSize < prevTotalSize) {307 if (!IsNewArchitecture && state.initialScroll && totalSize < prevTotalSize) {
299 state.pendingTotalSize = totalSize;308 state.pendingTotalSize = totalSize;
300 } else {309 } else {
301@@ -1304,18 +1317,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {310@@ -1304,18 +1326,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
302 }311 }
303 312
304 // src/core/clampScrollOffset.ts313 // src/core/clampScrollOffset.ts
...@@ -324,7 +333,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -324,7 +333,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
324 return clampedOffset;333 return clampedOffset;
325 }334 }
326 335
327@@ -1451,10 +1469,10 @@ function checkFinishedScrollFrame(ctx) {336@@ -1451,10 +1478,10 @@ function checkFinishedScrollFrame(ctx) {
328 finishScrollTo(ctx);337 finishScrollTo(ctx);
329 }338 }
330 }339 }
...@@ -337,7 +346,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -337,7 +346,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
337 x: ctx.state.props.horizontal ? offset : 0,346 x: ctx.state.props.horizontal ? offset : 0,
338 y: ctx.state.props.horizontal ? 0 : offset347 y: ctx.state.props.horizontal ? 0 : offset
339 });348 });
340@@ -1503,7 +1521,10 @@ function checkFinishedScrollFallback(ctx) {349@@ -1503,7 +1530,10 @@ function checkFinishedScrollFallback(ctx) {
341 );350 );
342 scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS);351 scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS);
343 } else if (shouldRetryUnalignedEndScroll) {352 } else if (shouldRetryUnalignedEndScroll) {
...@@ -349,7 +358,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -349,7 +358,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
349 scheduleFallbackCheck(100);358 scheduleFallbackCheck(100);
350 } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) {359 } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) {
351 finishScrollTo(ctx);360 finishScrollTo(ctx);
352@@ -1560,15 +1581,28 @@ function doMaintainScrollAtEnd(ctx) {361@@ -1560,15 +1590,28 @@ function doMaintainScrollAtEnd(ctx) {
353 } = state;362 } = state;
354 const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold");363 const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold");
355 const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout);364 const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout);
...@@ -379,7 +388,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -379,7 +388,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
379 }388 }
380 if (!state.maintainingScrollAtEnd) {389 if (!state.maintainingScrollAtEnd) {
381 const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant";390 const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant";
382@@ -1591,9 +1625,18 @@ function doMaintainScrollAtEnd(ctx) {391@@ -1591,9 +1634,18 @@ function doMaintainScrollAtEnd(ctx) {
383 y: 0392 y: 0
384 });393 });
385 } else {394 } else {
...@@ -401,7 +410,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -401,7 +410,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
401 }410 }
402 setTimeout(411 setTimeout(
403 () => {412 () => {
404@@ -1624,6 +1667,10 @@ function doMaintainScrollAtEnd(ctx) {413@@ -1624,6 +1676,10 @@ function doMaintainScrollAtEnd(ctx) {
405 function requestAdjust(ctx, positionDiff, dataChanged) {414 function requestAdjust(ctx, positionDiff, dataChanged) {
406 const state = ctx.state;415 const state = ctx.state;
407 if (Math.abs(positionDiff) > 0.1) {416 if (Math.abs(positionDiff) > 0.1) {
...@@ -412,7 +421,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -412,7 +421,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
412 const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff;421 const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff;
413 const doit = () => {422 const doit = () => {
414 if (needsScrollWorkaround) {423 if (needsScrollWorkaround) {
415@@ -1728,7 +1775,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) {424@@ -1728,7 +1784,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) {
416 if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) {425 if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) {
417 return 0;426 return 0;
418 }427 }
...@@ -423,7 +432,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -423,7 +432,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
423 const clampDelta = maxScroll - state.scroll;432 const clampDelta = maxScroll - state.scroll;
424 if (unresolvedAmount < 0) {433 if (unresolvedAmount < 0) {
425 return Math.max(unresolvedAmount, Math.min(0, clampDelta));434 return Math.max(unresolvedAmount, Math.min(0, clampDelta));
426@@ -1790,7 +1839,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) {435@@ -1790,7 +1848,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) {
427 settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta);436 settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta);
428 return true;437 return true;
429 }438 }
...@@ -432,7 +441,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -432,7 +441,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
432 const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll);441 const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll);
433 const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON;442 const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON;
434 if (isAtExpectedNativeClamp) {443 if (isAtExpectedNativeClamp) {
435@@ -1923,7 +1972,7 @@ function prepareMVCP(ctx, dataChanged) {444@@ -1923,7 +1981,7 @@ function prepareMVCP(ctx, dataChanged) {
436 if (diff > 0) {445 if (diff > 0) {
437 diff = Math.max(0, totalSize - state.scroll - state.scrollLength);446 diff = Math.max(0, totalSize - state.scroll - state.scrollLength);
438 } else {447 } else {
...@@ -441,7 +450,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -441,7 +450,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
441 state.scroll = maxScroll;450 state.scroll = maxScroll;
442 state.scrollPending = maxScroll;451 state.scrollPending = maxScroll;
443 diff = 0;452 diff = 0;
444@@ -2320,8 +2369,121 @@ function scrollToIndex(ctx, {453@@ -2320,8 +2378,121 @@ function scrollToIndex(ctx, {
445 }454 }
446 455
447 // src/core/initialScroll.ts456 // src/core/initialScroll.ts
...@@ -451,11 +460,11 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -451,11 +460,11 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
451+var INSET_END_REVEAL_MAX_HOLD_FRAMES = 40;460+var INSET_END_REVEAL_MAX_HOLD_FRAMES = 40;
452+function startInsetEndSettleWatchdog(ctx) {461+function startInsetEndSettleWatchdog(ctx) {
453+ const state = ctx.state;462+ const state = ctx.state;
454+ if (state.insetEndSettleWatchdogActive) {463+ if (state.insetEndSettleWatchdogStarted || state.didLoad) {
455+ return;464+ return;
456+ }465+ }
466+ state.insetEndSettleWatchdogStarted = true;
457+ state.insetEndSettleWatchdogActive = true;467+ state.insetEndSettleWatchdogActive = true;
458+ state.didUserDrag = false;
459+ // Hold the readyToRender opacity gate until the end landing is stable, so468+ // Hold the readyToRender opacity gate until the end landing is stable, so
460+ // the estimated-to-measured settle chase happens before first VISIBLE469+ // the estimated-to-measured settle chase happens before first VISIBLE
461+ // paint instead of in front of the user. Capped so slow measurement can470+ // paint instead of in front of the user. Capped so slow measurement can
...@@ -464,6 +473,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -464,6 +473,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
464+ let frames = 0;473+ let frames = 0;
465+ let settledFrames = 0;474+ let settledFrames = 0;
466+ let revealStableFrames = 0;475+ let revealStableFrames = 0;
476+ let previousEndOffset;
467+ const releaseRevealHold = () => {477+ const releaseRevealHold = () => {
468+ if (state.insetEndRevealHold) {478+ if (state.insetEndRevealHold) {
469+ state.insetEndRevealHold = false;479+ state.insetEndRevealHold = false;
...@@ -503,33 +513,32 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -503,33 +513,32 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
503+ const insetStartAdjustment = getContentInsetStartAdjustment(ctx);513+ const insetStartAdjustment = getContentInsetStartAdjustment(ctx);
504+ const contentSize = getContentSize(ctx);514+ const contentSize = getContentSize(ctx);
505+ const scrollLength = state.scrollLength;515+ const scrollLength = state.scrollLength;
506+ if (insetStartAdjustment > 0 && scrollLength > 0 && Number.isFinite(contentSize) && contentSize > scrollLength && !state.scrollingTo && !state.maintainingScrollAtEnd) {516+ const endOffset = Math.max(-insetStartAdjustment, contentSize - scrollLength);
507+ const endOffset = Math.max(-insetStartAdjustment, contentSize - scrollLength);517+ // state.scroll is optimistic: non-animated scrollTo writes it before UIKit
508+ const distance = endOffset - state.scroll;518+ // moves. Only a native scroll event proves that the requested offset landed.
509+ // Estimated row sizes converging to measured ones can strand the initial519+ const nativeOffset = state.lastNativeScroll;
510+ // end landing when the library's own end-anchor bookkeeping gives up.520+ const hasNativeOffset = typeof nativeOffset === "number" && Number.isFinite(nativeOffset);
511+ // While still near the end (never fighting a user who scrolled away),521+ const distance = hasNativeOffset ? endOffset - nativeOffset : Infinity;
512+ // re-pin to the current true end until sizes stop changing.522+ const isIdle = !state.scrollingTo && !state.maintainingScrollAtEnd;
513+ if (Math.abs(distance) > 2 && Math.abs(distance) <= scrollLength * 0.5) {523+ const hasViewport = insetStartAdjustment > 0 && scrollLength > 0 && Number.isFinite(contentSize);
514+ settledFrames = 0;524+ const isStable = hasViewport && isIdle && Math.abs(distance) <= 2 && previousEndOffset !== void 0 && Math.abs(endOffset - previousEndOffset) <= 1;
515+ revealStableFrames = 0;525+ previousEndOffset = endOffset;
526+ if (isStable) {
527+ settledFrames++;
528+ revealStableFrames++;
529+ if (revealStableFrames >= INSET_END_REVEAL_STABLE_FRAMES) {
530+ onRevealStability();
531+ }
532+ } else {
533+ settledFrames = 0;
534+ revealStableFrames = 0;
535+ // Keep the existing near-end correction limit. In-flight scrolls and
536+ // unknown native offsets wait; neither counts as a settled frame.
537+ if (hasViewport && isIdle && Math.abs(distance) > 2 && Math.abs(distance) <= scrollLength * 0.5) {
516+ const scroller = state.refScroller.current;538+ const scroller = state.refScroller.current;
517+ if (scroller) {539+ if (scroller) {
518+ scroller.scrollTo({ animated: false, x: 0, y: endOffset });540+ scroller.scrollTo({ animated: false, x: 0, y: endOffset });
519+ }541+ }
520+ } else {
521+ settledFrames++;
522+ revealStableFrames++;
523+ if (revealStableFrames >= INSET_END_REVEAL_STABLE_FRAMES) {
524+ onRevealStability();
525+ }
526+ }
527+ } else {
528+ // Conditions that make re-pinning unnecessary (underflow, in-flight
529+ // programmatic scroll) count toward stability for the reveal.
530+ revealStableFrames++;
531+ if (revealStableFrames >= INSET_END_REVEAL_STABLE_FRAMES) {
532+ onRevealStability();
533+ }542+ }
534+ }543+ }
535+ requestAnimationFrame(tick);544+ requestAnimationFrame(tick);
...@@ -563,7 +572,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -563,7 +572,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
563 const requestedIndex = target.index;572 const requestedIndex = target.index;
564 const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0;573 const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0;
565 const itemSize = getItemSizeAtIndex(ctx, index);574 const itemSize = getItemSizeAtIndex(ctx, index);
566@@ -2747,7 +2909,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) {575@@ -2747,7 +2918,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) {
567 return;576 return;
568 }577 }
569 if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) {578 if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) {
...@@ -574,7 +583,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -574,7 +583,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
574 if (!shouldKeepEndTargetAlive) {583 if (!shouldKeepEndTargetAlive) {
575 if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) {584 if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) {
576 clearPendingInitialScrollFooterLayout(ctx, {585 clearPendingInitialScrollFooterLayout(ctx, {
577@@ -4672,7 +4836,8 @@ function maybeUpdateAnchoredEndSpace(ctx) {586@@ -4672,7 +4845,8 @@ function maybeUpdateAnchoredEndSpace(ctx) {
578 contentBelowAnchor = Math.max(0, contentBelowAnchor - ctx.scrollAxisGap);587 contentBelowAnchor = Math.max(0, contentBelowAnchor - ctx.scrollAxisGap);
579 contentBelowAnchor += (ctx.values.get("footerSize") || 0) + getStylePaddingEnd(state.props);588 contentBelowAnchor += (ctx.values.get("footerSize") || 0) + getStylePaddingEnd(state.props);
580 isReady = !hasUnknownTailSize;589 isReady = !hasUnknownTailSize;
...@@ -584,7 +593,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -584,7 +593,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
584 } else if (anchorIndex >= 0) {593 } else if (anchorIndex >= 0) {
585 isReady = false;594 isReady = false;
586 }595 }
587@@ -4692,6 +4857,12 @@ function maybeUpdateAnchoredEndSpace(ctx) {596@@ -4692,6 +4866,12 @@ function maybeUpdateAnchoredEndSpace(ctx) {
588 updateScroll(ctx, state.scroll, true, { markHasScrolled: false });597 updateScroll(ctx, state.scroll, true, { markHasScrolled: false });
589 }598 }
590 (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize });599 (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize });
...@@ -597,7 +606,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -597,7 +606,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
597 }606 }
598 return nextSize;607 return nextSize;
599 }608 }
600@@ -5715,6 +5886,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({609@@ -5715,6 +5895,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
601 horizontal610 horizontal
602 }) {611 }) {
603 const ctx = useStateContext();612 const ctx = useStateContext();
...@@ -605,7 +614,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -605,7 +614,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
605 const columnWrapperStyle = ctx.columnWrapperStyle;614 const columnWrapperStyle = ctx.columnWrapperStyle;
606 const animSize = useValue$("totalSize");615 const animSize = useValue$("totalSize");
607 const [readyToRender, numColumns, otherAxisSize = 0] = useArr$(["readyToRender", "numColumns", "otherAxisSize"]);616 const [readyToRender, numColumns, otherAxisSize = 0] = useArr$(["readyToRender", "numColumns", "otherAxisSize"]);
608@@ -5725,6 +5897,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({617@@ -5725,6 +5906,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
609 opacity: isVisible ? 1 : 0,618 opacity: isVisible ? 1 : 0,
610 width: animSize619 width: animSize
611 } : { height: animSize, minWidth: otherAxisSize, opacity: isVisible ? 1 : 0 };620 } : { height: animSize, minWidth: otherAxisSize, opacity: isVisible ? 1 : 0 };
...@@ -619,7 +628,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -619,7 +628,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
619 if (columnWrapperStyle) {628 if (columnWrapperStyle) {
620 const { columnGap, rowGap, gap } = columnWrapperStyle;629 const { columnGap, rowGap, gap } = columnWrapperStyle;
621 const gapX = columnGap || gap || 0;630 const gapX = columnGap || gap || 0;
622@@ -5745,7 +5924,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({631@@ -5745,7 +5933,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
623 }632 }
624 }633 }
625 }634 }
...@@ -629,7 +638,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -629,7 +638,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
629 });638 });
630 var Containers = typedMemo(function Containers2({639 var Containers = typedMemo(function Containers2({
631 freshDataTransitionEpoch,640 freshDataTransitionEpoch,
632@@ -5896,7 +6076,12 @@ var StyleSheet = ReactNative.StyleSheet;641@@ -5896,7 +6085,12 @@ var StyleSheet = ReactNative.StyleSheet;
633 642
634 // src/components/ListComponent.tsx643 // src/components/ListComponent.tsx
635 var AlignItemsAtEndSpacer = typedMemo(function AlignItemsAtEndSpacer2({ horizontal }) {644 var AlignItemsAtEndSpacer = typedMemo(function AlignItemsAtEndSpacer2({ horizontal }) {
...@@ -642,7 +651,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -642,7 +651,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
642 if (alignItemsAtEndPadding <= 0) {651 if (alignItemsAtEndPadding <= 0) {
643 return null;652 return null;
644 }653 }
645@@ -5929,8 +6114,12 @@ var ListComponent = typedMemo(function ListComponent2({654@@ -5929,8 +6123,12 @@ var ListComponent = typedMemo(function ListComponent2({
646 refScrollView,655 refScrollView,
647 renderScrollComponent,656 renderScrollComponent,
648 onLayoutFooter,657 onLayoutFooter,
...@@ -655,7 +664,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -655,7 +664,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
655 scrollAdjustHandler,664 scrollAdjustHandler,
656 snapToIndices,665 snapToIndices,
657 stickyHeaderConfig,666 stickyHeaderConfig,
658@@ -6001,7 +6190,17 @@ var ListComponent = typedMemo(function ListComponent2({667@@ -6001,7 +6199,17 @@ var ListComponent = typedMemo(function ListComponent2({
659 SnapOrScroll,668 SnapOrScroll,
660 {669 {
661 ...rest,670 ...rest,
...@@ -674,7 +683,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -674,7 +683,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
674 contentContainerStyle: [683 contentContainerStyle: [
675 horizontal ? { height: "100%" } : {},684 horizontal ? { height: "100%" } : {},
676 contentContainerStyle,685 contentContainerStyle,
677@@ -6010,7 +6209,10 @@ var ListComponent = typedMemo(function ListComponent2({686@@ -6010,7 +6218,10 @@ var ListComponent = typedMemo(function ListComponent2({
678 ],687 ],
679 contentOffset: initialContentOffset !== void 0 ? horizontal ? { x: initialContentOffset, y: 0 } : { x: 0, y: initialContentOffset } : void 0,688 contentOffset: initialContentOffset !== void 0 ? horizontal ? { x: initialContentOffset, y: 0 } : { x: 0, y: initialContentOffset } : void 0,
680 horizontal,689 horizontal,
...@@ -686,7 +695,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -686,7 +695,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
686 onLayout,695 onLayout,
687 onScroll: onScroll2,696 onScroll: onScroll2,
688 ref: refScrollView,697 ref: refScrollView,
689@@ -6751,7 +6953,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) {698@@ -6751,7 +6962,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) {
690 endBuffered: state.endBuffered,699 endBuffered: state.endBuffered,
691 getAverageItemSizes: () => getAverageItemSizes(state),700 getAverageItemSizes: () => getAverageItemSizes(state),
692 indexByKey: (key) => state.indexByKey.get(key),701 indexByKey: (key) => state.indexByKey.get(key),
...@@ -695,7 +704,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -695,7 +704,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
695 isAtStart: peek$(ctx, "isAtStart"),704 isAtStart: peek$(ctx, "isAtStart"),
696 isEndReached: state.isEndReached,705 isEndReached: state.isEndReached,
697 isNearEnd: peek$(ctx, "isNearEnd"),706 isNearEnd: peek$(ctx, "isNearEnd"),
698@@ -7075,6 +7277,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded707@@ -7075,6 +7286,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
699 dataVersion,708 dataVersion,
700 drawDistance = 250,709 drawDistance = 250,
701 contentInsetEndAdjustment,710 contentInsetEndAdjustment,
...@@ -703,7 +712,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -703,7 +712,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
703 estimatedItemSize = 100,712 estimatedItemSize = 100,
704 estimatedListSize,713 estimatedListSize,
705 extraData,714 extraData,
706@@ -7132,10 +7335,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded715@@ -7132,10 +7344,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
707 const animatedPropsInternal = props.animatedPropsInternal;716 const animatedPropsInternal = props.animatedPropsInternal;
708 const anchoredEndSpaceOwner = (_a3 = props.anchoredEndSpaceOwnerInternal) != null ? _a3 : "list";717 const anchoredEndSpaceOwner = (_a3 = props.anchoredEndSpaceOwnerInternal) != null ? _a3 : "list";
709 const positionComponentInternal = props.positionComponentInternal;718 const positionComponentInternal = props.positionComponentInternal;
...@@ -716,7 +725,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -716,7 +725,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
716 stickyPositionComponentInternal: _stickyPositionComponentInternal,725 stickyPositionComponentInternal: _stickyPositionComponentInternal,
717 ...restProps726 ...restProps
718 } = rest;727 } = rest;
719@@ -7200,7 +7405,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded728@@ -7200,7 +7414,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
720 const combinedRef = useCombinedRef(refScroller, refScrollView);729 const combinedRef = useCombinedRef(refScroller, refScrollView);
721 const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString());730 const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString());
722 const stickyHeaderIndices = stickyHeaderIndicesProp;731 const stickyHeaderIndices = stickyHeaderIndicesProp;
...@@ -725,7 +734,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -725,7 +734,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
725 const previousContentInsetEndAdjustmentRef = React2.useRef(contentInsetEndAdjustmentResolved);734 const previousContentInsetEndAdjustmentRef = React2.useRef(contentInsetEndAdjustmentResolved);
726 const alwaysRenderIndices = React2.useMemo(() => {735 const alwaysRenderIndices = React2.useMemo(() => {
727 const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex);736 const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex);
728@@ -7341,6 +7546,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded737@@ -7341,6 +7555,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
729 contentContainerAlignItems: contentContainerStyle.alignItems,738 contentContainerAlignItems: contentContainerStyle.alignItems,
730 contentInset,739 contentInset,
731 contentInsetEndAdjustment: contentInsetEndAdjustmentResolved,740 contentInsetEndAdjustment: contentInsetEndAdjustmentResolved,
...@@ -733,7 +742,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -733,7 +742,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
733 data: dataProp,742 data: dataProp,
734 dataKey,743 dataKey,
735 dataVersion,744 dataVersion,
736@@ -7372,6 +7578,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded745@@ -7372,6 +7587,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
737 renderItem,746 renderItem,
738 rtl,747 rtl,
739 snapToIndices,748 snapToIndices,
...@@ -741,7 +750,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -741,7 +750,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
741 stickyHeaderIndicesArr: stickyHeaderIndices != null ? stickyHeaderIndices : [],750 stickyHeaderIndicesArr: stickyHeaderIndices != null ? stickyHeaderIndices : [],
742 stickyHeaderIndicesSet: React2.useMemo(() => new Set(stickyHeaderIndices != null ? stickyHeaderIndices : []), [stickyHeaderIndices == null ? void 0 : stickyHeaderIndices.join(",")]),751 stickyHeaderIndicesSet: React2.useMemo(() => new Set(stickyHeaderIndices != null ? stickyHeaderIndices : []), [stickyHeaderIndices == null ? void 0 : stickyHeaderIndices.join(",")]),
743 stickyPositionComponentInternal,752 stickyPositionComponentInternal,
744@@ -7423,6 +7630,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded753@@ -7423,6 +7639,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
745 return void 0;754 return void 0;
746 }755 }
747 const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll);756 const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll);
...@@ -755,7 +764,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -755,7 +764,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
755 return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset;764 return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset;
756 }, [usesBootstrapInitialScroll]);765 }, [usesBootstrapInitialScroll]);
757 React2.useLayoutEffect(() => {766 React2.useLayoutEffect(() => {
758@@ -7547,6 +7761,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded767@@ -7547,6 +7770,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
759 [768 [
760 dataKey,769 dataKey,
761 dataVersion,770 dataVersion,
...@@ -763,7 +772,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -763,7 +772,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
763 memoizedLastItemKeys.join(","),772 memoizedLastItemKeys.join(","),
764 numColumnsProp,773 numColumnsProp,
765 nextScrollAxisGap,774 nextScrollAxisGap,
766@@ -7643,6 +7858,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded775@@ -7643,6 +7867,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
767 () => ({776 () => ({
768 getRenderedItem: (key) => getRenderedItem(ctx, key),777 getRenderedItem: (key) => getRenderedItem(ctx, key),
769 onMomentumScrollEnd: (event) => {778 onMomentumScrollEnd: (event) => {
...@@ -771,7 +780,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -771,7 +780,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
771 checkFinishedScrollFallback(ctx);780 checkFinishedScrollFallback(ctx);
772 if (state.props.onMomentumScrollEnd) {781 if (state.props.onMomentumScrollEnd) {
773 state.props.onMomentumScrollEnd(event);782 state.props.onMomentumScrollEnd(event);
774@@ -7651,6 +7867,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded783@@ -7651,6 +7876,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
775 onScroll: (event) => onScroll(ctx, event),784 onScroll: (event) => onScroll(ctx, event),
776 onScrollBeginDrag: (event) => {785 onScrollBeginDrag: (event) => {
777 var _a4, _b2;786 var _a4, _b2;
...@@ -780,7 +789,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -780,7 +789,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
780 prepareReachedEdgeForNextUserScroll(ctx);789 prepareReachedEdgeForNextUserScroll(ctx);
781 (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event);790 (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event);
782 },791 },
783@@ -7676,11 +7894,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded792@@ -7676,11 +7903,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
784 ListFooterComponent,793 ListFooterComponent,
785 ListFooterComponentStyle,794 ListFooterComponentStyle,
786 ListHeaderComponent,795 ListHeaderComponent,
...@@ -800,7 +809,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00...@@ -800,7 +809,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..ddca4bcd8a5dc5863cb65e03b6cfac00
800 recycleItems,809 recycleItems,
801 refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2__namespace.cloneElement(refreshControlElement, {810 refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2__namespace.cloneElement(refreshControlElement, {
802diff --git a/react-native.mjs b/react-native.mjs811diff --git a/react-native.mjs b/react-native.mjs
803index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97440bc623 100644812index 40e87cda8c9bc79a889e5542f29af429a24b24d4..93aac741d2cf77ce35996362439108176f19da7a 100644
804--- a/react-native.mjs813--- a/react-native.mjs
805+++ b/react-native.mjs814+++ b/react-native.mjs
806@@ -696,6 +696,15 @@ function hasActiveInitialScroll(state) {815@@ -696,6 +696,15 @@ function hasActiveInitialScroll(state) {
...@@ -828,16 +837,25 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -828,16 +837,25 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
828 set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength);837 set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength);
829 set$(838 set$(
830 ctx,839 ctx,
831@@ -933,7 +942,7 @@ function setInitialRenderState(ctx, {840@@ -933,7 +942,16 @@ function setInitialRenderState(ctx, {
832 if (didInitialScroll) {841 if (didInitialScroll) {
833 state.didFinishInitialScroll = true;842 state.didFinishInitialScroll = true;
834 }843 }
835- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll);844- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll);
845+ // Native contentOffset can seed an end target without dispatchInitialScroll.
846+ // Both overflow completion paths must wait for the native end landing.
847+ // Underflow needs no scroll: preserve UIKit's untouched resting position.
848+ if (state.didContainersLayout && state.didFinishInitialScroll && state.initialScroll && state.initialScroll.viewPosition === 1 && state.initialScroll.index === state.props.data.length - 1 && state.props.data.length > 0) {
849+ const insetStartAdjustment = getContentInsetStartAdjustment(ctx);
850+ if (insetStartAdjustment > 0 && getContentSize(ctx) > state.scrollLength - insetStartAdjustment + 1) {
851+ startInsetEndSettleWatchdog(ctx);
852+ }
853+ }
836+ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll && !state.insetEndRevealHold);854+ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll && !state.insetEndRevealHold);
837 if (isReadyToRender && !peek$(ctx, "readyToRender")) {855 if (isReadyToRender && !peek$(ctx, "readyToRender")) {
838 set$(ctx, "readyToRender", true);856 set$(ctx, "readyToRender", true);
839 setAdaptiveRender(ctx, "normal", "ready");857 setAdaptiveRender(ctx, "normal", "ready");
840@@ -1069,7 +1078,7 @@ function getRawContentLength(ctx) {858@@ -1069,7 +1087,7 @@ function getRawContentLength(ctx) {
841 function getAlignItemsAtEndPadding(ctx) {859 function getAlignItemsAtEndPadding(ctx) {
842 const { state } = ctx;860 const { state } = ctx;
843 const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0;861 const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0;
...@@ -846,7 +864,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -846,7 +864,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
846 }864 }
847 function updateContentMetricsState(ctx) {865 function updateContentMetricsState(ctx) {
848 const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0;866 const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0;
849@@ -1094,6 +1103,10 @@ function addTotalSize(ctx, key, add, notifyTotalSize = true) {867@@ -1094,6 +1112,10 @@ function addTotalSize(ctx, key, add, notifyTotalSize = true) {
850 totalSize += add;868 totalSize += add;
851 }869 }
852 if (prevTotalSize !== totalSize) {870 if (prevTotalSize !== totalSize) {
...@@ -857,7 +875,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -857,7 +875,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
857 if (!IsNewArchitecture && state.initialScroll && totalSize < prevTotalSize) {875 if (!IsNewArchitecture && state.initialScroll && totalSize < prevTotalSize) {
858 state.pendingTotalSize = totalSize;876 state.pendingTotalSize = totalSize;
859 } else {877 } else {
860@@ -1283,18 +1296,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {878@@ -1283,18 +1305,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
861 }879 }
862 880
863 // src/core/clampScrollOffset.ts881 // src/core/clampScrollOffset.ts
...@@ -883,7 +901,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -883,7 +901,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
883 return clampedOffset;901 return clampedOffset;
884 }902 }
885 903
886@@ -1430,10 +1448,10 @@ function checkFinishedScrollFrame(ctx) {904@@ -1430,10 +1457,10 @@ function checkFinishedScrollFrame(ctx) {
887 finishScrollTo(ctx);905 finishScrollTo(ctx);
888 }906 }
889 }907 }
...@@ -896,7 +914,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -896,7 +914,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
896 x: ctx.state.props.horizontal ? offset : 0,914 x: ctx.state.props.horizontal ? offset : 0,
897 y: ctx.state.props.horizontal ? 0 : offset915 y: ctx.state.props.horizontal ? 0 : offset
898 });916 });
899@@ -1482,7 +1500,10 @@ function checkFinishedScrollFallback(ctx) {917@@ -1482,7 +1509,10 @@ function checkFinishedScrollFallback(ctx) {
900 );918 );
901 scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS);919 scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS);
902 } else if (shouldRetryUnalignedEndScroll) {920 } else if (shouldRetryUnalignedEndScroll) {
...@@ -908,7 +926,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -908,7 +926,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
908 scheduleFallbackCheck(100);926 scheduleFallbackCheck(100);
909 } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) {927 } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) {
910 finishScrollTo(ctx);928 finishScrollTo(ctx);
911@@ -1539,15 +1560,28 @@ function doMaintainScrollAtEnd(ctx) {929@@ -1539,15 +1569,28 @@ function doMaintainScrollAtEnd(ctx) {
912 } = state;930 } = state;
913 const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold");931 const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold");
914 const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout);932 const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout);
...@@ -938,7 +956,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -938,7 +956,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
938 }956 }
939 if (!state.maintainingScrollAtEnd) {957 if (!state.maintainingScrollAtEnd) {
940 const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant";958 const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant";
941@@ -1570,9 +1604,18 @@ function doMaintainScrollAtEnd(ctx) {959@@ -1570,9 +1613,18 @@ function doMaintainScrollAtEnd(ctx) {
942 y: 0960 y: 0
943 });961 });
944 } else {962 } else {
...@@ -960,7 +978,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -960,7 +978,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
960 }978 }
961 setTimeout(979 setTimeout(
962 () => {980 () => {
963@@ -1603,6 +1646,10 @@ function doMaintainScrollAtEnd(ctx) {981@@ -1603,6 +1655,10 @@ function doMaintainScrollAtEnd(ctx) {
964 function requestAdjust(ctx, positionDiff, dataChanged) {982 function requestAdjust(ctx, positionDiff, dataChanged) {
965 const state = ctx.state;983 const state = ctx.state;
966 if (Math.abs(positionDiff) > 0.1) {984 if (Math.abs(positionDiff) > 0.1) {
...@@ -971,7 +989,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -971,7 +989,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
971 const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff;989 const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff;
972 const doit = () => {990 const doit = () => {
973 if (needsScrollWorkaround) {991 if (needsScrollWorkaround) {
974@@ -1707,7 +1754,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) {992@@ -1707,7 +1763,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) {
975 if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) {993 if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) {
976 return 0;994 return 0;
977 }995 }
...@@ -982,7 +1000,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -982,7 +1000,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
982 const clampDelta = maxScroll - state.scroll;1000 const clampDelta = maxScroll - state.scroll;
983 if (unresolvedAmount < 0) {1001 if (unresolvedAmount < 0) {
984 return Math.max(unresolvedAmount, Math.min(0, clampDelta));1002 return Math.max(unresolvedAmount, Math.min(0, clampDelta));
985@@ -1769,7 +1818,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) {1003@@ -1769,7 +1827,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) {
986 settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta);1004 settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta);
987 return true;1005 return true;
988 }1006 }
...@@ -991,7 +1009,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -991,7 +1009,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
991 const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll);1009 const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll);
992 const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON;1010 const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON;
993 if (isAtExpectedNativeClamp) {1011 if (isAtExpectedNativeClamp) {
994@@ -1902,7 +1951,7 @@ function prepareMVCP(ctx, dataChanged) {1012@@ -1902,7 +1960,7 @@ function prepareMVCP(ctx, dataChanged) {
995 if (diff > 0) {1013 if (diff > 0) {
996 diff = Math.max(0, totalSize - state.scroll - state.scrollLength);1014 diff = Math.max(0, totalSize - state.scroll - state.scrollLength);
997 } else {1015 } else {
...@@ -1000,7 +1018,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1000,7 +1018,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1000 state.scroll = maxScroll;1018 state.scroll = maxScroll;
1001 state.scrollPending = maxScroll;1019 state.scrollPending = maxScroll;
1002 diff = 0;1020 diff = 0;
1003@@ -2299,8 +2348,121 @@ function scrollToIndex(ctx, {1021@@ -2299,8 +2357,121 @@ function scrollToIndex(ctx, {
1004 }1022 }
1005 1023
1006 // src/core/initialScroll.ts1024 // src/core/initialScroll.ts
...@@ -1010,11 +1028,11 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1010,11 +1028,11 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1010+var INSET_END_REVEAL_MAX_HOLD_FRAMES = 40;1028+var INSET_END_REVEAL_MAX_HOLD_FRAMES = 40;
1011+function startInsetEndSettleWatchdog(ctx) {1029+function startInsetEndSettleWatchdog(ctx) {
1012+ const state = ctx.state;1030+ const state = ctx.state;
1013+ if (state.insetEndSettleWatchdogActive) {1031+ if (state.insetEndSettleWatchdogStarted || state.didLoad) {
1014+ return;1032+ return;
1015+ }1033+ }
1034+ state.insetEndSettleWatchdogStarted = true;
1016+ state.insetEndSettleWatchdogActive = true;1035+ state.insetEndSettleWatchdogActive = true;
1017+ state.didUserDrag = false;
1018+ // Hold the readyToRender opacity gate until the end landing is stable, so1036+ // Hold the readyToRender opacity gate until the end landing is stable, so
1019+ // the estimated-to-measured settle chase happens before first VISIBLE1037+ // the estimated-to-measured settle chase happens before first VISIBLE
1020+ // paint instead of in front of the user. Capped so slow measurement can1038+ // paint instead of in front of the user. Capped so slow measurement can
...@@ -1023,6 +1041,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1023,6 +1041,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1023+ let frames = 0;1041+ let frames = 0;
1024+ let settledFrames = 0;1042+ let settledFrames = 0;
1025+ let revealStableFrames = 0;1043+ let revealStableFrames = 0;
1044+ let previousEndOffset;
1026+ const releaseRevealHold = () => {1045+ const releaseRevealHold = () => {
1027+ if (state.insetEndRevealHold) {1046+ if (state.insetEndRevealHold) {
1028+ state.insetEndRevealHold = false;1047+ state.insetEndRevealHold = false;
...@@ -1062,33 +1081,32 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1062,33 +1081,32 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1062+ const insetStartAdjustment = getContentInsetStartAdjustment(ctx);1081+ const insetStartAdjustment = getContentInsetStartAdjustment(ctx);
1063+ const contentSize = getContentSize(ctx);1082+ const contentSize = getContentSize(ctx);
1064+ const scrollLength = state.scrollLength;1083+ const scrollLength = state.scrollLength;
1065+ if (insetStartAdjustment > 0 && scrollLength > 0 && Number.isFinite(contentSize) && contentSize > scrollLength && !state.scrollingTo && !state.maintainingScrollAtEnd) {1084+ const endOffset = Math.max(-insetStartAdjustment, contentSize - scrollLength);
1066+ const endOffset = Math.max(-insetStartAdjustment, contentSize - scrollLength);1085+ // state.scroll is optimistic: non-animated scrollTo writes it before UIKit
1067+ const distance = endOffset - state.scroll;1086+ // moves. Only a native scroll event proves that the requested offset landed.
1068+ // Estimated row sizes converging to measured ones can strand the initial1087+ const nativeOffset = state.lastNativeScroll;
1069+ // end landing when the library's own end-anchor bookkeeping gives up.1088+ const hasNativeOffset = typeof nativeOffset === "number" && Number.isFinite(nativeOffset);
1070+ // While still near the end (never fighting a user who scrolled away),1089+ const distance = hasNativeOffset ? endOffset - nativeOffset : Infinity;
1071+ // re-pin to the current true end until sizes stop changing.1090+ const isIdle = !state.scrollingTo && !state.maintainingScrollAtEnd;
1072+ if (Math.abs(distance) > 2 && Math.abs(distance) <= scrollLength * 0.5) {1091+ const hasViewport = insetStartAdjustment > 0 && scrollLength > 0 && Number.isFinite(contentSize);
1073+ settledFrames = 0;1092+ const isStable = hasViewport && isIdle && Math.abs(distance) <= 2 && previousEndOffset !== void 0 && Math.abs(endOffset - previousEndOffset) <= 1;
1074+ revealStableFrames = 0;1093+ previousEndOffset = endOffset;
1094+ if (isStable) {
1095+ settledFrames++;
1096+ revealStableFrames++;
1097+ if (revealStableFrames >= INSET_END_REVEAL_STABLE_FRAMES) {
1098+ onRevealStability();
1099+ }
1100+ } else {
1101+ settledFrames = 0;
1102+ revealStableFrames = 0;
1103+ // Keep the existing near-end correction limit. In-flight scrolls and
1104+ // unknown native offsets wait; neither counts as a settled frame.
1105+ if (hasViewport && isIdle && Math.abs(distance) > 2 && Math.abs(distance) <= scrollLength * 0.5) {
1075+ const scroller = state.refScroller.current;1106+ const scroller = state.refScroller.current;
1076+ if (scroller) {1107+ if (scroller) {
1077+ scroller.scrollTo({ animated: false, x: 0, y: endOffset });1108+ scroller.scrollTo({ animated: false, x: 0, y: endOffset });
1078+ }1109+ }
1079+ } else {
1080+ settledFrames++;
1081+ revealStableFrames++;
1082+ if (revealStableFrames >= INSET_END_REVEAL_STABLE_FRAMES) {
1083+ onRevealStability();
1084+ }
1085+ }
1086+ } else {
1087+ // Conditions that make re-pinning unnecessary (underflow, in-flight
1088+ // programmatic scroll) count toward stability for the reveal.
1089+ revealStableFrames++;
1090+ if (revealStableFrames >= INSET_END_REVEAL_STABLE_FRAMES) {
1091+ onRevealStability();
1092+ }1110+ }
1093+ }1111+ }
1094+ requestAnimationFrame(tick);1112+ requestAnimationFrame(tick);
...@@ -1122,7 +1140,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1122,7 +1140,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1122 const requestedIndex = target.index;1140 const requestedIndex = target.index;
1123 const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0;1141 const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0;
1124 const itemSize = getItemSizeAtIndex(ctx, index);1142 const itemSize = getItemSizeAtIndex(ctx, index);
1125@@ -2726,7 +2888,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) {1143@@ -2726,7 +2897,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) {
1126 return;1144 return;
1127 }1145 }
1128 if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) {1146 if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) {
...@@ -1133,7 +1151,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1133,7 +1151,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1133 if (!shouldKeepEndTargetAlive) {1151 if (!shouldKeepEndTargetAlive) {
1134 if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) {1152 if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) {
1135 clearPendingInitialScrollFooterLayout(ctx, {1153 clearPendingInitialScrollFooterLayout(ctx, {
1136@@ -4651,7 +4815,8 @@ function maybeUpdateAnchoredEndSpace(ctx) {1154@@ -4651,7 +4824,8 @@ function maybeUpdateAnchoredEndSpace(ctx) {
1137 contentBelowAnchor = Math.max(0, contentBelowAnchor - ctx.scrollAxisGap);1155 contentBelowAnchor = Math.max(0, contentBelowAnchor - ctx.scrollAxisGap);
1138 contentBelowAnchor += (ctx.values.get("footerSize") || 0) + getStylePaddingEnd(state.props);1156 contentBelowAnchor += (ctx.values.get("footerSize") || 0) + getStylePaddingEnd(state.props);
1139 isReady = !hasUnknownTailSize;1157 isReady = !hasUnknownTailSize;
...@@ -1143,7 +1161,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1143,7 +1161,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1143 } else if (anchorIndex >= 0) {1161 } else if (anchorIndex >= 0) {
1144 isReady = false;1162 isReady = false;
1145 }1163 }
1146@@ -4671,6 +4836,12 @@ function maybeUpdateAnchoredEndSpace(ctx) {1164@@ -4671,6 +4845,12 @@ function maybeUpdateAnchoredEndSpace(ctx) {
1147 updateScroll(ctx, state.scroll, true, { markHasScrolled: false });1165 updateScroll(ctx, state.scroll, true, { markHasScrolled: false });
1148 }1166 }
1149 (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize });1167 (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize });
...@@ -1156,7 +1174,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1156,7 +1174,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1156 }1174 }
1157 return nextSize;1175 return nextSize;
1158 }1176 }
1159@@ -5694,6 +5865,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({1177@@ -5694,6 +5874,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
1160 horizontal1178 horizontal
1161 }) {1179 }) {
1162 const ctx = useStateContext();1180 const ctx = useStateContext();
...@@ -1164,7 +1182,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1164,7 +1182,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1164 const columnWrapperStyle = ctx.columnWrapperStyle;1182 const columnWrapperStyle = ctx.columnWrapperStyle;
1165 const animSize = useValue$("totalSize");1183 const animSize = useValue$("totalSize");
1166 const [readyToRender, numColumns, otherAxisSize = 0] = useArr$(["readyToRender", "numColumns", "otherAxisSize"]);1184 const [readyToRender, numColumns, otherAxisSize = 0] = useArr$(["readyToRender", "numColumns", "otherAxisSize"]);
1167@@ -5704,6 +5876,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({1185@@ -5704,6 +5885,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
1168 opacity: isVisible ? 1 : 0,1186 opacity: isVisible ? 1 : 0,
1169 width: animSize1187 width: animSize
1170 } : { height: animSize, minWidth: otherAxisSize, opacity: isVisible ? 1 : 0 };1188 } : { height: animSize, minWidth: otherAxisSize, opacity: isVisible ? 1 : 0 };
...@@ -1178,7 +1196,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1178,7 +1196,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1178 if (columnWrapperStyle) {1196 if (columnWrapperStyle) {
1179 const { columnGap, rowGap, gap } = columnWrapperStyle;1197 const { columnGap, rowGap, gap } = columnWrapperStyle;
1180 const gapX = columnGap || gap || 0;1198 const gapX = columnGap || gap || 0;
1181@@ -5724,7 +5903,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({1199@@ -5724,7 +5912,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
1182 }1200 }
1183 }1201 }
1184 }1202 }
...@@ -1188,7 +1206,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1188,7 +1206,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1188 });1206 });
1189 var Containers = typedMemo(function Containers2({1207 var Containers = typedMemo(function Containers2({
1190 freshDataTransitionEpoch,1208 freshDataTransitionEpoch,
1191@@ -5875,7 +6055,12 @@ var StyleSheet = StyleSheet$1;1209@@ -5875,7 +6064,12 @@ var StyleSheet = StyleSheet$1;
1192 1210
1193 // src/components/ListComponent.tsx1211 // src/components/ListComponent.tsx
1194 var AlignItemsAtEndSpacer = typedMemo(function AlignItemsAtEndSpacer2({ horizontal }) {1212 var AlignItemsAtEndSpacer = typedMemo(function AlignItemsAtEndSpacer2({ horizontal }) {
...@@ -1201,7 +1219,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1201,7 +1219,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1201 if (alignItemsAtEndPadding <= 0) {1219 if (alignItemsAtEndPadding <= 0) {
1202 return null;1220 return null;
1203 }1221 }
1204@@ -5908,8 +6093,12 @@ var ListComponent = typedMemo(function ListComponent2({1222@@ -5908,8 +6102,12 @@ var ListComponent = typedMemo(function ListComponent2({
1205 refScrollView,1223 refScrollView,
1206 renderScrollComponent,1224 renderScrollComponent,
1207 onLayoutFooter,1225 onLayoutFooter,
...@@ -1214,7 +1232,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1214,7 +1232,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1214 scrollAdjustHandler,1232 scrollAdjustHandler,
1215 snapToIndices,1233 snapToIndices,
1216 stickyHeaderConfig,1234 stickyHeaderConfig,
1217@@ -5980,7 +6169,17 @@ var ListComponent = typedMemo(function ListComponent2({1235@@ -5980,7 +6178,17 @@ var ListComponent = typedMemo(function ListComponent2({
1218 SnapOrScroll,1236 SnapOrScroll,
1219 {1237 {
1220 ...rest,1238 ...rest,
...@@ -1233,7 +1251,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1233,7 +1251,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1233 contentContainerStyle: [1251 contentContainerStyle: [
1234 horizontal ? { height: "100%" } : {},1252 horizontal ? { height: "100%" } : {},
1235 contentContainerStyle,1253 contentContainerStyle,
1236@@ -5989,7 +6188,10 @@ var ListComponent = typedMemo(function ListComponent2({1254@@ -5989,7 +6197,10 @@ var ListComponent = typedMemo(function ListComponent2({
1237 ],1255 ],
1238 contentOffset: initialContentOffset !== void 0 ? horizontal ? { x: initialContentOffset, y: 0 } : { x: 0, y: initialContentOffset } : void 0,1256 contentOffset: initialContentOffset !== void 0 ? horizontal ? { x: initialContentOffset, y: 0 } : { x: 0, y: initialContentOffset } : void 0,
1239 horizontal,1257 horizontal,
...@@ -1245,7 +1263,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1245,7 +1263,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1245 onLayout,1263 onLayout,
1246 onScroll: onScroll2,1264 onScroll: onScroll2,
1247 ref: refScrollView,1265 ref: refScrollView,
1248@@ -6730,7 +6932,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) {1266@@ -6730,7 +6941,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) {
1249 endBuffered: state.endBuffered,1267 endBuffered: state.endBuffered,
1250 getAverageItemSizes: () => getAverageItemSizes(state),1268 getAverageItemSizes: () => getAverageItemSizes(state),
1251 indexByKey: (key) => state.indexByKey.get(key),1269 indexByKey: (key) => state.indexByKey.get(key),
...@@ -1254,7 +1272,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1254,7 +1272,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1254 isAtStart: peek$(ctx, "isAtStart"),1272 isAtStart: peek$(ctx, "isAtStart"),
1255 isEndReached: state.isEndReached,1273 isEndReached: state.isEndReached,
1256 isNearEnd: peek$(ctx, "isNearEnd"),1274 isNearEnd: peek$(ctx, "isNearEnd"),
1257@@ -7054,6 +7256,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded1275@@ -7054,6 +7265,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1258 dataVersion,1276 dataVersion,
1259 drawDistance = 250,1277 drawDistance = 250,
1260 contentInsetEndAdjustment,1278 contentInsetEndAdjustment,
...@@ -1262,7 +1280,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1262,7 +1280,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1262 estimatedItemSize = 100,1280 estimatedItemSize = 100,
1263 estimatedListSize,1281 estimatedListSize,
1264 extraData,1282 extraData,
1265@@ -7111,10 +7314,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded1283@@ -7111,10 +7323,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1266 const animatedPropsInternal = props.animatedPropsInternal;1284 const animatedPropsInternal = props.animatedPropsInternal;
1267 const anchoredEndSpaceOwner = (_a3 = props.anchoredEndSpaceOwnerInternal) != null ? _a3 : "list";1285 const anchoredEndSpaceOwner = (_a3 = props.anchoredEndSpaceOwnerInternal) != null ? _a3 : "list";
1268 const positionComponentInternal = props.positionComponentInternal;1286 const positionComponentInternal = props.positionComponentInternal;
...@@ -1275,7 +1293,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1275,7 +1293,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1275 stickyPositionComponentInternal: _stickyPositionComponentInternal,1293 stickyPositionComponentInternal: _stickyPositionComponentInternal,
1276 ...restProps1294 ...restProps
1277 } = rest;1295 } = rest;
1278@@ -7179,7 +7384,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded1296@@ -7179,7 +7393,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1279 const combinedRef = useCombinedRef(refScroller, refScrollView);1297 const combinedRef = useCombinedRef(refScroller, refScrollView);
1280 const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString());1298 const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString());
1281 const stickyHeaderIndices = stickyHeaderIndicesProp;1299 const stickyHeaderIndices = stickyHeaderIndicesProp;
...@@ -1284,7 +1302,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1284,7 +1302,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1284 const previousContentInsetEndAdjustmentRef = useRef(contentInsetEndAdjustmentResolved);1302 const previousContentInsetEndAdjustmentRef = useRef(contentInsetEndAdjustmentResolved);
1285 const alwaysRenderIndices = useMemo(() => {1303 const alwaysRenderIndices = useMemo(() => {
1286 const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex);1304 const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex);
1287@@ -7320,6 +7525,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded1305@@ -7320,6 +7534,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1288 contentContainerAlignItems: contentContainerStyle.alignItems,1306 contentContainerAlignItems: contentContainerStyle.alignItems,
1289 contentInset,1307 contentInset,
1290 contentInsetEndAdjustment: contentInsetEndAdjustmentResolved,1308 contentInsetEndAdjustment: contentInsetEndAdjustmentResolved,
...@@ -1292,7 +1310,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1292,7 +1310,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1292 data: dataProp,1310 data: dataProp,
1293 dataKey,1311 dataKey,
1294 dataVersion,1312 dataVersion,
1295@@ -7351,6 +7557,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded1313@@ -7351,6 +7566,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1296 renderItem,1314 renderItem,
1297 rtl,1315 rtl,
1298 snapToIndices,1316 snapToIndices,
...@@ -1300,7 +1318,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1300,7 +1318,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1300 stickyHeaderIndicesArr: stickyHeaderIndices != null ? stickyHeaderIndices : [],1318 stickyHeaderIndicesArr: stickyHeaderIndices != null ? stickyHeaderIndices : [],
1301 stickyHeaderIndicesSet: useMemo(() => new Set(stickyHeaderIndices != null ? stickyHeaderIndices : []), [stickyHeaderIndices == null ? void 0 : stickyHeaderIndices.join(",")]),1319 stickyHeaderIndicesSet: useMemo(() => new Set(stickyHeaderIndices != null ? stickyHeaderIndices : []), [stickyHeaderIndices == null ? void 0 : stickyHeaderIndices.join(",")]),
1302 stickyPositionComponentInternal,1320 stickyPositionComponentInternal,
1303@@ -7402,6 +7609,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded1321@@ -7402,6 +7618,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1304 return void 0;1322 return void 0;
1305 }1323 }
1306 const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll);1324 const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll);
...@@ -1314,7 +1332,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1314,7 +1332,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1314 return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset;1332 return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset;
1315 }, [usesBootstrapInitialScroll]);1333 }, [usesBootstrapInitialScroll]);
1316 useLayoutEffect(() => {1334 useLayoutEffect(() => {
1317@@ -7526,6 +7740,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded1335@@ -7526,6 +7749,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1318 [1336 [
1319 dataKey,1337 dataKey,
1320 dataVersion,1338 dataVersion,
...@@ -1322,7 +1340,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1322,7 +1340,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1322 memoizedLastItemKeys.join(","),1340 memoizedLastItemKeys.join(","),
1323 numColumnsProp,1341 numColumnsProp,
1324 nextScrollAxisGap,1342 nextScrollAxisGap,
1325@@ -7622,6 +7837,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded1343@@ -7622,6 +7846,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1326 () => ({1344 () => ({
1327 getRenderedItem: (key) => getRenderedItem(ctx, key),1345 getRenderedItem: (key) => getRenderedItem(ctx, key),
1328 onMomentumScrollEnd: (event) => {1346 onMomentumScrollEnd: (event) => {
...@@ -1330,7 +1348,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1330,7 +1348,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1330 checkFinishedScrollFallback(ctx);1348 checkFinishedScrollFallback(ctx);
1331 if (state.props.onMomentumScrollEnd) {1349 if (state.props.onMomentumScrollEnd) {
1332 state.props.onMomentumScrollEnd(event);1350 state.props.onMomentumScrollEnd(event);
1333@@ -7630,6 +7846,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded1351@@ -7630,6 +7855,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1334 onScroll: (event) => onScroll(ctx, event),1352 onScroll: (event) => onScroll(ctx, event),
1335 onScrollBeginDrag: (event) => {1353 onScrollBeginDrag: (event) => {
1336 var _a4, _b2;1354 var _a4, _b2;
...@@ -1339,7 +1357,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97...@@ -1339,7 +1357,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..b8c6faf0be51cbda3e8af01f290dbe97
1339 prepareReachedEdgeForNextUserScroll(ctx);1357 prepareReachedEdgeForNextUserScroll(ctx);
1340 (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event);1358 (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event);
1341 },1359 },
1342@@ -7655,11 +7873,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded1360@@ -7655,11 +7882,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1343 ListFooterComponent,1361 ListFooterComponent,
1344 ListFooterComponentStyle,1362 ListFooterComponentStyle,
1345 ListHeaderComponent,1363 ListHeaderComponent,
pnpm-lock.yaml+5-5
...@@ -91,7 +91,7 @@ patchedDependencies:...@@ -91,7 +91,7 @@ patchedDependencies:
91 '@effect/vitest@4.0.0-beta.103': a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b91 '@effect/vitest@4.0.0-beta.103': a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b
92 '@expo/metro-config@57.0.12': 96f1a75347e6ea02dc4b7034ace815d8ee39e18b8166ebfb573d9e58328f0dc292 '@expo/metro-config@57.0.12': 96f1a75347e6ea02dc4b7034ace815d8ee39e18b8166ebfb573d9e58328f0dc2
93 '@ff-labs/fff-node@0.9.4': ab9ff544009e1891cfe3930105862d3699007f38922a79f3c98d90018deca36893 '@ff-labs/fff-node@0.9.4': ab9ff544009e1891cfe3930105862d3699007f38922a79f3c98d90018deca368
94 '@legendapp/list@3.3.5': 2c042ab630ac8b857024c178ee8c145d99c2f7297d9f94f5676238f08f0806ee94 '@legendapp/list@3.3.5': a05e968651a1352d2f374324016d1034b763b9fb2c5bf5f95111540d6899a52b
95 '@pierre/diffs@1.3.0-beta.10': 0ccee155b93b63d810e2c1a40c1fd676fb6fbcfa72cf6430dcedf1a3ae475ab495 '@pierre/diffs@1.3.0-beta.10': 0ccee155b93b63d810e2c1a40c1fd676fb6fbcfa72cf6430dcedf1a3ae475ab4
96 '@react-native-ai/apple@0.12.0': 2d09870c2848d185cb05b53ed823a46e12dba519324d8dd8e584e28731990f9d96 '@react-native-ai/apple@0.12.0': 2d09870c2848d185cb05b53ed823a46e12dba519324d8dd8e584e28731990f9d
97 '@react-native-menu/menu@2.0.0': f63d256bf6a97a873b5e628eb595bd6ef0075ddd5bdd890fc920f7a6024290dd97 '@react-native-menu/menu@2.0.0': f63d256bf6a97a873b5e628eb595bd6ef0075ddd5bdd890fc920f7a6024290dd
...@@ -236,7 +236,7 @@ importers:...@@ -236,7 +236,7 @@ importers:
236 version: 57.0.14(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)236 version: 57.0.14(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
237 '@legendapp/list':237 '@legendapp/list':
238 specifier: 'catalog:'238 specifier: 'catalog:'
239 version: 3.3.5(patch_hash=2c042ab630ac8b857024c178ee8c145d99c2f7297d9f94f5676238f08f0806ee)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)239 version: 3.3.5(patch_hash=a05e968651a1352d2f374324016d1034b763b9fb2c5bf5f95111540d6899a52b)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
240 '@noble/curves':240 '@noble/curves':
241 specifier: 'catalog:'241 specifier: 'catalog:'
242 version: 1.9.1242 version: 1.9.1
...@@ -580,7 +580,7 @@ importers:...@@ -580,7 +580,7 @@ importers:
580 version: 0.9.0580 version: 0.9.0
581 '@legendapp/list':581 '@legendapp/list':
582 specifier: 'catalog:'582 specifier: 'catalog:'
583 version: 3.3.5(patch_hash=2c042ab630ac8b857024c178ee8c145d99c2f7297d9f94f5676238f08f0806ee)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)583 version: 3.3.5(patch_hash=a05e968651a1352d2f374324016d1034b763b9fb2c5bf5f95111540d6899a52b)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
584 '@lexical/react':584 '@lexical/react':
585 specifier: ^0.41.0585 specifier: ^0.41.0
586 version: 0.41.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(yjs@13.6.31)586 version: 0.41.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(yjs@13.6.31)
...@@ -13269,7 +13269,7 @@ snapshots:...@@ -13269,7 +13269,7 @@ snapshots:
13269 dependencies:13269 dependencies:
13270 jsbi: 4.3.213270 jsbi: 4.3.2
1327113271
13272 '@legendapp/list@3.3.5(patch_hash=2c042ab630ac8b857024c178ee8c145d99c2f7297d9f94f5676238f08f0806ee)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':13272 '@legendapp/list@3.3.5(patch_hash=a05e968651a1352d2f374324016d1034b763b9fb2c5bf5f95111540d6899a52b)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':
13273 dependencies:13273 dependencies:
13274 react: 19.2.313274 react: 19.2.3
13275 use-sync-external-store: 1.6.0(react@19.2.3)13275 use-sync-external-store: 1.6.0(react@19.2.3)
...@@ -13277,7 +13277,7 @@ snapshots:...@@ -13277,7 +13277,7 @@ snapshots:
13277 react-dom: 19.2.3(react@19.2.3)13277 react-dom: 19.2.3(react@19.2.3)
13278 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)13278 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
1327913279
13280 '@legendapp/list@3.3.5(patch_hash=2c042ab630ac8b857024c178ee8c145d99c2f7297d9f94f5676238f08f0806ee)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':13280 '@legendapp/list@3.3.5(patch_hash=a05e968651a1352d2f374324016d1034b763b9fb2c5bf5f95111540d6899a52b)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
13281 dependencies:13281 dependencies:
13282 react: 19.2.613282 react: 19.2.6
13283 use-sync-external-store: 1.6.0(react@19.2.6)13283 use-sync-external-store: 1.6.0(react@19.2.6)
scripts/legend-list-initial-reveal.test.ts created+214
...@@ -0,0 +1,214 @@
1// @effect-diagnostics nodeBuiltinImport:off - reads the installed native JS bundle for synchronous VM tests.
2import * as NodeFS from "node:fs";
3import * as NodeVM from "node:vm";
4import { describe, expect, it, vi } from "vite-plus/test";
5
6// Exercise the shipped patch without loading React Native in the Node runner.
7// Native scroll delivery and animation frames advance independently here.
8function createList(bundle: string) {
9 const source = NodeFS.readFileSync(
10 new URL(`../apps/mobile/node_modules/@legendapp/list/${bundle}`, import.meta.url),
11 "utf8",
12 );
13 const renderState = source.slice(
14 source.indexOf("function setInitialRenderState("),
15 source.indexOf("// src/core/finishInitialScroll.ts"),
16 );
17 const watchdog = source.slice(
18 source.indexOf("var INSET_END_SETTLE_WATCHDOG_FRAMES"),
19 source.indexOf("function dispatchInitialScroll("),
20 );
21 const frames: Array<() => void> = [];
22 const values = new Map<string, unknown>();
23 const scrollTo = vi.fn();
24 const onLoad = vi.fn();
25 const state = {
26 props: { data: ["message"], onLoad, drawDistance: 500 },
27 initialScroll: { index: 0, viewPosition: 1 },
28 loadStartTime: 0,
29 didContainersLayout: true,
30 didFinishInitialScroll: true,
31 didLoad: false,
32 didUserDrag: false,
33 scrollLength: 800,
34 scroll: 400,
35 lastNativeScroll: 200 as number | undefined,
36 scrollingTo: undefined as { offset: number } | undefined,
37 maintainingScrollAtEnd: false,
38 refScroller: { current: { scrollTo } },
39 };
40 const ctx = { state };
41 let contentSize = 1200;
42 let inset = 100;
43 const api = NodeVM.runInNewContext(
44 `${renderState}\n${watchdog}\n({ start: startInsetEndSettleWatchdog, complete: setInitialRenderState })`,
45 {
46 requestAnimationFrame: (callback: () => void) => frames.push(callback),
47 getContentSize: () => contentSize,
48 getContentInsetStartAdjustment: () => inset,
49 peek$: (_ctx: unknown, key: string) => values.get(key),
50 set$: (_ctx: unknown, key: string, value: unknown) => values.set(key, value),
51 setAdaptiveRender: vi.fn(),
52 scheduleFullDrawDistancePrewarm: vi.fn(),
53 INITIAL_DRAW_DISTANCE: 250,
54 },
55 ) as {
56 start: (context: typeof ctx) => void;
57 complete: (context: typeof ctx, flags: Record<string, boolean>) => void;
58 };
59 return {
60 state,
61 scrollTo,
62 onLoad,
63 start: () => api.start(ctx),
64 complete: () => api.complete(ctx, {}),
65 ready: () => values.get("readyToRender") === true,
66 resize: (size: number) => {
67 contentSize = size;
68 },
69 setInset: (value: number) => {
70 inset = value;
71 },
72 advance(count: number) {
73 for (let index = 0; index < count; index++) {
74 const batch = frames.splice(0);
75 for (const frame of batch) frame();
76 }
77 },
78 };
79}
80
81for (const bundle of ["react-native.js", "react-native.mjs"]) {
82 describe(`initial inset end reveal (${bundle})`, () => {
83 it("waits for the native offset instead of the optimistic scroll target", () => {
84 const list = createList(bundle);
85 list.start();
86 list.advance(8);
87 expect(list.ready()).toBe(false);
88 expect(list.scrollTo).toHaveBeenCalledWith({ animated: false, x: 0, y: 400 });
89 list.state.lastNativeScroll = 400;
90 list.advance(7);
91 expect(list.ready()).toBe(true);
92 expect(list.onLoad).toHaveBeenCalledTimes(1);
93 });
94
95 it("gates the seeded contentOffset completion path too", () => {
96 const list = createList(bundle);
97 list.complete();
98 expect(list.ready()).toBe(false);
99 list.state.lastNativeScroll = 400;
100 list.advance(7);
101 expect(list.ready()).toBe(true);
102 });
103
104 it("preserves an initial index before the last item", () => {
105 const list = createList(bundle);
106 list.state.props.data = ["requested message", "later message"];
107 list.state.scroll = 200;
108 list.complete();
109 list.advance(8);
110 expect(list.ready()).toBe(true);
111 expect(list.scrollTo).not.toHaveBeenCalled();
112 });
113
114 it("does not count an in-flight scroll as stability", () => {
115 const list = createList(bundle);
116 list.state.lastNativeScroll = 400;
117 list.state.scrollingTo = { offset: 400 };
118 list.start();
119 list.advance(8);
120 expect(list.ready()).toBe(false);
121 list.state.scrollingTo = undefined;
122 list.state.maintainingScrollAtEnd = true;
123 list.advance(8);
124 expect(list.ready()).toBe(false);
125 list.state.maintainingScrollAtEnd = false;
126 list.advance(7);
127 expect(list.ready()).toBe(true);
128 });
129
130 it("waits for a stable end target even when native scrolling follows each measurement", () => {
131 const list = createList(bundle);
132 list.start();
133 for (let index = 0; index < 10; index++) {
134 list.resize(1200 + index * 10);
135 list.state.lastNativeScroll = 400 + index * 10;
136 list.advance(1);
137 }
138 expect(list.ready()).toBe(false);
139 list.advance(7);
140 expect(list.ready()).toBe(true);
141 });
142
143 it("accounts for header insets when content is shorter than the full viewport", () => {
144 const list = createList(bundle);
145 list.resize(780);
146 list.state.scroll = -20;
147 list.state.lastNativeScroll = -100;
148 list.start();
149 list.advance(8);
150 expect(list.ready()).toBe(false);
151 expect(list.scrollTo).toHaveBeenCalledWith({ animated: false, x: 0, y: -20 });
152 list.state.lastNativeScroll = -20;
153 list.advance(7);
154 expect(list.ready()).toBe(true);
155 });
156
157 it("keeps the reveal bounded when native events never arrive", () => {
158 const list = createList(bundle);
159 list.state.lastNativeScroll = undefined;
160 list.start();
161 list.advance(8);
162 expect(list.ready()).toBe(false);
163 expect(list.scrollTo).not.toHaveBeenCalled();
164 list.advance(32);
165 expect(list.ready()).toBe(true);
166 });
167
168 it("releases control on drag and never starts another initial hold", () => {
169 const list = createList(bundle);
170 list.start();
171 list.state.didUserDrag = true;
172 list.advance(1);
173 expect(list.ready()).toBe(true);
174 expect(list.scrollTo).not.toHaveBeenCalled();
175 list.complete();
176 list.start();
177 list.advance(20);
178 expect(list.state.didUserDrag).toBe(true);
179 expect(list.scrollTo).not.toHaveBeenCalled();
180 expect(list.onLoad).toHaveBeenCalledTimes(1);
181 });
182
183 it("preserves a drag that started before initial layout completed", () => {
184 const list = createList(bundle);
185 list.state.didUserDrag = true;
186 list.complete();
187 list.advance(1);
188 expect(list.ready()).toBe(true);
189 expect(list.state.didUserDrag).toBe(true);
190 expect(list.scrollTo).not.toHaveBeenCalled();
191 });
192
193 it("reveals short content at UIKit's resting offset without awaiting a scroll event", () => {
194 const list = createList(bundle);
195 list.resize(700);
196 list.state.lastNativeScroll = undefined;
197 list.complete();
198 expect(list.ready()).toBe(true);
199 list.advance(10);
200 expect(list.scrollTo).not.toHaveBeenCalled();
201 });
202
203 it("does not gate empty lists or lists without an automatic header inset", () => {
204 const list = createList(bundle);
205 list.setInset(0);
206 list.complete();
207 expect(list.ready()).toBe(true);
208 const empty = createList(bundle);
209 empty.state.props.data = [];
210 empty.complete();
211 expect(empty.ready()).toBe(true);
212 });
213 });
214}