diff --git a/apps/mobile/package.json b/apps/mobile/package.json index c99351547be..9a5e64aa46f 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -49,7 +49,7 @@ "@expo-google-fonts/dm-sans": "^0.4.2", "@expo/metro-runtime": "~56.0.15", "@expo/ui": "~56.0.18", - "@legendapp/list": "3.2.0", + "@legendapp/list": "3.3.3", "@noble/curves": "catalog:", "@noble/hashes": "catalog:", "@pierre/diffs": "catalog:", diff --git a/patches/@legendapp__list@3.2.0.patch b/patches/@legendapp__list@3.3.3.patch similarity index 82% rename from patches/@legendapp__list@3.2.0.patch rename to patches/@legendapp__list@3.3.3.patch index 8059fbb5f4e..4fa135d5aa0 100644 --- a/patches/@legendapp__list@3.2.0.patch +++ b/patches/@legendapp__list@3.3.3.patch @@ -1,8 +1,8 @@ diff --git a/keyboard.d.ts b/keyboard.d.ts -index 5a115ea..2c65d31 100644 +index 7bc3bb8..75ec120 100644 --- a/keyboard.d.ts +++ b/keyboard.d.ts -@@ -269,7 +269,7 @@ type KeyboardChatComposerInsetListRef = { +@@ -277,7 +277,7 @@ type KeyboardChatComposerInsetListRef = { type KeyboardChatComposerRef = { current: Pick | null; }; @@ -11,7 +11,7 @@ index 5a115ea..2c65d31 100644 contentInsetEndAdjustment: SharedValue; onComposerLayout: (event: LayoutChangeEvent) => void; }; -@@ -278,8 +278,10 @@ declare function useKeyboardScrollToEnd({ freeze: freezeProp, listRef }: UseKeyb +@@ -286,8 +286,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 72d3f59..435a5fc 100644 +index 8204015..cdeaab7 100644 --- a/react-native.d.ts +++ b/react-native.d.ts -@@ -284,6 +284,12 @@ interface LegendListSpecificProps { +@@ -293,6 +293,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 72d3f59..435a5fc 100644 * Number of columns to render items in. * @default 1 diff --git a/react-native.js b/react-native.js -index 8d4ff89..003b2f7 100644 +index 229f09a..2a1ceb6 100644 --- a/react-native.js +++ b/react-native.js -@@ -1195,7 +1195,7 @@ function setInitialRenderState(ctx, { +@@ -930,7 +930,7 @@ function setInitialRenderState(ctx, { if (didInitialScroll) { state.didFinishInitialScroll = true; } @@ -215,8 +215,8 @@ index 8d4ff89..003b2f7 100644 + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll && !state.insetEndRevealHold); if (isReadyToRender && !peek$(ctx, "readyToRender")) { set$(ctx, "readyToRender", true); - setAdaptiveRender(ctx, "normal"); -@@ -1480,18 +1480,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { + setAdaptiveRender(ctx, "normal", "ready"); +@@ -1259,18 +1259,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { } // src/core/clampScrollOffset.ts @@ -242,7 +242,7 @@ index 8d4ff89..003b2f7 100644 return clampedOffset; } -@@ -1626,10 +1631,10 @@ function checkFinishedScrollFrame(ctx) { +@@ -1406,10 +1411,10 @@ function checkFinishedScrollFrame(ctx) { finishScrollTo(ctx); } } @@ -255,7 +255,7 @@ index 8d4ff89..003b2f7 100644 x: ctx.state.props.horizontal ? offset : 0, y: ctx.state.props.horizontal ? 0 : offset }); -@@ -1676,7 +1681,10 @@ function checkFinishedScrollFallback(ctx) { +@@ -1456,7 +1461,10 @@ function checkFinishedScrollFallback(ctx) { }); scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); } else if (shouldRetryUnalignedEndScroll) { @@ -267,10 +267,10 @@ index 8d4ff89..003b2f7 100644 scheduleFallbackCheck(100); } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) { finishScrollTo(ctx); -@@ -1737,9 +1745,18 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1517,9 +1525,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 8d4ff89..003b2f7 100644 } if (!state.maintainingScrollAtEnd) { const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; -@@ -1759,9 +1776,18 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1539,9 +1556,18 @@ function doMaintainScrollAtEnd(ctx) { y: 0 }); } else { @@ -309,7 +309,18 @@ index 8d4ff89..003b2f7 100644 } setTimeout( () => { -@@ -1888,7 +1914,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { +@@ -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) { if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) { return 0; } @@ -320,7 +331,7 @@ index 8d4ff89..003b2f7 100644 const clampDelta = maxScroll - state.scroll; if (unresolvedAmount < 0) { return Math.max(unresolvedAmount, Math.min(0, clampDelta)); -@@ -1950,7 +1978,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { +@@ -1736,7 +1768,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta); return true; } @@ -329,7 +340,7 @@ index 8d4ff89..003b2f7 100644 const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll); const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON; if (isAtExpectedNativeClamp) { -@@ -2083,7 +2111,7 @@ function prepareMVCP(ctx, dataChanged) { +@@ -1869,7 +1901,7 @@ function prepareMVCP(ctx, dataChanged) { if (diff > 0) { diff = Math.max(0, totalSize - state.scroll - state.scrollLength); } else { @@ -338,7 +349,7 @@ index 8d4ff89..003b2f7 100644 state.scroll = maxScroll; state.scrollPending = maxScroll; diff = 0; -@@ -2374,8 +2402,121 @@ function scrollToIndex(ctx, { +@@ -2274,8 +2306,121 @@ function scrollToIndex(ctx, { } // src/core/initialScroll.ts @@ -460,7 +471,7 @@ index 8d4ff89..003b2f7 100644 const requestedIndex = target.index; const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0; const itemSize = getItemSizeAtIndex(ctx, index); -@@ -2804,7 +2945,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { +@@ -2704,7 +2849,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { return; } if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) { @@ -471,7 +482,7 @@ index 8d4ff89..003b2f7 100644 if (!shouldKeepEndTargetAlive) { if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) { clearPendingInitialScrollFooterLayout(ctx, { -@@ -4646,7 +4789,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { +@@ -4637,7 +4784,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { } contentBelowAnchor += footerSize + stylePaddingBottom; isReady = !hasUnknownTailSize; @@ -481,7 +492,7 @@ index 8d4ff89..003b2f7 100644 } else if (anchorIndex >= 0) { isReady = false; } -@@ -4664,6 +4808,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { +@@ -4655,6 +4803,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 }); @@ -494,7 +505,7 @@ index 8d4ff89..003b2f7 100644 } return nextSize; } -@@ -6462,6 +6612,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6960,6 +7114,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded dataVersion, drawDistance = 250, contentInsetEndAdjustment, @@ -502,15 +513,15 @@ index 8d4ff89..003b2f7 100644 estimatedItemSize = 100, estimatedListSize, extraData, -@@ -6492,6 +6643,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6990,6 +7145,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onLayout: onLayoutProp, onLoad, onMomentumScrollEnd, + onScrollBeginDrag, onRefresh, onScroll: onScrollProp, - onStartReached, -@@ -6577,7 +6729,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + onScrollBeginDrag, +@@ -7076,7 +7232,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; @@ -519,15 +530,15 @@ index 8d4ff89..003b2f7 100644 const previousContentInsetEndAdjustmentRef = React2.useRef(contentInsetEndAdjustmentResolved); const alwaysRenderIndices = React2.useMemo(() => { const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex); -@@ -6710,6 +6862,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7215,6 +7371,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded contentContainerAlignItems: contentContainerStyle.alignItems, contentInset, contentInsetEndAdjustment: contentInsetEndAdjustmentResolved, + contentInsetStartAdjustment, data: dataProp, + dataKey, dataVersion, - drawDistance, -@@ -6789,6 +6942,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7303,6 +7460,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded return void 0; } const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll); @@ -541,20 +552,15 @@ index 8d4ff89..003b2f7 100644 return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset; }, [usesBootstrapInitialScroll]); React2.useLayoutEffect(() => { -@@ -6995,6 +7155,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded - onMomentumScrollEnd(event); - } - }, -+ onScrollBeginDrag: (event) => { +@@ -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; -+ if (onScrollBeginDrag) { -+ onScrollBeginDrag(event); -+ } -+ }, - onScroll: (event) => onScroll(ctx, event) - }), - [] -@@ -7019,6 +7185,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + prepareReachedEdgeForNextUserScroll(ctx); + (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); + }, +@@ -7555,6 +7720,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onLayout, onLayoutFooter, onMomentumScrollEnd: fns.onMomentumScrollEnd, @@ -563,10 +569,10 @@ index 8d4ff89..003b2f7 100644 recycleItems, refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2__namespace.cloneElement(refreshControlElement, { diff --git a/react-native.mjs b/react-native.mjs -index 2e96ca7..fc88d4b 100644 +index c2e0f38..5313086 100644 --- a/react-native.mjs +++ b/react-native.mjs -@@ -1174,7 +1174,7 @@ function setInitialRenderState(ctx, { +@@ -909,7 +909,7 @@ function setInitialRenderState(ctx, { if (didInitialScroll) { state.didFinishInitialScroll = true; } @@ -574,8 +580,8 @@ index 2e96ca7..fc88d4b 100644 + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll && !state.insetEndRevealHold); if (isReadyToRender && !peek$(ctx, "readyToRender")) { set$(ctx, "readyToRender", true); - setAdaptiveRender(ctx, "normal"); -@@ -1459,18 +1459,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { + setAdaptiveRender(ctx, "normal", "ready"); +@@ -1238,18 +1238,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { } // src/core/clampScrollOffset.ts @@ -601,7 +607,7 @@ index 2e96ca7..fc88d4b 100644 return clampedOffset; } -@@ -1605,10 +1610,10 @@ function checkFinishedScrollFrame(ctx) { +@@ -1385,10 +1390,10 @@ function checkFinishedScrollFrame(ctx) { finishScrollTo(ctx); } } @@ -614,7 +620,7 @@ index 2e96ca7..fc88d4b 100644 x: ctx.state.props.horizontal ? offset : 0, y: ctx.state.props.horizontal ? 0 : offset }); -@@ -1655,7 +1660,10 @@ function checkFinishedScrollFallback(ctx) { +@@ -1435,7 +1440,10 @@ function checkFinishedScrollFallback(ctx) { }); scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); } else if (shouldRetryUnalignedEndScroll) { @@ -626,10 +632,10 @@ index 2e96ca7..fc88d4b 100644 scheduleFallbackCheck(100); } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) { finishScrollTo(ctx); -@@ -1716,9 +1724,18 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1496,9 +1504,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)) { @@ -646,7 +652,7 @@ index 2e96ca7..fc88d4b 100644 } if (!state.maintainingScrollAtEnd) { const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; -@@ -1738,9 +1755,18 @@ function doMaintainScrollAtEnd(ctx) { +@@ -1518,9 +1535,18 @@ function doMaintainScrollAtEnd(ctx) { y: 0 }); } else { @@ -668,7 +674,18 @@ index 2e96ca7..fc88d4b 100644 } setTimeout( () => { -@@ -1867,7 +1893,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { +@@ -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) { if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) { return 0; } @@ -679,7 +696,7 @@ index 2e96ca7..fc88d4b 100644 const clampDelta = maxScroll - state.scroll; if (unresolvedAmount < 0) { return Math.max(unresolvedAmount, Math.min(0, clampDelta)); -@@ -1929,7 +1957,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { +@@ -1715,7 +1747,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta); return true; } @@ -688,7 +705,7 @@ index 2e96ca7..fc88d4b 100644 const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll); const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON; if (isAtExpectedNativeClamp) { -@@ -2062,7 +2090,7 @@ function prepareMVCP(ctx, dataChanged) { +@@ -1848,7 +1880,7 @@ function prepareMVCP(ctx, dataChanged) { if (diff > 0) { diff = Math.max(0, totalSize - state.scroll - state.scrollLength); } else { @@ -697,7 +714,7 @@ index 2e96ca7..fc88d4b 100644 state.scroll = maxScroll; state.scrollPending = maxScroll; diff = 0; -@@ -2353,8 +2381,121 @@ function scrollToIndex(ctx, { +@@ -2253,8 +2285,121 @@ function scrollToIndex(ctx, { } // src/core/initialScroll.ts @@ -819,7 +836,7 @@ index 2e96ca7..fc88d4b 100644 const requestedIndex = target.index; const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0; const itemSize = getItemSizeAtIndex(ctx, index); -@@ -2783,7 +2924,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { +@@ -2683,7 +2828,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { return; } if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) { @@ -830,7 +847,7 @@ index 2e96ca7..fc88d4b 100644 if (!shouldKeepEndTargetAlive) { if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) { clearPendingInitialScrollFooterLayout(ctx, { -@@ -4625,7 +4768,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { +@@ -4616,7 +4763,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { } contentBelowAnchor += footerSize + stylePaddingBottom; isReady = !hasUnknownTailSize; @@ -840,7 +857,7 @@ index 2e96ca7..fc88d4b 100644 } else if (anchorIndex >= 0) { isReady = false; } -@@ -4643,6 +4787,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { +@@ -4634,6 +4782,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 }); @@ -853,7 +870,7 @@ index 2e96ca7..fc88d4b 100644 } return nextSize; } -@@ -6441,6 +6591,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -6939,6 +7093,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded dataVersion, drawDistance = 250, contentInsetEndAdjustment, @@ -861,15 +878,7 @@ index 2e96ca7..fc88d4b 100644 estimatedItemSize = 100, estimatedListSize, extraData, -@@ -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 +@@ -7055,7 +7210,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; @@ -878,15 +887,15 @@ index 2e96ca7..fc88d4b 100644 const previousContentInsetEndAdjustmentRef = useRef(contentInsetEndAdjustmentResolved); const alwaysRenderIndices = useMemo(() => { const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex); -@@ -6689,6 +6841,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7194,6 +7349,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded contentContainerAlignItems: contentContainerStyle.alignItems, contentInset, contentInsetEndAdjustment: contentInsetEndAdjustmentResolved, + contentInsetStartAdjustment, data: dataProp, + dataKey, dataVersion, - drawDistance, -@@ -6768,6 +6921,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7282,6 +7438,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded return void 0; } const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll); @@ -900,20 +909,15 @@ index 2e96ca7..fc88d4b 100644 return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset; }, [usesBootstrapInitialScroll]); useLayoutEffect(() => { -@@ -6974,6 +7134,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded - onMomentumScrollEnd(event); - } - }, -+ onScrollBeginDrag: (event) => { +@@ -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; -+ if (onScrollBeginDrag) { -+ onScrollBeginDrag(event); -+ } -+ }, - onScroll: (event) => onScroll(ctx, event) - }), - [] -@@ -6998,6 +7164,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded + prepareReachedEdgeForNextUserScroll(ctx); + (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); + }, +@@ -7534,6 +7698,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onLayout, onLayoutFooter, onMomentumScrollEnd: fns.onMomentumScrollEnd, @@ -922,10 +926,10 @@ index 2e96ca7..fc88d4b 100644 recycleItems, refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2.cloneElement(refreshControlElement, { diff --git a/reanimated.d.ts b/reanimated.d.ts -index 7e2d11f..d5b0d66 100644 +index 940da28..28dccbe 100644 --- a/reanimated.d.ts +++ b/reanimated.d.ts -@@ -285,6 +285,12 @@ interface LegendListSpecificProps { +@@ -294,6 +294,12 @@ interface LegendListSpecificProps { * The adjustment is also rendered as real content padding so the browser scroll range includes it. */ contentInsetEndAdjustment?: number; @@ -938,3 +942,73 @@ index 7e2d11f..d5b0d66 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 c46d2c6a4dc..816846ab529 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.2.0': 9203ec3db86574a31e157a265cefd8997b259cc420a0c4c61c580c80ed8cf78d + '@legendapp/list@3.3.3': d162d67b73933ab077d00627cdf52b18ea88b28c4fae4e8340a854df25026f09 '@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.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) + 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) '@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(patch_hash=9203ec3db86574a31e157a265cefd8997b259cc420a0c4c61c580c80ed8cf78d)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 3.2.0(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) @@ -2975,6 +2975,18 @@ 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==} @@ -12982,7 +12994,14 @@ snapshots: dependencies: jsbi: 4.3.2 - '@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)': + '@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)': dependencies: react: 19.2.3 use-sync-external-store: 1.6.0(react@19.2.3) @@ -12990,13 +13009,6 @@ 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 70f61242053..d41e50e8784 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -127,7 +127,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.2.0": patches/@legendapp__list@3.2.0.patch + "@legendapp/list@3.3.3": patches/@legendapp__list@3.3.3.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