From 88255707f791a4495638315a8b6f1cb0feb33896 Mon Sep 17 00:00:00 2001 From: T3 Code PR Stack <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Jul 2026 22:22:37 +0200 Subject: [PATCH] Revert "fix(mobile): stop long iOS threads from jumping while scrolling up (#4867)" This reverts commit 90f3913a81380af7aeafac62f8132aeb98345a12. --- apps/mobile/package.json | 2 +- ...3.3.patch => @legendapp__list@3.2.0.patch} | 244 ++++++------------ pnpm-lock.yaml | 36 +-- pnpm-workspace.yaml | 2 +- 4 files changed, 99 insertions(+), 185 deletions(-) rename patches/{@legendapp__list@3.3.3.patch => @legendapp__list@3.2.0.patch} (82%) diff --git a/apps/mobile/package.json b/apps/mobile/package.json index d2195282ef5..b75589c76ec 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -51,7 +51,7 @@ "@expo-google-fonts/dm-sans": "^0.4.2", "@expo/metro-runtime": "~56.0.15", "@expo/ui": "~56.0.18", - "@legendapp/list": "3.3.3", + "@legendapp/list": "3.2.0", "@noble/curves": "catalog:", "@noble/hashes": "catalog:", "@pierre/diffs": "catalog:", diff --git a/patches/@legendapp__list@3.3.3.patch b/patches/@legendapp__list@3.2.0.patch similarity index 82% rename from patches/@legendapp__list@3.3.3.patch rename to patches/@legendapp__list@3.2.0.patch index 4fa135d5aa0..8059fbb5f4e 100644 --- a/patches/@legendapp__list@3.3.3.patch +++ b/patches/@legendapp__list@3.2.0.patch @@ -1,8 +1,8 @@ diff --git a/keyboard.d.ts b/keyboard.d.ts -index 7bc3bb8..75ec120 100644 +index 5a115ea..2c65d31 100644 --- a/keyboard.d.ts +++ b/keyboard.d.ts -@@ -277,7 +277,7 @@ type KeyboardChatComposerInsetListRef = { +@@ -269,7 +269,7 @@ type KeyboardChatComposerInsetListRef = { type KeyboardChatComposerRef = { current: Pick | null; }; @@ -11,7 +11,7 @@ index 7bc3bb8..75ec120 100644 contentInsetEndAdjustment: SharedValue; onComposerLayout: (event: LayoutChangeEvent) => void; }; -@@ -286,8 +286,10 @@ declare function useKeyboardScrollToEnd({ freeze: freezeProp, listRef }: UseKeyb +@@ -278,8 +278,10 @@ declare function useKeyboardScrollToEnd({ freeze: freezeProp, listRef }: UseKeyb scrollMessageToEnd: ({ animated, closeKeyboard }: ScrollMessageToEndOptions) => Promise; }; declare const KeyboardAwareLegendList: (props: Omit, "anchoredEndSpace" | "contentInsetEndAdjustment" | "renderScrollComponent"> & KeyboardChatScrollViewPropsUnique & { @@ -187,10 +187,10 @@ index c1dd270..cb0d142 100644 renderScrollComponent: memoList, ...rest diff --git a/react-native.d.ts b/react-native.d.ts -index 8204015..cdeaab7 100644 +index 72d3f59..435a5fc 100644 --- a/react-native.d.ts +++ b/react-native.d.ts -@@ -293,6 +293,12 @@ interface LegendListSpecificProps { +@@ -284,6 +284,12 @@ interface LegendListSpecificProps { * The adjustment is also rendered as real content padding so the browser scroll range includes it. */ contentInsetEndAdjustment?: number; @@ -204,10 +204,10 @@ index 8204015..cdeaab7 100644 * Number of columns to render items in. * @default 1 diff --git a/react-native.js b/react-native.js -index 229f09a..2a1ceb6 100644 +index 8d4ff89..003b2f7 100644 --- a/react-native.js +++ b/react-native.js -@@ -930,7 +930,7 @@ function setInitialRenderState(ctx, { +@@ -1195,7 +1195,7 @@ function setInitialRenderState(ctx, { if (didInitialScroll) { state.didFinishInitialScroll = true; } @@ -215,8 +215,8 @@ index 229f09a..2a1ceb6 100644 + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll && !state.insetEndRevealHold); if (isReadyToRender && !peek$(ctx, "readyToRender")) { set$(ctx, "readyToRender", true); - setAdaptiveRender(ctx, "normal", "ready"); -@@ -1259,18 +1259,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { + setAdaptiveRender(ctx, "normal"); +@@ -1480,18 +1480,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { } // src/core/clampScrollOffset.ts @@ -242,7 +242,7 @@ index 229f09a..2a1ceb6 100644 return clampedOffset; } -@@ -1406,10 +1411,10 @@ function checkFinishedScrollFrame(ctx) { +@@ -1626,10 +1631,10 @@ function checkFinishedScrollFrame(ctx) { finishScrollTo(ctx); } } @@ -255,7 +255,7 @@ index 229f09a..2a1ceb6 100644 x: ctx.state.props.horizontal ? offset : 0, y: ctx.state.props.horizontal ? 0 : offset }); -@@ -1456,7 +1461,10 @@ function checkFinishedScrollFallback(ctx) { +@@ -1676,7 +1681,10 @@ function checkFinishedScrollFallback(ctx) { }); scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); } else if (shouldRetryUnalignedEndScroll) { @@ -267,10 +267,10 @@ index 229f09a..2a1ceb6 100644 scheduleFallbackCheck(100); } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) { finishScrollTo(ctx); -@@ -1517,9 +1525,18 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1737,9 +1745,18 @@ function doMaintainScrollAtEnd(ctx) { } + state.pendingMaintainScrollAtEnd = false; if (shouldMaintainScrollAtEnd) { - state.pendingMaintainScrollAtEnd = false; + const maintainAnchoredEndSpace = state.props.anchoredEndSpace; + const maintainAnchorIndex = maintainAnchoredEndSpace == null ? void 0 : maintainAnchoredEndSpace.anchorIndex; + if (maintainAnchorIndex !== void 0 && maintainAnchorIndex >= 0 && maintainAnchorIndex < state.props.data.length && !areKnownOrFixedItemSizesAvailable(ctx, maintainAnchorIndex, state.props.data.length - 1)) { @@ -287,7 +287,7 @@ index 229f09a..2a1ceb6 100644 } if (!state.maintainingScrollAtEnd) { const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; -@@ -1539,9 +1556,18 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1759,9 +1776,18 @@ function doMaintainScrollAtEnd(ctx) { y: 0 }); } else { @@ -309,18 +309,7 @@ index 229f09a..2a1ceb6 100644 } setTimeout( () => { -@@ -1571,6 +1597,10 @@ function doMaintainScrollAtEnd(ctx) { - function requestAdjust(ctx, positionDiff, dataChanged) { - const state = ctx.state; - if (Math.abs(positionDiff) > 0.1) { -+ // Timestamp for ReanimatedPositionView: repositions caused by an MVCP -+ // size adjustment are already compensated by a contentOffset shift, so -+ // animating them would make rows visibly lurch and slide back. -+ state.lastMVCPAdjustTime = Date.now(); - const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff; - const doit = () => { - if (needsScrollWorkaround) { -@@ -1674,7 +1704,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { +@@ -1888,7 +1914,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) { return 0; } @@ -331,7 +320,7 @@ index 229f09a..2a1ceb6 100644 const clampDelta = maxScroll - state.scroll; if (unresolvedAmount < 0) { return Math.max(unresolvedAmount, Math.min(0, clampDelta)); -@@ -1736,7 +1768,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { +@@ -1950,7 +1978,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta); return true; } @@ -340,7 +329,7 @@ index 229f09a..2a1ceb6 100644 const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll); const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON; if (isAtExpectedNativeClamp) { -@@ -1869,7 +1901,7 @@ function prepareMVCP(ctx, dataChanged) { +@@ -2083,7 +2111,7 @@ function prepareMVCP(ctx, dataChanged) { if (diff > 0) { diff = Math.max(0, totalSize - state.scroll - state.scrollLength); } else { @@ -349,7 +338,7 @@ index 229f09a..2a1ceb6 100644 state.scroll = maxScroll; state.scrollPending = maxScroll; diff = 0; -@@ -2274,8 +2306,121 @@ function scrollToIndex(ctx, { +@@ -2374,8 +2402,121 @@ function scrollToIndex(ctx, { } // src/core/initialScroll.ts @@ -471,7 +460,7 @@ index 229f09a..2a1ceb6 100644 const requestedIndex = target.index; const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0; const itemSize = getItemSizeAtIndex(ctx, index); -@@ -2704,7 +2849,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { +@@ -2804,7 +2945,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { return; } if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) { @@ -482,7 +471,7 @@ index 229f09a..2a1ceb6 100644 if (!shouldKeepEndTargetAlive) { if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) { clearPendingInitialScrollFooterLayout(ctx, { -@@ -4637,7 +4784,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { +@@ -4646,7 +4789,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { } contentBelowAnchor += footerSize + stylePaddingBottom; isReady = !hasUnknownTailSize; @@ -492,7 +481,7 @@ index 229f09a..2a1ceb6 100644 } else if (anchorIndex >= 0) { isReady = false; } -@@ -4655,6 +4803,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { +@@ -4664,6 +4808,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { updateScroll(ctx, state.scroll, true); } (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize }); @@ -505,7 +494,7 @@ index 229f09a..2a1ceb6 100644 } return nextSize; } -@@ -6960,6 +7114,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6462,6 +6612,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded dataVersion, drawDistance = 250, contentInsetEndAdjustment, @@ -513,15 +502,15 @@ index 229f09a..2a1ceb6 100644 estimatedItemSize = 100, estimatedListSize, extraData, -@@ -6990,6 +7145,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6492,6 +6643,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onLayout: onLayoutProp, onLoad, onMomentumScrollEnd, + onScrollBeginDrag, onRefresh, onScroll: onScrollProp, - onScrollBeginDrag, -@@ -7076,7 +7232,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + onStartReached, +@@ -6577,7 +6729,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded const combinedRef = useCombinedRef(refScroller, refScrollView); const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString()); const stickyHeaderIndices = stickyHeaderIndicesProp; @@ -530,15 +519,15 @@ index 229f09a..2a1ceb6 100644 const previousContentInsetEndAdjustmentRef = React2.useRef(contentInsetEndAdjustmentResolved); const alwaysRenderIndices = React2.useMemo(() => { const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex); -@@ -7215,6 +7371,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6710,6 +6862,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded contentContainerAlignItems: contentContainerStyle.alignItems, contentInset, contentInsetEndAdjustment: contentInsetEndAdjustmentResolved, + contentInsetStartAdjustment, data: dataProp, - dataKey, dataVersion, -@@ -7303,6 +7460,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + drawDistance, +@@ -6789,6 +6942,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded return void 0; } const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll); @@ -552,15 +541,20 @@ index 229f09a..2a1ceb6 100644 return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset; }, [usesBootstrapInitialScroll]); React2.useLayoutEffect(() => { -@@ -7526,6 +7690,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded - onScroll: (event) => onScroll(ctx, event), - onScrollBeginDrag: (event) => { - var _a4, _b2; -+ ctx.state.didUserDrag = true; - prepareReachedEdgeForNextUserScroll(ctx); - (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); +@@ -6995,6 +7155,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + onMomentumScrollEnd(event); + } }, -@@ -7555,6 +7720,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded ++ onScrollBeginDrag: (event) => { ++ ctx.state.didUserDrag = true; ++ if (onScrollBeginDrag) { ++ onScrollBeginDrag(event); ++ } ++ }, + onScroll: (event) => onScroll(ctx, event) + }), + [] +@@ -7019,6 +7185,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onLayout, onLayoutFooter, onMomentumScrollEnd: fns.onMomentumScrollEnd, @@ -569,10 +563,10 @@ index 229f09a..2a1ceb6 100644 recycleItems, refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2__namespace.cloneElement(refreshControlElement, { diff --git a/react-native.mjs b/react-native.mjs -index c2e0f38..5313086 100644 +index 2e96ca7..fc88d4b 100644 --- a/react-native.mjs +++ b/react-native.mjs -@@ -909,7 +909,7 @@ function setInitialRenderState(ctx, { +@@ -1174,7 +1174,7 @@ function setInitialRenderState(ctx, { if (didInitialScroll) { state.didFinishInitialScroll = true; } @@ -580,8 +574,8 @@ index c2e0f38..5313086 100644 + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll && !state.insetEndRevealHold); if (isReadyToRender && !peek$(ctx, "readyToRender")) { set$(ctx, "readyToRender", true); - setAdaptiveRender(ctx, "normal", "ready"); -@@ -1238,18 +1238,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { + setAdaptiveRender(ctx, "normal"); +@@ -1459,18 +1459,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { } // src/core/clampScrollOffset.ts @@ -607,7 +601,7 @@ index c2e0f38..5313086 100644 return clampedOffset; } -@@ -1385,10 +1390,10 @@ function checkFinishedScrollFrame(ctx) { +@@ -1605,10 +1610,10 @@ function checkFinishedScrollFrame(ctx) { finishScrollTo(ctx); } } @@ -620,7 +614,7 @@ index c2e0f38..5313086 100644 x: ctx.state.props.horizontal ? offset : 0, y: ctx.state.props.horizontal ? 0 : offset }); -@@ -1435,7 +1440,10 @@ function checkFinishedScrollFallback(ctx) { +@@ -1655,7 +1660,10 @@ function checkFinishedScrollFallback(ctx) { }); scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); } else if (shouldRetryUnalignedEndScroll) { @@ -632,10 +626,10 @@ index c2e0f38..5313086 100644 scheduleFallbackCheck(100); } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) { finishScrollTo(ctx); -@@ -1496,9 +1504,18 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1716,9 +1724,18 @@ function doMaintainScrollAtEnd(ctx) { } + state.pendingMaintainScrollAtEnd = false; if (shouldMaintainScrollAtEnd) { - state.pendingMaintainScrollAtEnd = false; + const maintainAnchoredEndSpace = state.props.anchoredEndSpace; + const maintainAnchorIndex = maintainAnchoredEndSpace == null ? void 0 : maintainAnchoredEndSpace.anchorIndex; + if (maintainAnchorIndex !== void 0 && maintainAnchorIndex >= 0 && maintainAnchorIndex < state.props.data.length && !areKnownOrFixedItemSizesAvailable(ctx, maintainAnchorIndex, state.props.data.length - 1)) { @@ -652,7 +646,7 @@ index c2e0f38..5313086 100644 } if (!state.maintainingScrollAtEnd) { const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; -@@ -1518,9 +1535,18 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1738,9 +1755,18 @@ function doMaintainScrollAtEnd(ctx) { y: 0 }); } else { @@ -674,18 +668,7 @@ index c2e0f38..5313086 100644 } setTimeout( () => { -@@ -1550,6 +1576,10 @@ function doMaintainScrollAtEnd(ctx) { - function requestAdjust(ctx, positionDiff, dataChanged) { - const state = ctx.state; - if (Math.abs(positionDiff) > 0.1) { -+ // Timestamp for ReanimatedPositionView: repositions caused by an MVCP -+ // size adjustment are already compensated by a contentOffset shift, so -+ // animating them would make rows visibly lurch and slide back. -+ state.lastMVCPAdjustTime = Date.now(); - const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff; - const doit = () => { - if (needsScrollWorkaround) { -@@ -1653,7 +1683,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { +@@ -1867,7 +1893,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) { return 0; } @@ -696,7 +679,7 @@ index c2e0f38..5313086 100644 const clampDelta = maxScroll - state.scroll; if (unresolvedAmount < 0) { return Math.max(unresolvedAmount, Math.min(0, clampDelta)); -@@ -1715,7 +1747,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { +@@ -1929,7 +1957,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta); return true; } @@ -705,7 +688,7 @@ index c2e0f38..5313086 100644 const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll); const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON; if (isAtExpectedNativeClamp) { -@@ -1848,7 +1880,7 @@ function prepareMVCP(ctx, dataChanged) { +@@ -2062,7 +2090,7 @@ function prepareMVCP(ctx, dataChanged) { if (diff > 0) { diff = Math.max(0, totalSize - state.scroll - state.scrollLength); } else { @@ -714,7 +697,7 @@ index c2e0f38..5313086 100644 state.scroll = maxScroll; state.scrollPending = maxScroll; diff = 0; -@@ -2253,8 +2285,121 @@ function scrollToIndex(ctx, { +@@ -2353,8 +2381,121 @@ function scrollToIndex(ctx, { } // src/core/initialScroll.ts @@ -836,7 +819,7 @@ index c2e0f38..5313086 100644 const requestedIndex = target.index; const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0; const itemSize = getItemSizeAtIndex(ctx, index); -@@ -2683,7 +2828,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { +@@ -2783,7 +2924,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { return; } if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) { @@ -847,7 +830,7 @@ index c2e0f38..5313086 100644 if (!shouldKeepEndTargetAlive) { if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) { clearPendingInitialScrollFooterLayout(ctx, { -@@ -4616,7 +4763,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { +@@ -4625,7 +4768,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { } contentBelowAnchor += footerSize + stylePaddingBottom; isReady = !hasUnknownTailSize; @@ -857,7 +840,7 @@ index c2e0f38..5313086 100644 } else if (anchorIndex >= 0) { isReady = false; } -@@ -4634,6 +4782,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { +@@ -4643,6 +4787,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { updateScroll(ctx, state.scroll, true); } (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize }); @@ -870,7 +853,7 @@ index c2e0f38..5313086 100644 } return nextSize; } -@@ -6939,6 +7093,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6441,6 +6591,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded dataVersion, drawDistance = 250, contentInsetEndAdjustment, @@ -878,7 +861,15 @@ index c2e0f38..5313086 100644 estimatedItemSize = 100, estimatedListSize, extraData, -@@ -7055,7 +7210,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6471,6 +6622,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + onLayout: onLayoutProp, + onLoad, + onMomentumScrollEnd, ++ onScrollBeginDrag, + onRefresh, + onScroll: onScrollProp, + onStartReached, +@@ -6556,7 +6708,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded const combinedRef = useCombinedRef(refScroller, refScrollView); const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString()); const stickyHeaderIndices = stickyHeaderIndicesProp; @@ -887,15 +878,15 @@ index c2e0f38..5313086 100644 const previousContentInsetEndAdjustmentRef = useRef(contentInsetEndAdjustmentResolved); const alwaysRenderIndices = useMemo(() => { const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex); -@@ -7194,6 +7349,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6689,6 +6841,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded contentContainerAlignItems: contentContainerStyle.alignItems, contentInset, contentInsetEndAdjustment: contentInsetEndAdjustmentResolved, + contentInsetStartAdjustment, data: dataProp, - dataKey, dataVersion, -@@ -7282,6 +7438,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + drawDistance, +@@ -6768,6 +6921,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded return void 0; } const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll); @@ -909,15 +900,20 @@ index c2e0f38..5313086 100644 return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset; }, [usesBootstrapInitialScroll]); useLayoutEffect(() => { -@@ -7505,6 +7668,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded - onScroll: (event) => onScroll(ctx, event), - onScrollBeginDrag: (event) => { - var _a4, _b2; -+ ctx.state.didUserDrag = true; - prepareReachedEdgeForNextUserScroll(ctx); - (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); +@@ -6974,6 +7134,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + onMomentumScrollEnd(event); + } }, -@@ -7534,6 +7698,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded ++ onScrollBeginDrag: (event) => { ++ ctx.state.didUserDrag = true; ++ if (onScrollBeginDrag) { ++ onScrollBeginDrag(event); ++ } ++ }, + onScroll: (event) => onScroll(ctx, event) + }), + [] +@@ -6998,6 +7164,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onLayout, onLayoutFooter, onMomentumScrollEnd: fns.onMomentumScrollEnd, @@ -926,10 +922,10 @@ index c2e0f38..5313086 100644 recycleItems, refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2.cloneElement(refreshControlElement, { diff --git a/reanimated.d.ts b/reanimated.d.ts -index 940da28..28dccbe 100644 +index 7e2d11f..d5b0d66 100644 --- a/reanimated.d.ts +++ b/reanimated.d.ts -@@ -294,6 +294,12 @@ interface LegendListSpecificProps { +@@ -285,6 +285,12 @@ interface LegendListSpecificProps { * The adjustment is also rendered as real content padding so the browser scroll range includes it. */ contentInsetEndAdjustment?: number; @@ -942,73 +938,3 @@ index 940da28..28dccbe 100644 /** * Number of columns to render items in. * @default 1 -diff --git a/reanimated.js b/reanimated.js -index f1265fa..16dcef0 100644 ---- a/reanimated.js -+++ b/reanimated.js -@@ -116,7 +116,7 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( - const [positionValue = POSITION_OUT_OF_VIEW] = useArr$([`containerPosition${id}`]); - const prevItemKeyRef = React__namespace.useRef(void 0); - let shouldSkipTransitionForRecycleReuse = false; -- if (recycleItems && layoutTransition) { -+ if (layoutTransition) { - const itemKeySignal = `containerItemKey${id}`; - const itemKey = peek$(ctx, itemKeySignal); - shouldSkipTransitionForRecycleReuse = itemKey !== void 0 && prevItemKeyRef.current !== void 0 && prevItemKeyRef.current !== itemKey; -@@ -130,10 +130,20 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( - () => [style, horizontal ? { left: positionValue } : { top: positionValue }], - [horizontal, positionValue, style] - ); -+ // Two kinds of repositions must not animate: (1) MVCP size adjustments, -+ // which are compensated by an equal contentOffset shift so the row should -+ // not visibly move — animating turns the invisible correction into a -+ // lurch-and-settle; (2) any reposition while the user is actively -+ // scrolling, where rows shifting under the finger reads as jank. The -+ // transition exists to smooth at-rest shifts (streaming text, work-log -+ // folds), so gate it to at-rest moments. -+ const now = Date.now(); -+ const isMVCPReposition = now - (ctx.state.lastMVCPAdjustTime || 0) < 300 || -+ now - (ctx.state.lastNativeScrollTime || 0) < 300; - return /* @__PURE__ */ React__namespace.createElement( - Reanimated__default.default.View, - { -- layout: shouldSkipTransitionForRecycleReuse ? void 0 : layoutTransition, -+ layout: shouldSkipTransitionForRecycleReuse || isMVCPReposition ? void 0 : layoutTransition, - ref: refView, - style: viewStyle, - ...rest -diff --git a/reanimated.mjs b/reanimated.mjs -index 29a00d5..9c25ec5 100644 ---- a/reanimated.mjs -+++ b/reanimated.mjs -@@ -92,7 +92,7 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( - const [positionValue = POSITION_OUT_OF_VIEW] = useArr$([`containerPosition${id}`]); - const prevItemKeyRef = React.useRef(void 0); - let shouldSkipTransitionForRecycleReuse = false; -- if (recycleItems && layoutTransition) { -+ if (layoutTransition) { - const itemKeySignal = `containerItemKey${id}`; - const itemKey = peek$(ctx, itemKeySignal); - shouldSkipTransitionForRecycleReuse = itemKey !== void 0 && prevItemKeyRef.current !== void 0 && prevItemKeyRef.current !== itemKey; -@@ -106,10 +106,20 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( - () => [style, horizontal ? { left: positionValue } : { top: positionValue }], - [horizontal, positionValue, style] - ); -+ // Two kinds of repositions must not animate: (1) MVCP size adjustments, -+ // which are compensated by an equal contentOffset shift so the row should -+ // not visibly move — animating turns the invisible correction into a -+ // lurch-and-settle; (2) any reposition while the user is actively -+ // scrolling, where rows shifting under the finger reads as jank. The -+ // transition exists to smooth at-rest shifts (streaming text, work-log -+ // folds), so gate it to at-rest moments. -+ const now = Date.now(); -+ const isMVCPReposition = now - (ctx.state.lastMVCPAdjustTime || 0) < 300 || -+ now - (ctx.state.lastNativeScrollTime || 0) < 300; - return /* @__PURE__ */ React.createElement( - Reanimated.View, - { -- layout: shouldSkipTransitionForRecycleReuse ? void 0 : layoutTransition, -+ layout: shouldSkipTransitionForRecycleReuse || isMVCPReposition ? void 0 : layoutTransition, - ref: refView, - style: viewStyle, - ...rest diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c8dd408c51..2c8c7bcbf95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,7 +74,7 @@ patchedDependencies: '@effect/vitest@4.0.0-beta.102': a607339aab944136a084a05f159aa0f5a69776934d4b835dab8f9992f1c13425 '@expo/metro-config@56.0.14': 8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46 '@ff-labs/fff-node@0.9.4': 2b16019ce7ab61aec6478dd02f79ef468cc1d5c51e9d00764f7d2ab8167210c8 - '@legendapp/list@3.3.3': d162d67b73933ab077d00627cdf52b18ea88b28c4fae4e8340a854df25026f09 + '@legendapp/list@3.2.0': 9203ec3db86574a31e157a265cefd8997b259cc420a0c4c61c580c80ed8cf78d '@pierre/diffs@1.3.0-beta.10': 7ef7cb0cbabb17c15cdb137554068b36f15f6f5265e73fb51452aa3380db91aa '@react-native-menu/menu@2.0.0': 5ea3ae4bf1d9baf5443b65c269bb09621c27a68d556f713778f37b1e8d46aaae '@react-navigation/native-stack@7.17.6': c7fc101b78d434904425e5a24c22fb0042298dec6f807250486e784f3c717273 @@ -212,8 +212,8 @@ importers: specifier: ~56.0.18 version: 56.0.18(3cdc0dde9f93166d952f1e1bd0cb25c0) '@legendapp/list': - specifier: 3.3.3 - version: 3.3.3(patch_hash=d162d67b73933ab077d00627cdf52b18ea88b28c4fae4e8340a854df25026f09)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.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) + specifier: 3.2.0 + version: 3.2.0(patch_hash=9203ec3db86574a31e157a265cefd8997b259cc420a0c4c61c580c80ed8cf78d)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.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) '@noble/curves': specifier: 'catalog:' version: 1.9.1 @@ -545,7 +545,7 @@ importers: version: 0.9.0 '@legendapp/list': specifier: 3.2.0 - version: 3.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 3.2.0(patch_hash=9203ec3db86574a31e157a265cefd8997b259cc420a0c4c61c580c80ed8cf78d)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@lexical/react': specifier: ^0.41.0 version: 0.41.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(yjs@13.6.31) @@ -2981,18 +2981,6 @@ packages: react-native: optional: true - '@legendapp/list@3.3.3': - resolution: {integrity: sha512-p3g4xG6f//s4XQKhuus2189GCQgOHEIbJXHePqeDxj+6UQQQyij4YBjyArNSCgqoP0c03sxDPSOuCFB128Ql6g==} - peerDependencies: - react: '*' - react-dom: '*' - react-native: '*' - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true - '@lexical/clipboard@0.41.0': resolution: {integrity: sha512-Ex5lPkb4NBBX1DCPzOAIeHBJFH1bJcmATjREaqpnTfxCbuOeQkt44wchezUA0oDl+iAxNZ3+pLLWiUju9icoSA==} @@ -13000,14 +12988,7 @@ snapshots: dependencies: jsbi: 4.3.2 - '@legendapp/list@3.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - react: 19.2.6 - use-sync-external-store: 1.6.0(react@19.2.6) - optionalDependencies: - react-dom: 19.2.6(react@19.2.6) - - '@legendapp/list@3.3.3(patch_hash=d162d67b73933ab077d00627cdf52b18ea88b28c4fae4e8340a854df25026f09)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.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)': + '@legendapp/list@3.2.0(patch_hash=9203ec3db86574a31e157a265cefd8997b259cc420a0c4c61c580c80ed8cf78d)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.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)': dependencies: react: 19.2.3 use-sync-external-store: 1.6.0(react@19.2.3) @@ -13015,6 +12996,13 @@ snapshots: react-dom: 19.2.3(react@19.2.3) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.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) + '@legendapp/list@3.2.0(patch_hash=9203ec3db86574a31e157a265cefd8997b259cc420a0c4c61c580c80ed8cf78d)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + react: 19.2.6 + use-sync-external-store: 1.6.0(react@19.2.6) + optionalDependencies: + react-dom: 19.2.6(react@19.2.6) + '@lexical/clipboard@0.41.0': dependencies: '@lexical/html': 0.41.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 49a3bd44351..26a451327fa 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -128,7 +128,7 @@ patchedDependencies: "@effect/vitest@4.0.0-beta.102": patches/@effect__vitest@4.0.0-beta.102.patch "@expo/metro-config@56.0.14": patches/@expo%2Fmetro-config@56.0.14.patch "@ff-labs/fff-node@0.9.4": patches/@ff-labs__fff-node@0.9.4.patch - "@legendapp/list@3.3.3": patches/@legendapp__list@3.3.3.patch + "@legendapp/list@3.2.0": patches/@legendapp__list@3.2.0.patch "@pierre/diffs@1.3.0-beta.10": patches/@pierre%2Fdiffs@1.3.0-beta.10.patch "@react-native-menu/menu@2.0.0": patches/@react-native-menu__menu@2.0.0.patch "@react-navigation/native-stack@7.17.6": patches/@react-navigation%2Fnative-stack@7.17.6.patch