| author | |
| committer | |
| log | 90f3913a81380af7aeafac62f8132aeb98345a12 |
| tree | ddfdf3728bdd6695e41337b5f3c00a02b5f0a130 |
| parent | 2652fee49bdb0a7d286124cd2bd318155d4a7931 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>5 files changed, 1040 insertions(+), 954 deletions(-)
apps/mobile/package.json+1-1| ... | @@ -49,7 +49,7 @@ | ... | @@ -49,7 +49,7 @@ |
| 49 | "@expo-google-fonts/dm-sans": "^0.4.2", | 49 | "@expo-google-fonts/dm-sans": "^0.4.2", |
| 50 | "@expo/metro-runtime": "~56.0.15", | 50 | "@expo/metro-runtime": "~56.0.15", |
| 51 | "@expo/ui": "~56.0.18", | 51 | "@expo/ui": "~56.0.18", |
| 52 | "@legendapp/list": "3.2.0", | 52 | "@legendapp/list": "3.3.3", |
| 53 | "@noble/curves": "catalog:", | 53 | "@noble/curves": "catalog:", |
| 54 | "@noble/hashes": "catalog:", | 54 | "@noble/hashes": "catalog:", |
| 55 | "@pierre/diffs": "catalog:", | 55 | "@pierre/diffs": "catalog:", |
patches/@legendapp__list@3.2.0.patch deleted-940| ... | @@ -1,940 +0,0 @@ | ||
| 1 | diff --git a/keyboard.d.ts b/keyboard.d.ts | ||
| 2 | index 5a115ea..2c65d31 100644 | ||
| 3 | --- a/keyboard.d.ts | ||
| 4 | +++ b/keyboard.d.ts | ||
| 5 | @@ -269,7 +269,7 @@ type KeyboardChatComposerInsetListRef = { | ||
| 6 | type KeyboardChatComposerRef = { | ||
| 7 | current: Pick<View, "measure"> | null; | ||
| 8 | }; | ||
| 9 | -declare function useKeyboardChatComposerInset(listRef: KeyboardChatComposerInsetListRef, composerRef: KeyboardChatComposerRef, initialHeight?: number): { | ||
| 10 | +declare function useKeyboardChatComposerInset(listRef: KeyboardChatComposerInsetListRef, composerRef: KeyboardChatComposerRef, initialHeight?: number, heightAdjustment?: number): { | ||
| 11 | contentInsetEndAdjustment: SharedValue<number>; | ||
| 12 | onComposerLayout: (event: LayoutChangeEvent) => void; | ||
| 13 | }; | ||
| 14 | @@ -278,8 +278,10 @@ declare function useKeyboardScrollToEnd({ freeze: freezeProp, listRef }: UseKeyb | ||
| 15 | scrollMessageToEnd: ({ animated, closeKeyboard }: ScrollMessageToEndOptions) => Promise<void>; | ||
| 16 | }; | ||
| 17 | declare const KeyboardAwareLegendList: <ItemT>(props: Omit<AnimatedLegendListProps<ItemT>, "anchoredEndSpace" | "contentInsetEndAdjustment" | "renderScrollComponent"> & KeyboardChatScrollViewPropsUnique & { | ||
| 18 | + adjustedInsetCompensation?: number; | ||
| 19 | anchoredEndSpace?: AnchoredEndSpaceConfig; | ||
| 20 | contentInsetEndAdjustment?: SharedValue<number>; | ||
| 21 | + contentInsetEndStaticAdjustment?: number; | ||
| 22 | keyboardOffset?: number; | ||
| 23 | } & React.RefAttributes<LegendListRef>) => React.ReactElement | null; | ||
| 24 | |||
| 25 | diff --git a/keyboard.js b/keyboard.js | ||
| 26 | index 736286a..8218172 100644 | ||
| 27 | --- a/keyboard.js | ||
| 28 | +++ b/keyboard.js | ||
| 29 | @@ -33,19 +33,19 @@ if (typeof __DEV__ !== "undefined" && __DEV__ && !reactNativeKeyboardController. | ||
| 30 | "[legend-list] KeyboardAwareLegendList requires a recent react-native-keyboard-controller with KeyboardChatScrollView. Please upgrade react-native-keyboard-controller to at least 1.21.7." | ||
| 31 | ); | ||
| 32 | } | ||
| 33 | -function useKeyboardChatComposerInset(listRef, composerRef, initialHeight = 0) { | ||
| 34 | +function useKeyboardChatComposerInset(listRef, composerRef, initialHeight = 0, heightAdjustment = 0) { | ||
| 35 | const contentInsetEndAdjustment = reactNativeReanimated.useSharedValue(initialHeight); | ||
| 36 | const lastHeightRef = React.useRef(void 0); | ||
| 37 | const reportHeight = React.useCallback( | ||
| 38 | - (height) => { | ||
| 39 | + (rawHeight) => { | ||
| 40 | + const height = Math.max(0, rawHeight + heightAdjustment); | ||
| 41 | var _a; | ||
| 42 | if (Number.isFinite(height) && height !== lastHeightRef.current) { | ||
| 43 | lastHeightRef.current = height; | ||
| 44 | contentInsetEndAdjustment.value = height; | ||
| 45 | - (_a = listRef.current) == null ? void 0 : _a.reportContentInset({ bottom: height }); | ||
| 46 | } | ||
| 47 | }, | ||
| 48 | - [contentInsetEndAdjustment, listRef] | ||
| 49 | + [contentInsetEndAdjustment, heightAdjustment, listRef] | ||
| 50 | ); | ||
| 51 | React.useLayoutEffect(() => { | ||
| 52 | var _a; | ||
| 53 | @@ -84,9 +84,11 @@ function useKeyboardScrollToEnd({ freeze: freezeProp, listRef }) { | ||
| 54 | } | ||
| 55 | var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2(props, forwardedRef) { | ||
| 56 | const { | ||
| 57 | + adjustedInsetCompensation, | ||
| 58 | anchoredEndSpace, | ||
| 59 | applyWorkaroundForContentInsetHitTestBug, | ||
| 60 | contentInsetEndAdjustment, | ||
| 61 | + contentInsetEndStaticAdjustment, | ||
| 62 | freeze, | ||
| 63 | keyboardLiftBehavior, | ||
| 64 | keyboardOffset, | ||
| 65 | @@ -109,11 +111,15 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | ||
| 66 | includeInEndInset: true, | ||
| 67 | onSizeChanged: (size) => { | ||
| 68 | var _a; | ||
| 69 | - blankSpace.value = size; | ||
| 70 | + // The anchored blank is sized against the ADJUSTED viewport, but the | ||
| 71 | + // scroll component writes it as a raw contentInset that UIKit tops up | ||
| 72 | + // with the safe-area bottom — write it net of that or the end rest | ||
| 73 | + // (and the anchored message) sinks one safe-area under the header. | ||
| 74 | + blankSpace.value = size > 0 ? Math.max(0, size - (adjustedInsetCompensation || 0)) : 0; | ||
| 75 | (_a = anchoredEndSpace.onSizeChanged) == null ? void 0 : _a.call(anchoredEndSpace, size); | ||
| 76 | } | ||
| 77 | }; | ||
| 78 | - }, [anchoredEndSpace, blankSpace]); | ||
| 79 | + }, [adjustedInsetCompensation, anchoredEndSpace, blankSpace]); | ||
| 80 | const onContentInsetChange = React.useCallback((insets) => { | ||
| 81 | var _a; | ||
| 82 | (_a = refLegendList.current) == null ? void 0 : _a.reportContentInset(insets); | ||
| 83 | @@ -124,6 +130,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | ||
| 84 | reactNativeKeyboardController.KeyboardChatScrollView, | ||
| 85 | { | ||
| 86 | ...scrollProps, | ||
| 87 | + adjustedInsetCompensation, | ||
| 88 | applyWorkaroundForContentInsetHitTestBug, | ||
| 89 | blankSpace, | ||
| 90 | extraContentPadding: contentInsetEndAdjustment, | ||
| 91 | @@ -135,6 +142,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | ||
| 92 | ); | ||
| 93 | }, | ||
| 94 | [ | ||
| 95 | + adjustedInsetCompensation, | ||
| 96 | applyWorkaroundForContentInsetHitTestBug, | ||
| 97 | blankSpace, | ||
| 98 | contentInsetEndAdjustment, | ||
| 99 | @@ -149,6 +157,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | ||
| 100 | AnimatedLegendListInternal, | ||
| 101 | { | ||
| 102 | anchoredEndSpace: anchoredEndSpaceWithBlankSpace, | ||
| 103 | + contentInsetEndAdjustment: contentInsetEndStaticAdjustment, | ||
| 104 | ref: combinedRef, | ||
| 105 | renderScrollComponent: memoList, | ||
| 106 | ...rest | ||
| 107 | diff --git a/keyboard.mjs b/keyboard.mjs | ||
| 108 | index c1dd270..cb0d142 100644 | ||
| 109 | --- a/keyboard.mjs | ||
| 110 | +++ b/keyboard.mjs | ||
| 111 | @@ -12,19 +12,19 @@ if (typeof __DEV__ !== "undefined" && __DEV__ && !KeyboardChatScrollView) { | ||
| 112 | "[legend-list] KeyboardAwareLegendList requires a recent react-native-keyboard-controller with KeyboardChatScrollView. Please upgrade react-native-keyboard-controller to at least 1.21.7." | ||
| 113 | ); | ||
| 114 | } | ||
| 115 | -function useKeyboardChatComposerInset(listRef, composerRef, initialHeight = 0) { | ||
| 116 | +function useKeyboardChatComposerInset(listRef, composerRef, initialHeight = 0, heightAdjustment = 0) { | ||
| 117 | const contentInsetEndAdjustment = useSharedValue(initialHeight); | ||
| 118 | const lastHeightRef = useRef(void 0); | ||
| 119 | const reportHeight = useCallback( | ||
| 120 | - (height) => { | ||
| 121 | + (rawHeight) => { | ||
| 122 | + const height = Math.max(0, rawHeight + heightAdjustment); | ||
| 123 | var _a; | ||
| 124 | if (Number.isFinite(height) && height !== lastHeightRef.current) { | ||
| 125 | lastHeightRef.current = height; | ||
| 126 | contentInsetEndAdjustment.value = height; | ||
| 127 | - (_a = listRef.current) == null ? void 0 : _a.reportContentInset({ bottom: height }); | ||
| 128 | } | ||
| 129 | }, | ||
| 130 | - [contentInsetEndAdjustment, listRef] | ||
| 131 | + [contentInsetEndAdjustment, heightAdjustment, listRef] | ||
| 132 | ); | ||
| 133 | useLayoutEffect(() => { | ||
| 134 | var _a; | ||
| 135 | @@ -63,9 +63,11 @@ function useKeyboardScrollToEnd({ freeze: freezeProp, listRef }) { | ||
| 136 | } | ||
| 137 | var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2(props, forwardedRef) { | ||
| 138 | const { | ||
| 139 | + adjustedInsetCompensation, | ||
| 140 | anchoredEndSpace, | ||
| 141 | applyWorkaroundForContentInsetHitTestBug, | ||
| 142 | contentInsetEndAdjustment, | ||
| 143 | + contentInsetEndStaticAdjustment, | ||
| 144 | freeze, | ||
| 145 | keyboardLiftBehavior, | ||
| 146 | keyboardOffset, | ||
| 147 | @@ -88,11 +90,15 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | ||
| 148 | includeInEndInset: true, | ||
| 149 | onSizeChanged: (size) => { | ||
| 150 | var _a; | ||
| 151 | - blankSpace.value = size; | ||
| 152 | + // The anchored blank is sized against the ADJUSTED viewport, but the | ||
| 153 | + // scroll component writes it as a raw contentInset that UIKit tops up | ||
| 154 | + // with the safe-area bottom — write it net of that or the end rest | ||
| 155 | + // (and the anchored message) sinks one safe-area under the header. | ||
| 156 | + blankSpace.value = size > 0 ? Math.max(0, size - (adjustedInsetCompensation || 0)) : 0; | ||
| 157 | (_a = anchoredEndSpace.onSizeChanged) == null ? void 0 : _a.call(anchoredEndSpace, size); | ||
| 158 | } | ||
| 159 | }; | ||
| 160 | - }, [anchoredEndSpace, blankSpace]); | ||
| 161 | + }, [adjustedInsetCompensation, anchoredEndSpace, blankSpace]); | ||
| 162 | const onContentInsetChange = useCallback((insets) => { | ||
| 163 | var _a; | ||
| 164 | (_a = refLegendList.current) == null ? void 0 : _a.reportContentInset(insets); | ||
| 165 | @@ -103,6 +109,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | ||
| 166 | KeyboardChatScrollView, | ||
| 167 | { | ||
| 168 | ...scrollProps, | ||
| 169 | + adjustedInsetCompensation, | ||
| 170 | applyWorkaroundForContentInsetHitTestBug, | ||
| 171 | blankSpace, | ||
| 172 | extraContentPadding: contentInsetEndAdjustment, | ||
| 173 | @@ -114,6 +121,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | ||
| 174 | ); | ||
| 175 | }, | ||
| 176 | [ | ||
| 177 | + adjustedInsetCompensation, | ||
| 178 | applyWorkaroundForContentInsetHitTestBug, | ||
| 179 | blankSpace, | ||
| 180 | contentInsetEndAdjustment, | ||
| 181 | @@ -128,6 +136,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | ||
| 182 | AnimatedLegendListInternal, | ||
| 183 | { | ||
| 184 | anchoredEndSpace: anchoredEndSpaceWithBlankSpace, | ||
| 185 | + contentInsetEndAdjustment: contentInsetEndStaticAdjustment, | ||
| 186 | ref: combinedRef, | ||
| 187 | renderScrollComponent: memoList, | ||
| 188 | ...rest | ||
| 189 | diff --git a/react-native.d.ts b/react-native.d.ts | ||
| 190 | index 72d3f59..435a5fc 100644 | ||
| 191 | --- a/react-native.d.ts | ||
| 192 | +++ b/react-native.d.ts | ||
| 193 | @@ -284,6 +284,12 @@ interface LegendListSpecificProps<ItemT, TItemType extends string | undefined> { | ||
| 194 | * The adjustment is also rendered as real content padding so the browser scroll range includes it. | ||
| 195 | */ | ||
| 196 | contentInsetEndAdjustment?: number; | ||
| 197 | + /** | ||
| 198 | + * Width/height of a leading content inset applied natively outside the list's knowledge | ||
| 199 | + * (e.g. iOS contentInsetAdjustmentBehavior="automatic" under a transparent header). | ||
| 200 | + * Programmatic scrolls clamp to -adjustment instead of 0 so content can rest below the header. | ||
| 201 | + */ | ||
| 202 | + contentInsetStartAdjustment?: number; | ||
| 203 | /** | ||
| 204 | * Number of columns to render items in. | ||
| 205 | * @default 1 | ||
| 206 | diff --git a/react-native.js b/react-native.js | ||
| 207 | index 8d4ff89..003b2f7 100644 | ||
| 208 | --- a/react-native.js | ||
| 209 | +++ b/react-native.js | ||
| 210 | @@ -1195,7 +1195,7 @@ function setInitialRenderState(ctx, { | ||
| 211 | if (didInitialScroll) { | ||
| 212 | state.didFinishInitialScroll = true; | ||
| 213 | } | ||
| 214 | - const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); | ||
| 215 | + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll && !state.insetEndRevealHold); | ||
| 216 | if (isReadyToRender && !peek$(ctx, "readyToRender")) { | ||
| 217 | set$(ctx, "readyToRender", true); | ||
| 218 | setAdaptiveRender(ctx, "normal"); | ||
| 219 | @@ -1480,18 +1480,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { | ||
| 220 | } | ||
| 221 | |||
| 222 | // src/core/clampScrollOffset.ts | ||
| 223 | +function getContentInsetStartAdjustment(ctx) { | ||
| 224 | + const adjustment = ctx.state.props.contentInsetStartAdjustment; | ||
| 225 | + return typeof adjustment === "number" && Number.isFinite(adjustment) ? Math.max(0, adjustment) : 0; | ||
| 226 | +} | ||
| 227 | function clampScrollOffset(ctx, offset, scrollTarget) { | ||
| 228 | const state = ctx.state; | ||
| 229 | const contentSize = getContentSize(ctx); | ||
| 230 | + const minOffset = -getContentInsetStartAdjustment(ctx); | ||
| 231 | let clampedOffset = offset; | ||
| 232 | if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android" || state.lastLayout)) { | ||
| 233 | - const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); | ||
| 234 | + const baseMaxOffset = Math.max(minOffset, contentSize - state.scrollLength); | ||
| 235 | const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; | ||
| 236 | const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; | ||
| 237 | const maxOffset = baseMaxOffset + extraEndOffset; | ||
| 238 | clampedOffset = Math.min(offset, maxOffset); | ||
| 239 | } | ||
| 240 | - clampedOffset = Math.max(0, clampedOffset); | ||
| 241 | + clampedOffset = Math.max(minOffset, clampedOffset); | ||
| 242 | return clampedOffset; | ||
| 243 | } | ||
| 244 | |||
| 245 | @@ -1626,10 +1631,10 @@ function checkFinishedScrollFrame(ctx) { | ||
| 246 | finishScrollTo(ctx); | ||
| 247 | } | ||
| 248 | } | ||
| 249 | -function scrollToFallbackOffset(ctx, offset) { | ||
| 250 | +function scrollToFallbackOffset(ctx, offset, animated) { | ||
| 251 | var _a3; | ||
| 252 | (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({ | ||
| 253 | - animated: false, | ||
| 254 | + animated: !!animated, | ||
| 255 | x: ctx.state.props.horizontal ? offset : 0, | ||
| 256 | y: ctx.state.props.horizontal ? 0 : offset | ||
| 257 | }); | ||
| 258 | @@ -1676,7 +1681,10 @@ function checkFinishedScrollFallback(ctx) { | ||
| 259 | }); | ||
| 260 | scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); | ||
| 261 | } else if (shouldRetryUnalignedEndScroll) { | ||
| 262 | - scrollToFallbackOffset(ctx, completionState.clampedTargetOffset); | ||
| 263 | + const isActivelyAnimatingToEnd = !!isStillScrollingTo.animated && Date.now() - state.scrollTime < 100; | ||
| 264 | + if (!isActivelyAnimatingToEnd) { | ||
| 265 | + scrollToFallbackOffset(ctx, completionState.clampedTargetOffset, !!isStillScrollingTo.animated); | ||
| 266 | + } | ||
| 267 | scheduleFallbackCheck(100); | ||
| 268 | } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) { | ||
| 269 | finishScrollTo(ctx); | ||
| 270 | @@ -1737,9 +1745,18 @@ function doMaintainScrollAtEnd(ctx) { | ||
| 271 | } | ||
| 272 | state.pendingMaintainScrollAtEnd = false; | ||
| 273 | if (shouldMaintainScrollAtEnd) { | ||
| 274 | + const maintainAnchoredEndSpace = state.props.anchoredEndSpace; | ||
| 275 | + const maintainAnchorIndex = maintainAnchoredEndSpace == null ? void 0 : maintainAnchoredEndSpace.anchorIndex; | ||
| 276 | + if (maintainAnchorIndex !== void 0 && maintainAnchorIndex >= 0 && maintainAnchorIndex < state.props.data.length && !areKnownOrFixedItemSizesAvailable(ctx, maintainAnchorIndex, state.props.data.length - 1)) { | ||
| 277 | + return false; | ||
| 278 | + } | ||
| 279 | const contentSize = getContentSize(ctx); | ||
| 280 | + const maintainInsetStartAdjustment = getContentInsetStartAdjustment(ctx); | ||
| 281 | if (contentSize < state.scrollLength) { | ||
| 282 | - state.scroll = 0; | ||
| 283 | + state.scroll = -maintainInsetStartAdjustment; | ||
| 284 | + if (maintainInsetStartAdjustment > 0) { | ||
| 285 | + return true; | ||
| 286 | + } | ||
| 287 | } | ||
| 288 | if (!state.maintainingScrollAtEnd) { | ||
| 289 | const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; | ||
| 290 | @@ -1759,9 +1776,18 @@ function doMaintainScrollAtEnd(ctx) { | ||
| 291 | y: 0 | ||
| 292 | }); | ||
| 293 | } else { | ||
| 294 | - scroller == null ? void 0 : scroller.scrollToEnd({ | ||
| 295 | - animated: maintainScrollAtEnd.animated | ||
| 296 | - }); | ||
| 297 | + const insetStartAdjustment = getContentInsetStartAdjustment(ctx); | ||
| 298 | + if (insetStartAdjustment > 0) { | ||
| 299 | + scroller == null ? void 0 : scroller.scrollTo({ | ||
| 300 | + animated: maintainScrollAtEnd.animated, | ||
| 301 | + x: 0, | ||
| 302 | + y: Math.max(-insetStartAdjustment, getContentSize(ctx) - state.scrollLength) | ||
| 303 | + }); | ||
| 304 | + } else { | ||
| 305 | + scroller == null ? void 0 : scroller.scrollToEnd({ | ||
| 306 | + animated: maintainScrollAtEnd.animated | ||
| 307 | + }); | ||
| 308 | + } | ||
| 309 | } | ||
| 310 | setTimeout( | ||
| 311 | () => { | ||
| 312 | @@ -1888,7 +1914,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { | ||
| 313 | if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) { | ||
| 314 | return 0; | ||
| 315 | } | ||
| 316 | - const maxScroll = Math.max(0, totalSize - state.scrollLength); | ||
| 317 | + const insetStartAdjustment = state.props.contentInsetStartAdjustment; | ||
| 318 | + const minScroll = typeof insetStartAdjustment === "number" && Number.isFinite(insetStartAdjustment) ? -Math.max(0, insetStartAdjustment) : 0; | ||
| 319 | + const maxScroll = Math.max(minScroll, totalSize - state.scrollLength); | ||
| 320 | const clampDelta = maxScroll - state.scroll; | ||
| 321 | if (unresolvedAmount < 0) { | ||
| 322 | return Math.max(unresolvedAmount, Math.min(0, clampDelta)); | ||
| 323 | @@ -1950,7 +1978,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { | ||
| 324 | settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta); | ||
| 325 | return true; | ||
| 326 | } | ||
| 327 | - const expectedNativeClampScroll = Math.max(0, getContentSize(ctx) - state.scrollLength); | ||
| 328 | + const expectedNativeClampScroll = Math.max(-getContentInsetStartAdjustment(ctx), getContentSize(ctx) - state.scrollLength); | ||
| 329 | const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll); | ||
| 330 | const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON; | ||
| 331 | if (isAtExpectedNativeClamp) { | ||
| 332 | @@ -2083,7 +2111,7 @@ function prepareMVCP(ctx, dataChanged) { | ||
| 333 | if (diff > 0) { | ||
| 334 | diff = Math.max(0, totalSize - state.scroll - state.scrollLength); | ||
| 335 | } else { | ||
| 336 | - const maxScroll = Math.max(0, totalSize - state.scrollLength); | ||
| 337 | + const maxScroll = Math.max(-getContentInsetStartAdjustment(ctx), totalSize - state.scrollLength); | ||
| 338 | state.scroll = maxScroll; | ||
| 339 | state.scrollPending = maxScroll; | ||
| 340 | diff = 0; | ||
| 341 | @@ -2374,8 +2402,121 @@ function scrollToIndex(ctx, { | ||
| 342 | } | ||
| 343 | |||
| 344 | // src/core/initialScroll.ts | ||
| 345 | +var INSET_END_SETTLE_WATCHDOG_FRAMES = 150; | ||
| 346 | +var INSET_END_SETTLE_WATCHDOG_STABLE_FRAMES = 20; | ||
| 347 | +var INSET_END_REVEAL_STABLE_FRAMES = 3; | ||
| 348 | +var INSET_END_REVEAL_MAX_HOLD_FRAMES = 40; | ||
| 349 | +function startInsetEndSettleWatchdog(ctx) { | ||
| 350 | + const state = ctx.state; | ||
| 351 | + if (state.insetEndSettleWatchdogActive) { | ||
| 352 | + return; | ||
| 353 | + } | ||
| 354 | + state.insetEndSettleWatchdogActive = true; | ||
| 355 | + state.didUserDrag = false; | ||
| 356 | + // Hold the readyToRender opacity gate until the end landing is stable, so | ||
| 357 | + // the estimated-to-measured settle chase happens before first VISIBLE | ||
| 358 | + // paint instead of in front of the user. Capped so slow measurement can | ||
| 359 | + // never hide content for long. | ||
| 360 | + state.insetEndRevealHold = true; | ||
| 361 | + let frames = 0; | ||
| 362 | + let settledFrames = 0; | ||
| 363 | + let revealStableFrames = 0; | ||
| 364 | + const releaseRevealHold = () => { | ||
| 365 | + if (state.insetEndRevealHold) { | ||
| 366 | + state.insetEndRevealHold = false; | ||
| 367 | + setInitialRenderState(ctx, {}); | ||
| 368 | + } | ||
| 369 | + }; | ||
| 370 | + // Revealing flips adaptive rendering to "normal", which re-renders rows and | ||
| 371 | + // can move the end target one more time — do that upgrade while still | ||
| 372 | + // hidden (phase 1), then require stability again before revealing (phase 2) | ||
| 373 | + // so the first visible frame is the settled one. | ||
| 374 | + let revealPhase = 1; | ||
| 375 | + const onRevealStability = () => { | ||
| 376 | + var _a3; | ||
| 377 | + if (!state.insetEndRevealHold) { | ||
| 378 | + return; | ||
| 379 | + } | ||
| 380 | + if (revealPhase === 1) { | ||
| 381 | + revealPhase = 2; | ||
| 382 | + revealStableFrames = 0; | ||
| 383 | + setAdaptiveRender(ctx, "normal"); | ||
| 384 | + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); | ||
| 385 | + return; | ||
| 386 | + } | ||
| 387 | + releaseRevealHold(); | ||
| 388 | + }; | ||
| 389 | + const tick = () => { | ||
| 390 | + // The user owns the scroll from their first drag — a re-pin here would | ||
| 391 | + // pin the list under their finger. | ||
| 392 | + if (frames++ >= INSET_END_SETTLE_WATCHDOG_FRAMES || settledFrames >= INSET_END_SETTLE_WATCHDOG_STABLE_FRAMES || state.didUserDrag || !ctx.state || ctx.state !== state) { | ||
| 393 | + state.insetEndSettleWatchdogActive = false; | ||
| 394 | + releaseRevealHold(); | ||
| 395 | + return; | ||
| 396 | + } | ||
| 397 | + if (frames >= INSET_END_REVEAL_MAX_HOLD_FRAMES) { | ||
| 398 | + releaseRevealHold(); | ||
| 399 | + } | ||
| 400 | + const insetStartAdjustment = getContentInsetStartAdjustment(ctx); | ||
| 401 | + const contentSize = getContentSize(ctx); | ||
| 402 | + const scrollLength = state.scrollLength; | ||
| 403 | + if (insetStartAdjustment > 0 && scrollLength > 0 && Number.isFinite(contentSize) && contentSize > scrollLength && !state.scrollingTo && !state.maintainingScrollAtEnd) { | ||
| 404 | + const endOffset = Math.max(-insetStartAdjustment, contentSize - scrollLength); | ||
| 405 | + const distance = endOffset - state.scroll; | ||
| 406 | + // Estimated row sizes converging to measured ones can strand the initial | ||
| 407 | + // end landing when the library's own end-anchor bookkeeping gives up. | ||
| 408 | + // While still near the end (never fighting a user who scrolled away), | ||
| 409 | + // re-pin to the current true end until sizes stop changing. | ||
| 410 | + if (Math.abs(distance) > 2 && Math.abs(distance) <= scrollLength * 0.5) { | ||
| 411 | + settledFrames = 0; | ||
| 412 | + revealStableFrames = 0; | ||
| 413 | + const scroller = state.refScroller.current; | ||
| 414 | + if (scroller) { | ||
| 415 | + scroller.scrollTo({ animated: false, x: 0, y: endOffset }); | ||
| 416 | + } | ||
| 417 | + } else { | ||
| 418 | + settledFrames++; | ||
| 419 | + revealStableFrames++; | ||
| 420 | + if (revealStableFrames >= INSET_END_REVEAL_STABLE_FRAMES) { | ||
| 421 | + onRevealStability(); | ||
| 422 | + } | ||
| 423 | + } | ||
| 424 | + } else { | ||
| 425 | + // Conditions that make re-pinning unnecessary (underflow, in-flight | ||
| 426 | + // programmatic scroll) count toward stability for the reveal. | ||
| 427 | + revealStableFrames++; | ||
| 428 | + if (revealStableFrames >= INSET_END_REVEAL_STABLE_FRAMES) { | ||
| 429 | + onRevealStability(); | ||
| 430 | + } | ||
| 431 | + } | ||
| 432 | + requestAnimationFrame(tick); | ||
| 433 | + }; | ||
| 434 | + requestAnimationFrame(tick); | ||
| 435 | +} | ||
| 436 | function dispatchInitialScroll(ctx, params) { | ||
| 437 | const { forceScroll, resolvedOffset, target, waitForCompletionFrame } = params; | ||
| 438 | + const insetStartAdjustment = getContentInsetStartAdjustment(ctx); | ||
| 439 | + if (insetStartAdjustment > 0 && ctx.state.props.data.length > 0) { | ||
| 440 | + if (ctx.state.scrollLength <= 0) { | ||
| 441 | + // The scroll length is unknown, so the resolved target is meaningless; | ||
| 442 | + // the initial-scroll machinery re-advances after layout. | ||
| 443 | + return; | ||
| 444 | + } | ||
| 445 | + if (resolvedOffset <= -insetStartAdjustment + 1 && !ctx.state.didDispatchInsetInitialScroll) { | ||
| 446 | + // Content underflows the viewport and nothing has scrolled yet: the | ||
| 447 | + // untouched native rest position (UIKit's adjustedContentInset) IS the | ||
| 448 | + // end position. Dispatching a negative scroll here would race UIKit's | ||
| 449 | + // inset application during screen attach, so just finish the session. | ||
| 450 | + finishInitialScroll(ctx, { | ||
| 451 | + resolvedOffset | ||
| 452 | + }); | ||
| 453 | + return; | ||
| 454 | + } | ||
| 455 | + ctx.state.didDispatchInsetInitialScroll = true; | ||
| 456 | + if (target.viewPosition === 1) { | ||
| 457 | + startInsetEndSettleWatchdog(ctx); | ||
| 458 | + } | ||
| 459 | + } | ||
| 460 | const requestedIndex = target.index; | ||
| 461 | const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0; | ||
| 462 | const itemSize = getItemSizeAtIndex(ctx, index); | ||
| 463 | @@ -2804,7 +2945,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { | ||
| 464 | return; | ||
| 465 | } | ||
| 466 | if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) { | ||
| 467 | - const shouldKeepEndTargetAlive = isRetargetableBottomAlignedInitialScrollTarget(initialScroll) && peek$(ctx, "isAtEnd"); | ||
| 468 | + const endTargetDistanceFromEnd = getContentSize(ctx) - state.scroll - state.scrollLength - getContentInsetEnd(ctx); | ||
| 469 | + const isNearEndForInsetList = getContentInsetStartAdjustment(ctx) > 0 && Number.isFinite(endTargetDistanceFromEnd) && endTargetDistanceFromEnd <= state.scrollLength * 0.5; | ||
| 470 | + const shouldKeepEndTargetAlive = isRetargetableBottomAlignedInitialScrollTarget(initialScroll) && (peek$(ctx, "isAtEnd") || isNearEndForInsetList); | ||
| 471 | if (!shouldKeepEndTargetAlive) { | ||
| 472 | if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) { | ||
| 473 | clearPendingInitialScrollFooterLayout(ctx, { | ||
| 474 | @@ -4646,7 +4789,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { | ||
| 475 | } | ||
| 476 | contentBelowAnchor += footerSize + stylePaddingBottom; | ||
| 477 | isReady = !hasUnknownTailSize; | ||
| 478 | - nextSize = hasUnknownTailSize ? previousSize || 0 : Math.max(0, state.scrollLength - contentBelowAnchor - anchorOffset); | ||
| 479 | + const knownSizeBound = Math.max(0, state.scrollLength - contentBelowAnchor - anchorOffset); | ||
| 480 | + nextSize = hasUnknownTailSize ? Math.min(previousSize || 0, knownSizeBound) : knownSizeBound; | ||
| 481 | } else if (anchorIndex >= 0) { | ||
| 482 | isReady = false; | ||
| 483 | } | ||
| 484 | @@ -4664,6 +4808,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { | ||
| 485 | updateScroll(ctx, state.scroll, true); | ||
| 486 | } | ||
| 487 | (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize }); | ||
| 488 | + } else if (!isReady && didSizeChange && nextSize < (previousSize || 0)) { | ||
| 489 | + set$(ctx, "anchoredEndSpaceSize", nextSize); | ||
| 490 | + (_a3 = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onSizeChanged) == null ? void 0 : _a3.call(anchoredEndSpace, nextSize); | ||
| 491 | + if (anchoredEndSpace == null ? void 0 : anchoredEndSpace.includeInEndInset) { | ||
| 492 | + updateScroll(ctx, state.scroll, true); | ||
| 493 | + } | ||
| 494 | } | ||
| 495 | return nextSize; | ||
| 496 | } | ||
| 497 | @@ -6462,6 +6612,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 498 | dataVersion, | ||
| 499 | drawDistance = 250, | ||
| 500 | contentInsetEndAdjustment, | ||
| 501 | + contentInsetStartAdjustment, | ||
| 502 | estimatedItemSize = 100, | ||
| 503 | estimatedListSize, | ||
| 504 | extraData, | ||
| 505 | @@ -6492,6 +6643,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 506 | onLayout: onLayoutProp, | ||
| 507 | onLoad, | ||
| 508 | onMomentumScrollEnd, | ||
| 509 | + onScrollBeginDrag, | ||
| 510 | onRefresh, | ||
| 511 | onScroll: onScrollProp, | ||
| 512 | onStartReached, | ||
| 513 | @@ -6577,7 +6729,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 514 | const combinedRef = useCombinedRef(refScroller, refScrollView); | ||
| 515 | const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString()); | ||
| 516 | const stickyHeaderIndices = stickyHeaderIndicesProp; | ||
| 517 | - const contentInsetEndAdjustmentResolved = Platform.OS === "web" ? contentInsetEndAdjustment : void 0; | ||
| 518 | + const contentInsetEndAdjustmentResolved = contentInsetEndAdjustment; | ||
| 519 | const previousContentInsetEndAdjustmentRef = React2.useRef(contentInsetEndAdjustmentResolved); | ||
| 520 | const alwaysRenderIndices = React2.useMemo(() => { | ||
| 521 | const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex); | ||
| 522 | @@ -6710,6 +6862,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 523 | contentContainerAlignItems: contentContainerStyle.alignItems, | ||
| 524 | contentInset, | ||
| 525 | contentInsetEndAdjustment: contentInsetEndAdjustmentResolved, | ||
| 526 | + contentInsetStartAdjustment, | ||
| 527 | data: dataProp, | ||
| 528 | dataVersion, | ||
| 529 | drawDistance, | ||
| 530 | @@ -6789,6 +6942,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 531 | return void 0; | ||
| 532 | } | ||
| 533 | const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll); | ||
| 534 | + if (getContentInsetStartAdjustment(ctx) > 0) { | ||
| 535 | + // With a native leading inset (transparent header + automatic insets), | ||
| 536 | + // any mount-time contentOffset races UIKit's adjustedContentInset | ||
| 537 | + // application. The gated initial-scroll dispatch positions the list once | ||
| 538 | + // real sizes are known instead. | ||
| 539 | + return void 0; | ||
| 540 | + } | ||
| 541 | return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset; | ||
| 542 | }, [usesBootstrapInitialScroll]); | ||
| 543 | React2.useLayoutEffect(() => { | ||
| 544 | @@ -6995,6 +7155,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 545 | onMomentumScrollEnd(event); | ||
| 546 | } | ||
| 547 | }, | ||
| 548 | + onScrollBeginDrag: (event) => { | ||
| 549 | + ctx.state.didUserDrag = true; | ||
| 550 | + if (onScrollBeginDrag) { | ||
| 551 | + onScrollBeginDrag(event); | ||
| 552 | + } | ||
| 553 | + }, | ||
| 554 | onScroll: (event) => onScroll(ctx, event) | ||
| 555 | }), | ||
| 556 | [] | ||
| 557 | @@ -7019,6 +7185,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 558 | onLayout, | ||
| 559 | onLayoutFooter, | ||
| 560 | onMomentumScrollEnd: fns.onMomentumScrollEnd, | ||
| 561 | + onScrollBeginDrag: fns.onScrollBeginDrag, | ||
| 562 | onScroll: onScrollHandler, | ||
| 563 | recycleItems, | ||
| 564 | refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2__namespace.cloneElement(refreshControlElement, { | ||
| 565 | diff --git a/react-native.mjs b/react-native.mjs | ||
| 566 | index 2e96ca7..fc88d4b 100644 | ||
| 567 | --- a/react-native.mjs | ||
| 568 | +++ b/react-native.mjs | ||
| 569 | @@ -1174,7 +1174,7 @@ function setInitialRenderState(ctx, { | ||
| 570 | if (didInitialScroll) { | ||
| 571 | state.didFinishInitialScroll = true; | ||
| 572 | } | ||
| 573 | - const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); | ||
| 574 | + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll && !state.insetEndRevealHold); | ||
| 575 | if (isReadyToRender && !peek$(ctx, "readyToRender")) { | ||
| 576 | set$(ctx, "readyToRender", true); | ||
| 577 | setAdaptiveRender(ctx, "normal"); | ||
| 578 | @@ -1459,18 +1459,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { | ||
| 579 | } | ||
| 580 | |||
| 581 | // src/core/clampScrollOffset.ts | ||
| 582 | +function getContentInsetStartAdjustment(ctx) { | ||
| 583 | + const adjustment = ctx.state.props.contentInsetStartAdjustment; | ||
| 584 | + return typeof adjustment === "number" && Number.isFinite(adjustment) ? Math.max(0, adjustment) : 0; | ||
| 585 | +} | ||
| 586 | function clampScrollOffset(ctx, offset, scrollTarget) { | ||
| 587 | const state = ctx.state; | ||
| 588 | const contentSize = getContentSize(ctx); | ||
| 589 | + const minOffset = -getContentInsetStartAdjustment(ctx); | ||
| 590 | let clampedOffset = offset; | ||
| 591 | if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android" || state.lastLayout)) { | ||
| 592 | - const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); | ||
| 593 | + const baseMaxOffset = Math.max(minOffset, contentSize - state.scrollLength); | ||
| 594 | const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; | ||
| 595 | const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; | ||
| 596 | const maxOffset = baseMaxOffset + extraEndOffset; | ||
| 597 | clampedOffset = Math.min(offset, maxOffset); | ||
| 598 | } | ||
| 599 | - clampedOffset = Math.max(0, clampedOffset); | ||
| 600 | + clampedOffset = Math.max(minOffset, clampedOffset); | ||
| 601 | return clampedOffset; | ||
| 602 | } | ||
| 603 | |||
| 604 | @@ -1605,10 +1610,10 @@ function checkFinishedScrollFrame(ctx) { | ||
| 605 | finishScrollTo(ctx); | ||
| 606 | } | ||
| 607 | } | ||
| 608 | -function scrollToFallbackOffset(ctx, offset) { | ||
| 609 | +function scrollToFallbackOffset(ctx, offset, animated) { | ||
| 610 | var _a3; | ||
| 611 | (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({ | ||
| 612 | - animated: false, | ||
| 613 | + animated: !!animated, | ||
| 614 | x: ctx.state.props.horizontal ? offset : 0, | ||
| 615 | y: ctx.state.props.horizontal ? 0 : offset | ||
| 616 | }); | ||
| 617 | @@ -1655,7 +1660,10 @@ function checkFinishedScrollFallback(ctx) { | ||
| 618 | }); | ||
| 619 | scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); | ||
| 620 | } else if (shouldRetryUnalignedEndScroll) { | ||
| 621 | - scrollToFallbackOffset(ctx, completionState.clampedTargetOffset); | ||
| 622 | + const isActivelyAnimatingToEnd = !!isStillScrollingTo.animated && Date.now() - state.scrollTime < 100; | ||
| 623 | + if (!isActivelyAnimatingToEnd) { | ||
| 624 | + scrollToFallbackOffset(ctx, completionState.clampedTargetOffset, !!isStillScrollingTo.animated); | ||
| 625 | + } | ||
| 626 | scheduleFallbackCheck(100); | ||
| 627 | } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) { | ||
| 628 | finishScrollTo(ctx); | ||
| 629 | @@ -1716,9 +1724,18 @@ function doMaintainScrollAtEnd(ctx) { | ||
| 630 | } | ||
| 631 | state.pendingMaintainScrollAtEnd = false; | ||
| 632 | if (shouldMaintainScrollAtEnd) { | ||
| 633 | + const maintainAnchoredEndSpace = state.props.anchoredEndSpace; | ||
| 634 | + const maintainAnchorIndex = maintainAnchoredEndSpace == null ? void 0 : maintainAnchoredEndSpace.anchorIndex; | ||
| 635 | + if (maintainAnchorIndex !== void 0 && maintainAnchorIndex >= 0 && maintainAnchorIndex < state.props.data.length && !areKnownOrFixedItemSizesAvailable(ctx, maintainAnchorIndex, state.props.data.length - 1)) { | ||
| 636 | + return false; | ||
| 637 | + } | ||
| 638 | const contentSize = getContentSize(ctx); | ||
| 639 | + const maintainInsetStartAdjustment = getContentInsetStartAdjustment(ctx); | ||
| 640 | if (contentSize < state.scrollLength) { | ||
| 641 | - state.scroll = 0; | ||
| 642 | + state.scroll = -maintainInsetStartAdjustment; | ||
| 643 | + if (maintainInsetStartAdjustment > 0) { | ||
| 644 | + return true; | ||
| 645 | + } | ||
| 646 | } | ||
| 647 | if (!state.maintainingScrollAtEnd) { | ||
| 648 | const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; | ||
| 649 | @@ -1738,9 +1755,18 @@ function doMaintainScrollAtEnd(ctx) { | ||
| 650 | y: 0 | ||
| 651 | }); | ||
| 652 | } else { | ||
| 653 | - scroller == null ? void 0 : scroller.scrollToEnd({ | ||
| 654 | - animated: maintainScrollAtEnd.animated | ||
| 655 | - }); | ||
| 656 | + const insetStartAdjustment = getContentInsetStartAdjustment(ctx); | ||
| 657 | + if (insetStartAdjustment > 0) { | ||
| 658 | + scroller == null ? void 0 : scroller.scrollTo({ | ||
| 659 | + animated: maintainScrollAtEnd.animated, | ||
| 660 | + x: 0, | ||
| 661 | + y: Math.max(-insetStartAdjustment, getContentSize(ctx) - state.scrollLength) | ||
| 662 | + }); | ||
| 663 | + } else { | ||
| 664 | + scroller == null ? void 0 : scroller.scrollToEnd({ | ||
| 665 | + animated: maintainScrollAtEnd.animated | ||
| 666 | + }); | ||
| 667 | + } | ||
| 668 | } | ||
| 669 | setTimeout( | ||
| 670 | () => { | ||
| 671 | @@ -1867,7 +1893,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { | ||
| 672 | if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) { | ||
| 673 | return 0; | ||
| 674 | } | ||
| 675 | - const maxScroll = Math.max(0, totalSize - state.scrollLength); | ||
| 676 | + const insetStartAdjustment = state.props.contentInsetStartAdjustment; | ||
| 677 | + const minScroll = typeof insetStartAdjustment === "number" && Number.isFinite(insetStartAdjustment) ? -Math.max(0, insetStartAdjustment) : 0; | ||
| 678 | + const maxScroll = Math.max(minScroll, totalSize - state.scrollLength); | ||
| 679 | const clampDelta = maxScroll - state.scroll; | ||
| 680 | if (unresolvedAmount < 0) { | ||
| 681 | return Math.max(unresolvedAmount, Math.min(0, clampDelta)); | ||
| 682 | @@ -1929,7 +1957,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { | ||
| 683 | settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta); | ||
| 684 | return true; | ||
| 685 | } | ||
| 686 | - const expectedNativeClampScroll = Math.max(0, getContentSize(ctx) - state.scrollLength); | ||
| 687 | + const expectedNativeClampScroll = Math.max(-getContentInsetStartAdjustment(ctx), getContentSize(ctx) - state.scrollLength); | ||
| 688 | const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll); | ||
| 689 | const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON; | ||
| 690 | if (isAtExpectedNativeClamp) { | ||
| 691 | @@ -2062,7 +2090,7 @@ function prepareMVCP(ctx, dataChanged) { | ||
| 692 | if (diff > 0) { | ||
| 693 | diff = Math.max(0, totalSize - state.scroll - state.scrollLength); | ||
| 694 | } else { | ||
| 695 | - const maxScroll = Math.max(0, totalSize - state.scrollLength); | ||
| 696 | + const maxScroll = Math.max(-getContentInsetStartAdjustment(ctx), totalSize - state.scrollLength); | ||
| 697 | state.scroll = maxScroll; | ||
| 698 | state.scrollPending = maxScroll; | ||
| 699 | diff = 0; | ||
| 700 | @@ -2353,8 +2381,121 @@ function scrollToIndex(ctx, { | ||
| 701 | } | ||
| 702 | |||
| 703 | // src/core/initialScroll.ts | ||
| 704 | +var INSET_END_SETTLE_WATCHDOG_FRAMES = 150; | ||
| 705 | +var INSET_END_SETTLE_WATCHDOG_STABLE_FRAMES = 20; | ||
| 706 | +var INSET_END_REVEAL_STABLE_FRAMES = 3; | ||
| 707 | +var INSET_END_REVEAL_MAX_HOLD_FRAMES = 40; | ||
| 708 | +function startInsetEndSettleWatchdog(ctx) { | ||
| 709 | + const state = ctx.state; | ||
| 710 | + if (state.insetEndSettleWatchdogActive) { | ||
| 711 | + return; | ||
| 712 | + } | ||
| 713 | + state.insetEndSettleWatchdogActive = true; | ||
| 714 | + state.didUserDrag = false; | ||
| 715 | + // Hold the readyToRender opacity gate until the end landing is stable, so | ||
| 716 | + // the estimated-to-measured settle chase happens before first VISIBLE | ||
| 717 | + // paint instead of in front of the user. Capped so slow measurement can | ||
| 718 | + // never hide content for long. | ||
| 719 | + state.insetEndRevealHold = true; | ||
| 720 | + let frames = 0; | ||
| 721 | + let settledFrames = 0; | ||
| 722 | + let revealStableFrames = 0; | ||
| 723 | + const releaseRevealHold = () => { | ||
| 724 | + if (state.insetEndRevealHold) { | ||
| 725 | + state.insetEndRevealHold = false; | ||
| 726 | + setInitialRenderState(ctx, {}); | ||
| 727 | + } | ||
| 728 | + }; | ||
| 729 | + // Revealing flips adaptive rendering to "normal", which re-renders rows and | ||
| 730 | + // can move the end target one more time — do that upgrade while still | ||
| 731 | + // hidden (phase 1), then require stability again before revealing (phase 2) | ||
| 732 | + // so the first visible frame is the settled one. | ||
| 733 | + let revealPhase = 1; | ||
| 734 | + const onRevealStability = () => { | ||
| 735 | + var _a3; | ||
| 736 | + if (!state.insetEndRevealHold) { | ||
| 737 | + return; | ||
| 738 | + } | ||
| 739 | + if (revealPhase === 1) { | ||
| 740 | + revealPhase = 2; | ||
| 741 | + revealStableFrames = 0; | ||
| 742 | + setAdaptiveRender(ctx, "normal"); | ||
| 743 | + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); | ||
| 744 | + return; | ||
| 745 | + } | ||
| 746 | + releaseRevealHold(); | ||
| 747 | + }; | ||
| 748 | + const tick = () => { | ||
| 749 | + // The user owns the scroll from their first drag — a re-pin here would | ||
| 750 | + // pin the list under their finger. | ||
| 751 | + if (frames++ >= INSET_END_SETTLE_WATCHDOG_FRAMES || settledFrames >= INSET_END_SETTLE_WATCHDOG_STABLE_FRAMES || state.didUserDrag || !ctx.state || ctx.state !== state) { | ||
| 752 | + state.insetEndSettleWatchdogActive = false; | ||
| 753 | + releaseRevealHold(); | ||
| 754 | + return; | ||
| 755 | + } | ||
| 756 | + if (frames >= INSET_END_REVEAL_MAX_HOLD_FRAMES) { | ||
| 757 | + releaseRevealHold(); | ||
| 758 | + } | ||
| 759 | + const insetStartAdjustment = getContentInsetStartAdjustment(ctx); | ||
| 760 | + const contentSize = getContentSize(ctx); | ||
| 761 | + const scrollLength = state.scrollLength; | ||
| 762 | + if (insetStartAdjustment > 0 && scrollLength > 0 && Number.isFinite(contentSize) && contentSize > scrollLength && !state.scrollingTo && !state.maintainingScrollAtEnd) { | ||
| 763 | + const endOffset = Math.max(-insetStartAdjustment, contentSize - scrollLength); | ||
| 764 | + const distance = endOffset - state.scroll; | ||
| 765 | + // Estimated row sizes converging to measured ones can strand the initial | ||
| 766 | + // end landing when the library's own end-anchor bookkeeping gives up. | ||
| 767 | + // While still near the end (never fighting a user who scrolled away), | ||
| 768 | + // re-pin to the current true end until sizes stop changing. | ||
| 769 | + if (Math.abs(distance) > 2 && Math.abs(distance) <= scrollLength * 0.5) { | ||
| 770 | + settledFrames = 0; | ||
| 771 | + revealStableFrames = 0; | ||
| 772 | + const scroller = state.refScroller.current; | ||
| 773 | + if (scroller) { | ||
| 774 | + scroller.scrollTo({ animated: false, x: 0, y: endOffset }); | ||
| 775 | + } | ||
| 776 | + } else { | ||
| 777 | + settledFrames++; | ||
| 778 | + revealStableFrames++; | ||
| 779 | + if (revealStableFrames >= INSET_END_REVEAL_STABLE_FRAMES) { | ||
| 780 | + onRevealStability(); | ||
| 781 | + } | ||
| 782 | + } | ||
| 783 | + } else { | ||
| 784 | + // Conditions that make re-pinning unnecessary (underflow, in-flight | ||
| 785 | + // programmatic scroll) count toward stability for the reveal. | ||
| 786 | + revealStableFrames++; | ||
| 787 | + if (revealStableFrames >= INSET_END_REVEAL_STABLE_FRAMES) { | ||
| 788 | + onRevealStability(); | ||
| 789 | + } | ||
| 790 | + } | ||
| 791 | + requestAnimationFrame(tick); | ||
| 792 | + }; | ||
| 793 | + requestAnimationFrame(tick); | ||
| 794 | +} | ||
| 795 | function dispatchInitialScroll(ctx, params) { | ||
| 796 | const { forceScroll, resolvedOffset, target, waitForCompletionFrame } = params; | ||
| 797 | + const insetStartAdjustment = getContentInsetStartAdjustment(ctx); | ||
| 798 | + if (insetStartAdjustment > 0 && ctx.state.props.data.length > 0) { | ||
| 799 | + if (ctx.state.scrollLength <= 0) { | ||
| 800 | + // The scroll length is unknown, so the resolved target is meaningless; | ||
| 801 | + // the initial-scroll machinery re-advances after layout. | ||
| 802 | + return; | ||
| 803 | + } | ||
| 804 | + if (resolvedOffset <= -insetStartAdjustment + 1 && !ctx.state.didDispatchInsetInitialScroll) { | ||
| 805 | + // Content underflows the viewport and nothing has scrolled yet: the | ||
| 806 | + // untouched native rest position (UIKit's adjustedContentInset) IS the | ||
| 807 | + // end position. Dispatching a negative scroll here would race UIKit's | ||
| 808 | + // inset application during screen attach, so just finish the session. | ||
| 809 | + finishInitialScroll(ctx, { | ||
| 810 | + resolvedOffset | ||
| 811 | + }); | ||
| 812 | + return; | ||
| 813 | + } | ||
| 814 | + ctx.state.didDispatchInsetInitialScroll = true; | ||
| 815 | + if (target.viewPosition === 1) { | ||
| 816 | + startInsetEndSettleWatchdog(ctx); | ||
| 817 | + } | ||
| 818 | + } | ||
| 819 | const requestedIndex = target.index; | ||
| 820 | const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0; | ||
| 821 | const itemSize = getItemSizeAtIndex(ctx, index); | ||
| 822 | @@ -2783,7 +2924,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { | ||
| 823 | return; | ||
| 824 | } | ||
| 825 | if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) { | ||
| 826 | - const shouldKeepEndTargetAlive = isRetargetableBottomAlignedInitialScrollTarget(initialScroll) && peek$(ctx, "isAtEnd"); | ||
| 827 | + const endTargetDistanceFromEnd = getContentSize(ctx) - state.scroll - state.scrollLength - getContentInsetEnd(ctx); | ||
| 828 | + const isNearEndForInsetList = getContentInsetStartAdjustment(ctx) > 0 && Number.isFinite(endTargetDistanceFromEnd) && endTargetDistanceFromEnd <= state.scrollLength * 0.5; | ||
| 829 | + const shouldKeepEndTargetAlive = isRetargetableBottomAlignedInitialScrollTarget(initialScroll) && (peek$(ctx, "isAtEnd") || isNearEndForInsetList); | ||
| 830 | if (!shouldKeepEndTargetAlive) { | ||
| 831 | if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) { | ||
| 832 | clearPendingInitialScrollFooterLayout(ctx, { | ||
| 833 | @@ -4625,7 +4768,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { | ||
| 834 | } | ||
| 835 | contentBelowAnchor += footerSize + stylePaddingBottom; | ||
| 836 | isReady = !hasUnknownTailSize; | ||
| 837 | - nextSize = hasUnknownTailSize ? previousSize || 0 : Math.max(0, state.scrollLength - contentBelowAnchor - anchorOffset); | ||
| 838 | + const knownSizeBound = Math.max(0, state.scrollLength - contentBelowAnchor - anchorOffset); | ||
| 839 | + nextSize = hasUnknownTailSize ? Math.min(previousSize || 0, knownSizeBound) : knownSizeBound; | ||
| 840 | } else if (anchorIndex >= 0) { | ||
| 841 | isReady = false; | ||
| 842 | } | ||
| 843 | @@ -4643,6 +4787,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { | ||
| 844 | updateScroll(ctx, state.scroll, true); | ||
| 845 | } | ||
| 846 | (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize }); | ||
| 847 | + } else if (!isReady && didSizeChange && nextSize < (previousSize || 0)) { | ||
| 848 | + set$(ctx, "anchoredEndSpaceSize", nextSize); | ||
| 849 | + (_a3 = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onSizeChanged) == null ? void 0 : _a3.call(anchoredEndSpace, nextSize); | ||
| 850 | + if (anchoredEndSpace == null ? void 0 : anchoredEndSpace.includeInEndInset) { | ||
| 851 | + updateScroll(ctx, state.scroll, true); | ||
| 852 | + } | ||
| 853 | } | ||
| 854 | return nextSize; | ||
| 855 | } | ||
| 856 | @@ -6441,6 +6591,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 857 | dataVersion, | ||
| 858 | drawDistance = 250, | ||
| 859 | contentInsetEndAdjustment, | ||
| 860 | + contentInsetStartAdjustment, | ||
| 861 | estimatedItemSize = 100, | ||
| 862 | estimatedListSize, | ||
| 863 | extraData, | ||
| 864 | @@ -6471,6 +6622,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 865 | onLayout: onLayoutProp, | ||
| 866 | onLoad, | ||
| 867 | onMomentumScrollEnd, | ||
| 868 | + onScrollBeginDrag, | ||
| 869 | onRefresh, | ||
| 870 | onScroll: onScrollProp, | ||
| 871 | onStartReached, | ||
| 872 | @@ -6556,7 +6708,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 873 | const combinedRef = useCombinedRef(refScroller, refScrollView); | ||
| 874 | const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString()); | ||
| 875 | const stickyHeaderIndices = stickyHeaderIndicesProp; | ||
| 876 | - const contentInsetEndAdjustmentResolved = Platform.OS === "web" ? contentInsetEndAdjustment : void 0; | ||
| 877 | + const contentInsetEndAdjustmentResolved = contentInsetEndAdjustment; | ||
| 878 | const previousContentInsetEndAdjustmentRef = useRef(contentInsetEndAdjustmentResolved); | ||
| 879 | const alwaysRenderIndices = useMemo(() => { | ||
| 880 | const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex); | ||
| 881 | @@ -6689,6 +6841,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 882 | contentContainerAlignItems: contentContainerStyle.alignItems, | ||
| 883 | contentInset, | ||
| 884 | contentInsetEndAdjustment: contentInsetEndAdjustmentResolved, | ||
| 885 | + contentInsetStartAdjustment, | ||
| 886 | data: dataProp, | ||
| 887 | dataVersion, | ||
| 888 | drawDistance, | ||
| 889 | @@ -6768,6 +6921,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 890 | return void 0; | ||
| 891 | } | ||
| 892 | const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll); | ||
| 893 | + if (getContentInsetStartAdjustment(ctx) > 0) { | ||
| 894 | + // With a native leading inset (transparent header + automatic insets), | ||
| 895 | + // any mount-time contentOffset races UIKit's adjustedContentInset | ||
| 896 | + // application. The gated initial-scroll dispatch positions the list once | ||
| 897 | + // real sizes are known instead. | ||
| 898 | + return void 0; | ||
| 899 | + } | ||
| 900 | return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset; | ||
| 901 | }, [usesBootstrapInitialScroll]); | ||
| 902 | useLayoutEffect(() => { | ||
| 903 | @@ -6974,6 +7134,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 904 | onMomentumScrollEnd(event); | ||
| 905 | } | ||
| 906 | }, | ||
| 907 | + onScrollBeginDrag: (event) => { | ||
| 908 | + ctx.state.didUserDrag = true; | ||
| 909 | + if (onScrollBeginDrag) { | ||
| 910 | + onScrollBeginDrag(event); | ||
| 911 | + } | ||
| 912 | + }, | ||
| 913 | onScroll: (event) => onScroll(ctx, event) | ||
| 914 | }), | ||
| 915 | [] | ||
| 916 | @@ -6998,6 +7164,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 917 | onLayout, | ||
| 918 | onLayoutFooter, | ||
| 919 | onMomentumScrollEnd: fns.onMomentumScrollEnd, | ||
| 920 | + onScrollBeginDrag: fns.onScrollBeginDrag, | ||
| 921 | onScroll: onScrollHandler, | ||
| 922 | recycleItems, | ||
| 923 | refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2.cloneElement(refreshControlElement, { | ||
| 924 | diff --git a/reanimated.d.ts b/reanimated.d.ts | ||
| 925 | index 7e2d11f..d5b0d66 100644 | ||
| 926 | --- a/reanimated.d.ts | ||
| 927 | +++ b/reanimated.d.ts | ||
| 928 | @@ -285,6 +285,12 @@ interface LegendListSpecificProps<ItemT, TItemType extends string | undefined> { | ||
| 929 | * The adjustment is also rendered as real content padding so the browser scroll range includes it. | ||
| 930 | */ | ||
| 931 | contentInsetEndAdjustment?: number; | ||
| 932 | + /** | ||
| 933 | + * Width/height of a leading content inset applied natively outside the list's knowledge | ||
| 934 | + * (e.g. iOS contentInsetAdjustmentBehavior="automatic" under a transparent header). | ||
| 935 | + * Programmatic scrolls clamp to -adjustment instead of 0 so content can rest below the header. | ||
| 936 | + */ | ||
| 937 | + contentInsetStartAdjustment?: number; | ||
| 938 | /** | ||
| 939 | * Number of columns to render items in. | ||
| 940 | * @default 1 | ||
patches/@legendapp__list@3.3.3.patch created+1014| ... | @@ -0,0 +1,1014 @@ | ||
| 1 | diff --git a/keyboard.d.ts b/keyboard.d.ts | ||
| 2 | index 7bc3bb8..75ec120 100644 | ||
| 3 | --- a/keyboard.d.ts | ||
| 4 | +++ b/keyboard.d.ts | ||
| 5 | @@ -277,7 +277,7 @@ type KeyboardChatComposerInsetListRef = { | ||
| 6 | type KeyboardChatComposerRef = { | ||
| 7 | current: Pick<View, "measure"> | null; | ||
| 8 | }; | ||
| 9 | -declare function useKeyboardChatComposerInset(listRef: KeyboardChatComposerInsetListRef, composerRef: KeyboardChatComposerRef, initialHeight?: number): { | ||
| 10 | +declare function useKeyboardChatComposerInset(listRef: KeyboardChatComposerInsetListRef, composerRef: KeyboardChatComposerRef, initialHeight?: number, heightAdjustment?: number): { | ||
| 11 | contentInsetEndAdjustment: SharedValue<number>; | ||
| 12 | onComposerLayout: (event: LayoutChangeEvent) => void; | ||
| 13 | }; | ||
| 14 | @@ -286,8 +286,10 @@ declare function useKeyboardScrollToEnd({ freeze: freezeProp, listRef }: UseKeyb | ||
| 15 | scrollMessageToEnd: ({ animated, closeKeyboard }: ScrollMessageToEndOptions) => Promise<void>; | ||
| 16 | }; | ||
| 17 | declare const KeyboardAwareLegendList: <ItemT>(props: Omit<AnimatedLegendListProps<ItemT>, "anchoredEndSpace" | "contentInsetEndAdjustment" | "renderScrollComponent"> & KeyboardChatScrollViewPropsUnique & { | ||
| 18 | + adjustedInsetCompensation?: number; | ||
| 19 | anchoredEndSpace?: AnchoredEndSpaceConfig; | ||
| 20 | contentInsetEndAdjustment?: SharedValue<number>; | ||
| 21 | + contentInsetEndStaticAdjustment?: number; | ||
| 22 | keyboardOffset?: number; | ||
| 23 | } & React.RefAttributes<LegendListRef>) => React.ReactElement | null; | ||
| 24 | |||
| 25 | diff --git a/keyboard.js b/keyboard.js | ||
| 26 | index 736286a..8218172 100644 | ||
| 27 | --- a/keyboard.js | ||
| 28 | +++ b/keyboard.js | ||
| 29 | @@ -33,19 +33,19 @@ if (typeof __DEV__ !== "undefined" && __DEV__ && !reactNativeKeyboardController. | ||
| 30 | "[legend-list] KeyboardAwareLegendList requires a recent react-native-keyboard-controller with KeyboardChatScrollView. Please upgrade react-native-keyboard-controller to at least 1.21.7." | ||
| 31 | ); | ||
| 32 | } | ||
| 33 | -function useKeyboardChatComposerInset(listRef, composerRef, initialHeight = 0) { | ||
| 34 | +function useKeyboardChatComposerInset(listRef, composerRef, initialHeight = 0, heightAdjustment = 0) { | ||
| 35 | const contentInsetEndAdjustment = reactNativeReanimated.useSharedValue(initialHeight); | ||
| 36 | const lastHeightRef = React.useRef(void 0); | ||
| 37 | const reportHeight = React.useCallback( | ||
| 38 | - (height) => { | ||
| 39 | + (rawHeight) => { | ||
| 40 | + const height = Math.max(0, rawHeight + heightAdjustment); | ||
| 41 | var _a; | ||
| 42 | if (Number.isFinite(height) && height !== lastHeightRef.current) { | ||
| 43 | lastHeightRef.current = height; | ||
| 44 | contentInsetEndAdjustment.value = height; | ||
| 45 | - (_a = listRef.current) == null ? void 0 : _a.reportContentInset({ bottom: height }); | ||
| 46 | } | ||
| 47 | }, | ||
| 48 | - [contentInsetEndAdjustment, listRef] | ||
| 49 | + [contentInsetEndAdjustment, heightAdjustment, listRef] | ||
| 50 | ); | ||
| 51 | React.useLayoutEffect(() => { | ||
| 52 | var _a; | ||
| 53 | @@ -84,9 +84,11 @@ function useKeyboardScrollToEnd({ freeze: freezeProp, listRef }) { | ||
| 54 | } | ||
| 55 | var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2(props, forwardedRef) { | ||
| 56 | const { | ||
| 57 | + adjustedInsetCompensation, | ||
| 58 | anchoredEndSpace, | ||
| 59 | applyWorkaroundForContentInsetHitTestBug, | ||
| 60 | contentInsetEndAdjustment, | ||
| 61 | + contentInsetEndStaticAdjustment, | ||
| 62 | freeze, | ||
| 63 | keyboardLiftBehavior, | ||
| 64 | keyboardOffset, | ||
| 65 | @@ -109,11 +111,15 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | ||
| 66 | includeInEndInset: true, | ||
| 67 | onSizeChanged: (size) => { | ||
| 68 | var _a; | ||
| 69 | - blankSpace.value = size; | ||
| 70 | + // The anchored blank is sized against the ADJUSTED viewport, but the | ||
| 71 | + // scroll component writes it as a raw contentInset that UIKit tops up | ||
| 72 | + // with the safe-area bottom — write it net of that or the end rest | ||
| 73 | + // (and the anchored message) sinks one safe-area under the header. | ||
| 74 | + blankSpace.value = size > 0 ? Math.max(0, size - (adjustedInsetCompensation || 0)) : 0; | ||
| 75 | (_a = anchoredEndSpace.onSizeChanged) == null ? void 0 : _a.call(anchoredEndSpace, size); | ||
| 76 | } | ||
| 77 | }; | ||
| 78 | - }, [anchoredEndSpace, blankSpace]); | ||
| 79 | + }, [adjustedInsetCompensation, anchoredEndSpace, blankSpace]); | ||
| 80 | const onContentInsetChange = React.useCallback((insets) => { | ||
| 81 | var _a; | ||
| 82 | (_a = refLegendList.current) == null ? void 0 : _a.reportContentInset(insets); | ||
| 83 | @@ -124,6 +130,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | ||
| 84 | reactNativeKeyboardController.KeyboardChatScrollView, | ||
| 85 | { | ||
| 86 | ...scrollProps, | ||
| 87 | + adjustedInsetCompensation, | ||
| 88 | applyWorkaroundForContentInsetHitTestBug, | ||
| 89 | blankSpace, | ||
| 90 | extraContentPadding: contentInsetEndAdjustment, | ||
| 91 | @@ -135,6 +142,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | ||
| 92 | ); | ||
| 93 | }, | ||
| 94 | [ | ||
| 95 | + adjustedInsetCompensation, | ||
| 96 | applyWorkaroundForContentInsetHitTestBug, | ||
| 97 | blankSpace, | ||
| 98 | contentInsetEndAdjustment, | ||
| 99 | @@ -149,6 +157,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | ||
| 100 | AnimatedLegendListInternal, | ||
| 101 | { | ||
| 102 | anchoredEndSpace: anchoredEndSpaceWithBlankSpace, | ||
| 103 | + contentInsetEndAdjustment: contentInsetEndStaticAdjustment, | ||
| 104 | ref: combinedRef, | ||
| 105 | renderScrollComponent: memoList, | ||
| 106 | ...rest | ||
| 107 | diff --git a/keyboard.mjs b/keyboard.mjs | ||
| 108 | index c1dd270..cb0d142 100644 | ||
| 109 | --- a/keyboard.mjs | ||
| 110 | +++ b/keyboard.mjs | ||
| 111 | @@ -12,19 +12,19 @@ if (typeof __DEV__ !== "undefined" && __DEV__ && !KeyboardChatScrollView) { | ||
| 112 | "[legend-list] KeyboardAwareLegendList requires a recent react-native-keyboard-controller with KeyboardChatScrollView. Please upgrade react-native-keyboard-controller to at least 1.21.7." | ||
| 113 | ); | ||
| 114 | } | ||
| 115 | -function useKeyboardChatComposerInset(listRef, composerRef, initialHeight = 0) { | ||
| 116 | +function useKeyboardChatComposerInset(listRef, composerRef, initialHeight = 0, heightAdjustment = 0) { | ||
| 117 | const contentInsetEndAdjustment = useSharedValue(initialHeight); | ||
| 118 | const lastHeightRef = useRef(void 0); | ||
| 119 | const reportHeight = useCallback( | ||
| 120 | - (height) => { | ||
| 121 | + (rawHeight) => { | ||
| 122 | + const height = Math.max(0, rawHeight + heightAdjustment); | ||
| 123 | var _a; | ||
| 124 | if (Number.isFinite(height) && height !== lastHeightRef.current) { | ||
| 125 | lastHeightRef.current = height; | ||
| 126 | contentInsetEndAdjustment.value = height; | ||
| 127 | - (_a = listRef.current) == null ? void 0 : _a.reportContentInset({ bottom: height }); | ||
| 128 | } | ||
| 129 | }, | ||
| 130 | - [contentInsetEndAdjustment, listRef] | ||
| 131 | + [contentInsetEndAdjustment, heightAdjustment, listRef] | ||
| 132 | ); | ||
| 133 | useLayoutEffect(() => { | ||
| 134 | var _a; | ||
| 135 | @@ -63,9 +63,11 @@ function useKeyboardScrollToEnd({ freeze: freezeProp, listRef }) { | ||
| 136 | } | ||
| 137 | var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2(props, forwardedRef) { | ||
| 138 | const { | ||
| 139 | + adjustedInsetCompensation, | ||
| 140 | anchoredEndSpace, | ||
| 141 | applyWorkaroundForContentInsetHitTestBug, | ||
| 142 | contentInsetEndAdjustment, | ||
| 143 | + contentInsetEndStaticAdjustment, | ||
| 144 | freeze, | ||
| 145 | keyboardLiftBehavior, | ||
| 146 | keyboardOffset, | ||
| 147 | @@ -88,11 +90,15 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | ||
| 148 | includeInEndInset: true, | ||
| 149 | onSizeChanged: (size) => { | ||
| 150 | var _a; | ||
| 151 | - blankSpace.value = size; | ||
| 152 | + // The anchored blank is sized against the ADJUSTED viewport, but the | ||
| 153 | + // scroll component writes it as a raw contentInset that UIKit tops up | ||
| 154 | + // with the safe-area bottom — write it net of that or the end rest | ||
| 155 | + // (and the anchored message) sinks one safe-area under the header. | ||
| 156 | + blankSpace.value = size > 0 ? Math.max(0, size - (adjustedInsetCompensation || 0)) : 0; | ||
| 157 | (_a = anchoredEndSpace.onSizeChanged) == null ? void 0 : _a.call(anchoredEndSpace, size); | ||
| 158 | } | ||
| 159 | }; | ||
| 160 | - }, [anchoredEndSpace, blankSpace]); | ||
| 161 | + }, [adjustedInsetCompensation, anchoredEndSpace, blankSpace]); | ||
| 162 | const onContentInsetChange = useCallback((insets) => { | ||
| 163 | var _a; | ||
| 164 | (_a = refLegendList.current) == null ? void 0 : _a.reportContentInset(insets); | ||
| 165 | @@ -103,6 +109,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | ||
| 166 | KeyboardChatScrollView, | ||
| 167 | { | ||
| 168 | ...scrollProps, | ||
| 169 | + adjustedInsetCompensation, | ||
| 170 | applyWorkaroundForContentInsetHitTestBug, | ||
| 171 | blankSpace, | ||
| 172 | extraContentPadding: contentInsetEndAdjustment, | ||
| 173 | @@ -114,6 +121,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | ||
| 174 | ); | ||
| 175 | }, | ||
| 176 | [ | ||
| 177 | + adjustedInsetCompensation, | ||
| 178 | applyWorkaroundForContentInsetHitTestBug, | ||
| 179 | blankSpace, | ||
| 180 | contentInsetEndAdjustment, | ||
| 181 | @@ -128,6 +136,7 @@ var KeyboardAwareLegendList = typedForwardRef(function KeyboardAwareLegendList2( | ||
| 182 | AnimatedLegendListInternal, | ||
| 183 | { | ||
| 184 | anchoredEndSpace: anchoredEndSpaceWithBlankSpace, | ||
| 185 | + contentInsetEndAdjustment: contentInsetEndStaticAdjustment, | ||
| 186 | ref: combinedRef, | ||
| 187 | renderScrollComponent: memoList, | ||
| 188 | ...rest | ||
| 189 | diff --git a/react-native.d.ts b/react-native.d.ts | ||
| 190 | index 8204015..cdeaab7 100644 | ||
| 191 | --- a/react-native.d.ts | ||
| 192 | +++ b/react-native.d.ts | ||
| 193 | @@ -293,6 +293,12 @@ interface LegendListSpecificProps<ItemT, TItemType extends string | undefined> { | ||
| 194 | * The adjustment is also rendered as real content padding so the browser scroll range includes it. | ||
| 195 | */ | ||
| 196 | contentInsetEndAdjustment?: number; | ||
| 197 | + /** | ||
| 198 | + * Width/height of a leading content inset applied natively outside the list's knowledge | ||
| 199 | + * (e.g. iOS contentInsetAdjustmentBehavior="automatic" under a transparent header). | ||
| 200 | + * Programmatic scrolls clamp to -adjustment instead of 0 so content can rest below the header. | ||
| 201 | + */ | ||
| 202 | + contentInsetStartAdjustment?: number; | ||
| 203 | /** | ||
| 204 | * Number of columns to render items in. | ||
| 205 | * @default 1 | ||
| 206 | diff --git a/react-native.js b/react-native.js | ||
| 207 | index 229f09a..2a1ceb6 100644 | ||
| 208 | --- a/react-native.js | ||
| 209 | +++ b/react-native.js | ||
| 210 | @@ -930,7 +930,7 @@ function setInitialRenderState(ctx, { | ||
| 211 | if (didInitialScroll) { | ||
| 212 | state.didFinishInitialScroll = true; | ||
| 213 | } | ||
| 214 | - const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); | ||
| 215 | + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll && !state.insetEndRevealHold); | ||
| 216 | if (isReadyToRender && !peek$(ctx, "readyToRender")) { | ||
| 217 | set$(ctx, "readyToRender", true); | ||
| 218 | setAdaptiveRender(ctx, "normal", "ready"); | ||
| 219 | @@ -1259,18 +1259,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { | ||
| 220 | } | ||
| 221 | |||
| 222 | // src/core/clampScrollOffset.ts | ||
| 223 | +function getContentInsetStartAdjustment(ctx) { | ||
| 224 | + const adjustment = ctx.state.props.contentInsetStartAdjustment; | ||
| 225 | + return typeof adjustment === "number" && Number.isFinite(adjustment) ? Math.max(0, adjustment) : 0; | ||
| 226 | +} | ||
| 227 | function clampScrollOffset(ctx, offset, scrollTarget) { | ||
| 228 | const state = ctx.state; | ||
| 229 | const contentSize = getContentSize(ctx); | ||
| 230 | + const minOffset = -getContentInsetStartAdjustment(ctx); | ||
| 231 | let clampedOffset = offset; | ||
| 232 | if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android" || state.lastLayout)) { | ||
| 233 | - const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); | ||
| 234 | + const baseMaxOffset = Math.max(minOffset, contentSize - state.scrollLength); | ||
| 235 | const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; | ||
| 236 | const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; | ||
| 237 | const maxOffset = baseMaxOffset + extraEndOffset; | ||
| 238 | clampedOffset = Math.min(offset, maxOffset); | ||
| 239 | } | ||
| 240 | - clampedOffset = Math.max(0, clampedOffset); | ||
| 241 | + clampedOffset = Math.max(minOffset, clampedOffset); | ||
| 242 | return clampedOffset; | ||
| 243 | } | ||
| 244 | |||
| 245 | @@ -1406,10 +1411,10 @@ function checkFinishedScrollFrame(ctx) { | ||
| 246 | finishScrollTo(ctx); | ||
| 247 | } | ||
| 248 | } | ||
| 249 | -function scrollToFallbackOffset(ctx, offset) { | ||
| 250 | +function scrollToFallbackOffset(ctx, offset, animated) { | ||
| 251 | var _a3; | ||
| 252 | (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({ | ||
| 253 | - animated: false, | ||
| 254 | + animated: !!animated, | ||
| 255 | x: ctx.state.props.horizontal ? offset : 0, | ||
| 256 | y: ctx.state.props.horizontal ? 0 : offset | ||
| 257 | }); | ||
| 258 | @@ -1456,7 +1461,10 @@ function checkFinishedScrollFallback(ctx) { | ||
| 259 | }); | ||
| 260 | scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); | ||
| 261 | } else if (shouldRetryUnalignedEndScroll) { | ||
| 262 | - scrollToFallbackOffset(ctx, completionState.clampedTargetOffset); | ||
| 263 | + const isActivelyAnimatingToEnd = !!isStillScrollingTo.animated && Date.now() - state.scrollTime < 100; | ||
| 264 | + if (!isActivelyAnimatingToEnd) { | ||
| 265 | + scrollToFallbackOffset(ctx, completionState.clampedTargetOffset, !!isStillScrollingTo.animated); | ||
| 266 | + } | ||
| 267 | scheduleFallbackCheck(100); | ||
| 268 | } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) { | ||
| 269 | finishScrollTo(ctx); | ||
| 270 | @@ -1517,9 +1525,18 @@ function doMaintainScrollAtEnd(ctx) { | ||
| 271 | } | ||
| 272 | if (shouldMaintainScrollAtEnd) { | ||
| 273 | state.pendingMaintainScrollAtEnd = false; | ||
| 274 | + const maintainAnchoredEndSpace = state.props.anchoredEndSpace; | ||
| 275 | + const maintainAnchorIndex = maintainAnchoredEndSpace == null ? void 0 : maintainAnchoredEndSpace.anchorIndex; | ||
| 276 | + if (maintainAnchorIndex !== void 0 && maintainAnchorIndex >= 0 && maintainAnchorIndex < state.props.data.length && !areKnownOrFixedItemSizesAvailable(ctx, maintainAnchorIndex, state.props.data.length - 1)) { | ||
| 277 | + return false; | ||
| 278 | + } | ||
| 279 | const contentSize = getContentSize(ctx); | ||
| 280 | + const maintainInsetStartAdjustment = getContentInsetStartAdjustment(ctx); | ||
| 281 | if (contentSize < state.scrollLength) { | ||
| 282 | - state.scroll = 0; | ||
| 283 | + state.scroll = -maintainInsetStartAdjustment; | ||
| 284 | + if (maintainInsetStartAdjustment > 0) { | ||
| 285 | + return true; | ||
| 286 | + } | ||
| 287 | } | ||
| 288 | if (!state.maintainingScrollAtEnd) { | ||
| 289 | const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; | ||
| 290 | @@ -1539,9 +1556,18 @@ function doMaintainScrollAtEnd(ctx) { | ||
| 291 | y: 0 | ||
| 292 | }); | ||
| 293 | } else { | ||
| 294 | - scroller == null ? void 0 : scroller.scrollToEnd({ | ||
| 295 | - animated: maintainScrollAtEnd.animated | ||
| 296 | - }); | ||
| 297 | + const insetStartAdjustment = getContentInsetStartAdjustment(ctx); | ||
| 298 | + if (insetStartAdjustment > 0) { | ||
| 299 | + scroller == null ? void 0 : scroller.scrollTo({ | ||
| 300 | + animated: maintainScrollAtEnd.animated, | ||
| 301 | + x: 0, | ||
| 302 | + y: Math.max(-insetStartAdjustment, getContentSize(ctx) - state.scrollLength) | ||
| 303 | + }); | ||
| 304 | + } else { | ||
| 305 | + scroller == null ? void 0 : scroller.scrollToEnd({ | ||
| 306 | + animated: maintainScrollAtEnd.animated | ||
| 307 | + }); | ||
| 308 | + } | ||
| 309 | } | ||
| 310 | setTimeout( | ||
| 311 | () => { | ||
| 312 | @@ -1571,6 +1597,10 @@ function doMaintainScrollAtEnd(ctx) { | ||
| 313 | function requestAdjust(ctx, positionDiff, dataChanged) { | ||
| 314 | const state = ctx.state; | ||
| 315 | if (Math.abs(positionDiff) > 0.1) { | ||
| 316 | + // Timestamp for ReanimatedPositionView: repositions caused by an MVCP | ||
| 317 | + // size adjustment are already compensated by a contentOffset shift, so | ||
| 318 | + // animating them would make rows visibly lurch and slide back. | ||
| 319 | + state.lastMVCPAdjustTime = Date.now(); | ||
| 320 | const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff; | ||
| 321 | const doit = () => { | ||
| 322 | if (needsScrollWorkaround) { | ||
| 323 | @@ -1674,7 +1704,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { | ||
| 324 | if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) { | ||
| 325 | return 0; | ||
| 326 | } | ||
| 327 | - const maxScroll = Math.max(0, totalSize - state.scrollLength); | ||
| 328 | + const insetStartAdjustment = state.props.contentInsetStartAdjustment; | ||
| 329 | + const minScroll = typeof insetStartAdjustment === "number" && Number.isFinite(insetStartAdjustment) ? -Math.max(0, insetStartAdjustment) : 0; | ||
| 330 | + const maxScroll = Math.max(minScroll, totalSize - state.scrollLength); | ||
| 331 | const clampDelta = maxScroll - state.scroll; | ||
| 332 | if (unresolvedAmount < 0) { | ||
| 333 | return Math.max(unresolvedAmount, Math.min(0, clampDelta)); | ||
| 334 | @@ -1736,7 +1768,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { | ||
| 335 | settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta); | ||
| 336 | return true; | ||
| 337 | } | ||
| 338 | - const expectedNativeClampScroll = Math.max(0, getContentSize(ctx) - state.scrollLength); | ||
| 339 | + const expectedNativeClampScroll = Math.max(-getContentInsetStartAdjustment(ctx), getContentSize(ctx) - state.scrollLength); | ||
| 340 | const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll); | ||
| 341 | const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON; | ||
| 342 | if (isAtExpectedNativeClamp) { | ||
| 343 | @@ -1869,7 +1901,7 @@ function prepareMVCP(ctx, dataChanged) { | ||
| 344 | if (diff > 0) { | ||
| 345 | diff = Math.max(0, totalSize - state.scroll - state.scrollLength); | ||
| 346 | } else { | ||
| 347 | - const maxScroll = Math.max(0, totalSize - state.scrollLength); | ||
| 348 | + const maxScroll = Math.max(-getContentInsetStartAdjustment(ctx), totalSize - state.scrollLength); | ||
| 349 | state.scroll = maxScroll; | ||
| 350 | state.scrollPending = maxScroll; | ||
| 351 | diff = 0; | ||
| 352 | @@ -2274,8 +2306,121 @@ function scrollToIndex(ctx, { | ||
| 353 | } | ||
| 354 | |||
| 355 | // src/core/initialScroll.ts | ||
| 356 | +var INSET_END_SETTLE_WATCHDOG_FRAMES = 150; | ||
| 357 | +var INSET_END_SETTLE_WATCHDOG_STABLE_FRAMES = 20; | ||
| 358 | +var INSET_END_REVEAL_STABLE_FRAMES = 3; | ||
| 359 | +var INSET_END_REVEAL_MAX_HOLD_FRAMES = 40; | ||
| 360 | +function startInsetEndSettleWatchdog(ctx) { | ||
| 361 | + const state = ctx.state; | ||
| 362 | + if (state.insetEndSettleWatchdogActive) { | ||
| 363 | + return; | ||
| 364 | + } | ||
| 365 | + state.insetEndSettleWatchdogActive = true; | ||
| 366 | + state.didUserDrag = false; | ||
| 367 | + // Hold the readyToRender opacity gate until the end landing is stable, so | ||
| 368 | + // the estimated-to-measured settle chase happens before first VISIBLE | ||
| 369 | + // paint instead of in front of the user. Capped so slow measurement can | ||
| 370 | + // never hide content for long. | ||
| 371 | + state.insetEndRevealHold = true; | ||
| 372 | + let frames = 0; | ||
| 373 | + let settledFrames = 0; | ||
| 374 | + let revealStableFrames = 0; | ||
| 375 | + const releaseRevealHold = () => { | ||
| 376 | + if (state.insetEndRevealHold) { | ||
| 377 | + state.insetEndRevealHold = false; | ||
| 378 | + setInitialRenderState(ctx, {}); | ||
| 379 | + } | ||
| 380 | + }; | ||
| 381 | + // Revealing flips adaptive rendering to "normal", which re-renders rows and | ||
| 382 | + // can move the end target one more time — do that upgrade while still | ||
| 383 | + // hidden (phase 1), then require stability again before revealing (phase 2) | ||
| 384 | + // so the first visible frame is the settled one. | ||
| 385 | + let revealPhase = 1; | ||
| 386 | + const onRevealStability = () => { | ||
| 387 | + var _a3; | ||
| 388 | + if (!state.insetEndRevealHold) { | ||
| 389 | + return; | ||
| 390 | + } | ||
| 391 | + if (revealPhase === 1) { | ||
| 392 | + revealPhase = 2; | ||
| 393 | + revealStableFrames = 0; | ||
| 394 | + setAdaptiveRender(ctx, "normal"); | ||
| 395 | + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); | ||
| 396 | + return; | ||
| 397 | + } | ||
| 398 | + releaseRevealHold(); | ||
| 399 | + }; | ||
| 400 | + const tick = () => { | ||
| 401 | + // The user owns the scroll from their first drag — a re-pin here would | ||
| 402 | + // pin the list under their finger. | ||
| 403 | + if (frames++ >= INSET_END_SETTLE_WATCHDOG_FRAMES || settledFrames >= INSET_END_SETTLE_WATCHDOG_STABLE_FRAMES || state.didUserDrag || !ctx.state || ctx.state !== state) { | ||
| 404 | + state.insetEndSettleWatchdogActive = false; | ||
| 405 | + releaseRevealHold(); | ||
| 406 | + return; | ||
| 407 | + } | ||
| 408 | + if (frames >= INSET_END_REVEAL_MAX_HOLD_FRAMES) { | ||
| 409 | + releaseRevealHold(); | ||
| 410 | + } | ||
| 411 | + const insetStartAdjustment = getContentInsetStartAdjustment(ctx); | ||
| 412 | + const contentSize = getContentSize(ctx); | ||
| 413 | + const scrollLength = state.scrollLength; | ||
| 414 | + if (insetStartAdjustment > 0 && scrollLength > 0 && Number.isFinite(contentSize) && contentSize > scrollLength && !state.scrollingTo && !state.maintainingScrollAtEnd) { | ||
| 415 | + const endOffset = Math.max(-insetStartAdjustment, contentSize - scrollLength); | ||
| 416 | + const distance = endOffset - state.scroll; | ||
| 417 | + // Estimated row sizes converging to measured ones can strand the initial | ||
| 418 | + // end landing when the library's own end-anchor bookkeeping gives up. | ||
| 419 | + // While still near the end (never fighting a user who scrolled away), | ||
| 420 | + // re-pin to the current true end until sizes stop changing. | ||
| 421 | + if (Math.abs(distance) > 2 && Math.abs(distance) <= scrollLength * 0.5) { | ||
| 422 | + settledFrames = 0; | ||
| 423 | + revealStableFrames = 0; | ||
| 424 | + const scroller = state.refScroller.current; | ||
| 425 | + if (scroller) { | ||
| 426 | + scroller.scrollTo({ animated: false, x: 0, y: endOffset }); | ||
| 427 | + } | ||
| 428 | + } else { | ||
| 429 | + settledFrames++; | ||
| 430 | + revealStableFrames++; | ||
| 431 | + if (revealStableFrames >= INSET_END_REVEAL_STABLE_FRAMES) { | ||
| 432 | + onRevealStability(); | ||
| 433 | + } | ||
| 434 | + } | ||
| 435 | + } else { | ||
| 436 | + // Conditions that make re-pinning unnecessary (underflow, in-flight | ||
| 437 | + // programmatic scroll) count toward stability for the reveal. | ||
| 438 | + revealStableFrames++; | ||
| 439 | + if (revealStableFrames >= INSET_END_REVEAL_STABLE_FRAMES) { | ||
| 440 | + onRevealStability(); | ||
| 441 | + } | ||
| 442 | + } | ||
| 443 | + requestAnimationFrame(tick); | ||
| 444 | + }; | ||
| 445 | + requestAnimationFrame(tick); | ||
| 446 | +} | ||
| 447 | function dispatchInitialScroll(ctx, params) { | ||
| 448 | const { forceScroll, resolvedOffset, target, waitForCompletionFrame } = params; | ||
| 449 | + const insetStartAdjustment = getContentInsetStartAdjustment(ctx); | ||
| 450 | + if (insetStartAdjustment > 0 && ctx.state.props.data.length > 0) { | ||
| 451 | + if (ctx.state.scrollLength <= 0) { | ||
| 452 | + // The scroll length is unknown, so the resolved target is meaningless; | ||
| 453 | + // the initial-scroll machinery re-advances after layout. | ||
| 454 | + return; | ||
| 455 | + } | ||
| 456 | + if (resolvedOffset <= -insetStartAdjustment + 1 && !ctx.state.didDispatchInsetInitialScroll) { | ||
| 457 | + // Content underflows the viewport and nothing has scrolled yet: the | ||
| 458 | + // untouched native rest position (UIKit's adjustedContentInset) IS the | ||
| 459 | + // end position. Dispatching a negative scroll here would race UIKit's | ||
| 460 | + // inset application during screen attach, so just finish the session. | ||
| 461 | + finishInitialScroll(ctx, { | ||
| 462 | + resolvedOffset | ||
| 463 | + }); | ||
| 464 | + return; | ||
| 465 | + } | ||
| 466 | + ctx.state.didDispatchInsetInitialScroll = true; | ||
| 467 | + if (target.viewPosition === 1) { | ||
| 468 | + startInsetEndSettleWatchdog(ctx); | ||
| 469 | + } | ||
| 470 | + } | ||
| 471 | const requestedIndex = target.index; | ||
| 472 | const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0; | ||
| 473 | const itemSize = getItemSizeAtIndex(ctx, index); | ||
| 474 | @@ -2704,7 +2849,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { | ||
| 475 | return; | ||
| 476 | } | ||
| 477 | if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) { | ||
| 478 | - const shouldKeepEndTargetAlive = isRetargetableBottomAlignedInitialScrollTarget(initialScroll) && peek$(ctx, "isAtEnd"); | ||
| 479 | + const endTargetDistanceFromEnd = getContentSize(ctx) - state.scroll - state.scrollLength - getContentInsetEnd(ctx); | ||
| 480 | + const isNearEndForInsetList = getContentInsetStartAdjustment(ctx) > 0 && Number.isFinite(endTargetDistanceFromEnd) && endTargetDistanceFromEnd <= state.scrollLength * 0.5; | ||
| 481 | + const shouldKeepEndTargetAlive = isRetargetableBottomAlignedInitialScrollTarget(initialScroll) && (peek$(ctx, "isAtEnd") || isNearEndForInsetList); | ||
| 482 | if (!shouldKeepEndTargetAlive) { | ||
| 483 | if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) { | ||
| 484 | clearPendingInitialScrollFooterLayout(ctx, { | ||
| 485 | @@ -4637,7 +4784,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { | ||
| 486 | } | ||
| 487 | contentBelowAnchor += footerSize + stylePaddingBottom; | ||
| 488 | isReady = !hasUnknownTailSize; | ||
| 489 | - nextSize = hasUnknownTailSize ? previousSize || 0 : Math.max(0, state.scrollLength - contentBelowAnchor - anchorOffset); | ||
| 490 | + const knownSizeBound = Math.max(0, state.scrollLength - contentBelowAnchor - anchorOffset); | ||
| 491 | + nextSize = hasUnknownTailSize ? Math.min(previousSize || 0, knownSizeBound) : knownSizeBound; | ||
| 492 | } else if (anchorIndex >= 0) { | ||
| 493 | isReady = false; | ||
| 494 | } | ||
| 495 | @@ -4655,6 +4803,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { | ||
| 496 | updateScroll(ctx, state.scroll, true); | ||
| 497 | } | ||
| 498 | (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize }); | ||
| 499 | + } else if (!isReady && didSizeChange && nextSize < (previousSize || 0)) { | ||
| 500 | + set$(ctx, "anchoredEndSpaceSize", nextSize); | ||
| 501 | + (_a3 = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onSizeChanged) == null ? void 0 : _a3.call(anchoredEndSpace, nextSize); | ||
| 502 | + if (anchoredEndSpace == null ? void 0 : anchoredEndSpace.includeInEndInset) { | ||
| 503 | + updateScroll(ctx, state.scroll, true); | ||
| 504 | + } | ||
| 505 | } | ||
| 506 | return nextSize; | ||
| 507 | } | ||
| 508 | @@ -6960,6 +7114,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 509 | dataVersion, | ||
| 510 | drawDistance = 250, | ||
| 511 | contentInsetEndAdjustment, | ||
| 512 | + contentInsetStartAdjustment, | ||
| 513 | estimatedItemSize = 100, | ||
| 514 | estimatedListSize, | ||
| 515 | extraData, | ||
| 516 | @@ -6990,6 +7145,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 517 | onLayout: onLayoutProp, | ||
| 518 | onLoad, | ||
| 519 | onMomentumScrollEnd, | ||
| 520 | + onScrollBeginDrag, | ||
| 521 | onRefresh, | ||
| 522 | onScroll: onScrollProp, | ||
| 523 | onScrollBeginDrag, | ||
| 524 | @@ -7076,7 +7232,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 525 | const combinedRef = useCombinedRef(refScroller, refScrollView); | ||
| 526 | const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString()); | ||
| 527 | const stickyHeaderIndices = stickyHeaderIndicesProp; | ||
| 528 | - const contentInsetEndAdjustmentResolved = Platform.OS === "web" ? contentInsetEndAdjustment : void 0; | ||
| 529 | + const contentInsetEndAdjustmentResolved = contentInsetEndAdjustment; | ||
| 530 | const previousContentInsetEndAdjustmentRef = React2.useRef(contentInsetEndAdjustmentResolved); | ||
| 531 | const alwaysRenderIndices = React2.useMemo(() => { | ||
| 532 | const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex); | ||
| 533 | @@ -7215,6 +7371,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 534 | contentContainerAlignItems: contentContainerStyle.alignItems, | ||
| 535 | contentInset, | ||
| 536 | contentInsetEndAdjustment: contentInsetEndAdjustmentResolved, | ||
| 537 | + contentInsetStartAdjustment, | ||
| 538 | data: dataProp, | ||
| 539 | dataKey, | ||
| 540 | dataVersion, | ||
| 541 | @@ -7303,6 +7460,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 542 | return void 0; | ||
| 543 | } | ||
| 544 | const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll); | ||
| 545 | + if (getContentInsetStartAdjustment(ctx) > 0) { | ||
| 546 | + // With a native leading inset (transparent header + automatic insets), | ||
| 547 | + // any mount-time contentOffset races UIKit's adjustedContentInset | ||
| 548 | + // application. The gated initial-scroll dispatch positions the list once | ||
| 549 | + // real sizes are known instead. | ||
| 550 | + return void 0; | ||
| 551 | + } | ||
| 552 | return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset; | ||
| 553 | }, [usesBootstrapInitialScroll]); | ||
| 554 | React2.useLayoutEffect(() => { | ||
| 555 | @@ -7526,6 +7690,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 556 | onScroll: (event) => onScroll(ctx, event), | ||
| 557 | onScrollBeginDrag: (event) => { | ||
| 558 | var _a4, _b2; | ||
| 559 | + ctx.state.didUserDrag = true; | ||
| 560 | prepareReachedEdgeForNextUserScroll(ctx); | ||
| 561 | (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); | ||
| 562 | }, | ||
| 563 | @@ -7555,6 +7720,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 564 | onLayout, | ||
| 565 | onLayoutFooter, | ||
| 566 | onMomentumScrollEnd: fns.onMomentumScrollEnd, | ||
| 567 | + onScrollBeginDrag: fns.onScrollBeginDrag, | ||
| 568 | onScroll: onScrollHandler, | ||
| 569 | recycleItems, | ||
| 570 | refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2__namespace.cloneElement(refreshControlElement, { | ||
| 571 | diff --git a/react-native.mjs b/react-native.mjs | ||
| 572 | index c2e0f38..5313086 100644 | ||
| 573 | --- a/react-native.mjs | ||
| 574 | +++ b/react-native.mjs | ||
| 575 | @@ -909,7 +909,7 @@ function setInitialRenderState(ctx, { | ||
| 576 | if (didInitialScroll) { | ||
| 577 | state.didFinishInitialScroll = true; | ||
| 578 | } | ||
| 579 | - const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); | ||
| 580 | + const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll && !state.insetEndRevealHold); | ||
| 581 | if (isReadyToRender && !peek$(ctx, "readyToRender")) { | ||
| 582 | set$(ctx, "readyToRender", true); | ||
| 583 | setAdaptiveRender(ctx, "normal", "ready"); | ||
| 584 | @@ -1238,18 +1238,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) { | ||
| 585 | } | ||
| 586 | |||
| 587 | // src/core/clampScrollOffset.ts | ||
| 588 | +function getContentInsetStartAdjustment(ctx) { | ||
| 589 | + const adjustment = ctx.state.props.contentInsetStartAdjustment; | ||
| 590 | + return typeof adjustment === "number" && Number.isFinite(adjustment) ? Math.max(0, adjustment) : 0; | ||
| 591 | +} | ||
| 592 | function clampScrollOffset(ctx, offset, scrollTarget) { | ||
| 593 | const state = ctx.state; | ||
| 594 | const contentSize = getContentSize(ctx); | ||
| 595 | + const minOffset = -getContentInsetStartAdjustment(ctx); | ||
| 596 | let clampedOffset = offset; | ||
| 597 | if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android" || state.lastLayout)) { | ||
| 598 | - const baseMaxOffset = Math.max(0, contentSize - state.scrollLength); | ||
| 599 | + const baseMaxOffset = Math.max(minOffset, contentSize - state.scrollLength); | ||
| 600 | const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset; | ||
| 601 | const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0; | ||
| 602 | const maxOffset = baseMaxOffset + extraEndOffset; | ||
| 603 | clampedOffset = Math.min(offset, maxOffset); | ||
| 604 | } | ||
| 605 | - clampedOffset = Math.max(0, clampedOffset); | ||
| 606 | + clampedOffset = Math.max(minOffset, clampedOffset); | ||
| 607 | return clampedOffset; | ||
| 608 | } | ||
| 609 | |||
| 610 | @@ -1385,10 +1390,10 @@ function checkFinishedScrollFrame(ctx) { | ||
| 611 | finishScrollTo(ctx); | ||
| 612 | } | ||
| 613 | } | ||
| 614 | -function scrollToFallbackOffset(ctx, offset) { | ||
| 615 | +function scrollToFallbackOffset(ctx, offset, animated) { | ||
| 616 | var _a3; | ||
| 617 | (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({ | ||
| 618 | - animated: false, | ||
| 619 | + animated: !!animated, | ||
| 620 | x: ctx.state.props.horizontal ? offset : 0, | ||
| 621 | y: ctx.state.props.horizontal ? 0 : offset | ||
| 622 | }); | ||
| 623 | @@ -1435,7 +1440,10 @@ function checkFinishedScrollFallback(ctx) { | ||
| 624 | }); | ||
| 625 | scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); | ||
| 626 | } else if (shouldRetryUnalignedEndScroll) { | ||
| 627 | - scrollToFallbackOffset(ctx, completionState.clampedTargetOffset); | ||
| 628 | + const isActivelyAnimatingToEnd = !!isStillScrollingTo.animated && Date.now() - state.scrollTime < 100; | ||
| 629 | + if (!isActivelyAnimatingToEnd) { | ||
| 630 | + scrollToFallbackOffset(ctx, completionState.clampedTargetOffset, !!isStillScrollingTo.animated); | ||
| 631 | + } | ||
| 632 | scheduleFallbackCheck(100); | ||
| 633 | } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) { | ||
| 634 | finishScrollTo(ctx); | ||
| 635 | @@ -1496,9 +1504,18 @@ function doMaintainScrollAtEnd(ctx) { | ||
| 636 | } | ||
| 637 | if (shouldMaintainScrollAtEnd) { | ||
| 638 | state.pendingMaintainScrollAtEnd = false; | ||
| 639 | + const maintainAnchoredEndSpace = state.props.anchoredEndSpace; | ||
| 640 | + const maintainAnchorIndex = maintainAnchoredEndSpace == null ? void 0 : maintainAnchoredEndSpace.anchorIndex; | ||
| 641 | + if (maintainAnchorIndex !== void 0 && maintainAnchorIndex >= 0 && maintainAnchorIndex < state.props.data.length && !areKnownOrFixedItemSizesAvailable(ctx, maintainAnchorIndex, state.props.data.length - 1)) { | ||
| 642 | + return false; | ||
| 643 | + } | ||
| 644 | const contentSize = getContentSize(ctx); | ||
| 645 | + const maintainInsetStartAdjustment = getContentInsetStartAdjustment(ctx); | ||
| 646 | if (contentSize < state.scrollLength) { | ||
| 647 | - state.scroll = 0; | ||
| 648 | + state.scroll = -maintainInsetStartAdjustment; | ||
| 649 | + if (maintainInsetStartAdjustment > 0) { | ||
| 650 | + return true; | ||
| 651 | + } | ||
| 652 | } | ||
| 653 | if (!state.maintainingScrollAtEnd) { | ||
| 654 | const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant"; | ||
| 655 | @@ -1518,9 +1535,18 @@ function doMaintainScrollAtEnd(ctx) { | ||
| 656 | y: 0 | ||
| 657 | }); | ||
| 658 | } else { | ||
| 659 | - scroller == null ? void 0 : scroller.scrollToEnd({ | ||
| 660 | - animated: maintainScrollAtEnd.animated | ||
| 661 | - }); | ||
| 662 | + const insetStartAdjustment = getContentInsetStartAdjustment(ctx); | ||
| 663 | + if (insetStartAdjustment > 0) { | ||
| 664 | + scroller == null ? void 0 : scroller.scrollTo({ | ||
| 665 | + animated: maintainScrollAtEnd.animated, | ||
| 666 | + x: 0, | ||
| 667 | + y: Math.max(-insetStartAdjustment, getContentSize(ctx) - state.scrollLength) | ||
| 668 | + }); | ||
| 669 | + } else { | ||
| 670 | + scroller == null ? void 0 : scroller.scrollToEnd({ | ||
| 671 | + animated: maintainScrollAtEnd.animated | ||
| 672 | + }); | ||
| 673 | + } | ||
| 674 | } | ||
| 675 | setTimeout( | ||
| 676 | () => { | ||
| 677 | @@ -1550,6 +1576,10 @@ function doMaintainScrollAtEnd(ctx) { | ||
| 678 | function requestAdjust(ctx, positionDiff, dataChanged) { | ||
| 679 | const state = ctx.state; | ||
| 680 | if (Math.abs(positionDiff) > 0.1) { | ||
| 681 | + // Timestamp for ReanimatedPositionView: repositions caused by an MVCP | ||
| 682 | + // size adjustment are already compensated by a contentOffset shift, so | ||
| 683 | + // animating them would make rows visibly lurch and slide back. | ||
| 684 | + state.lastMVCPAdjustTime = Date.now(); | ||
| 685 | const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff; | ||
| 686 | const doit = () => { | ||
| 687 | if (needsScrollWorkaround) { | ||
| 688 | @@ -1653,7 +1683,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) { | ||
| 689 | if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) { | ||
| 690 | return 0; | ||
| 691 | } | ||
| 692 | - const maxScroll = Math.max(0, totalSize - state.scrollLength); | ||
| 693 | + const insetStartAdjustment = state.props.contentInsetStartAdjustment; | ||
| 694 | + const minScroll = typeof insetStartAdjustment === "number" && Number.isFinite(insetStartAdjustment) ? -Math.max(0, insetStartAdjustment) : 0; | ||
| 695 | + const maxScroll = Math.max(minScroll, totalSize - state.scrollLength); | ||
| 696 | const clampDelta = maxScroll - state.scroll; | ||
| 697 | if (unresolvedAmount < 0) { | ||
| 698 | return Math.max(unresolvedAmount, Math.min(0, clampDelta)); | ||
| 699 | @@ -1715,7 +1747,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) { | ||
| 700 | settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta); | ||
| 701 | return true; | ||
| 702 | } | ||
| 703 | - const expectedNativeClampScroll = Math.max(0, getContentSize(ctx) - state.scrollLength); | ||
| 704 | + const expectedNativeClampScroll = Math.max(-getContentInsetStartAdjustment(ctx), getContentSize(ctx) - state.scrollLength); | ||
| 705 | const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll); | ||
| 706 | const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON; | ||
| 707 | if (isAtExpectedNativeClamp) { | ||
| 708 | @@ -1848,7 +1880,7 @@ function prepareMVCP(ctx, dataChanged) { | ||
| 709 | if (diff > 0) { | ||
| 710 | diff = Math.max(0, totalSize - state.scroll - state.scrollLength); | ||
| 711 | } else { | ||
| 712 | - const maxScroll = Math.max(0, totalSize - state.scrollLength); | ||
| 713 | + const maxScroll = Math.max(-getContentInsetStartAdjustment(ctx), totalSize - state.scrollLength); | ||
| 714 | state.scroll = maxScroll; | ||
| 715 | state.scrollPending = maxScroll; | ||
| 716 | diff = 0; | ||
| 717 | @@ -2253,8 +2285,121 @@ function scrollToIndex(ctx, { | ||
| 718 | } | ||
| 719 | |||
| 720 | // src/core/initialScroll.ts | ||
| 721 | +var INSET_END_SETTLE_WATCHDOG_FRAMES = 150; | ||
| 722 | +var INSET_END_SETTLE_WATCHDOG_STABLE_FRAMES = 20; | ||
| 723 | +var INSET_END_REVEAL_STABLE_FRAMES = 3; | ||
| 724 | +var INSET_END_REVEAL_MAX_HOLD_FRAMES = 40; | ||
| 725 | +function startInsetEndSettleWatchdog(ctx) { | ||
| 726 | + const state = ctx.state; | ||
| 727 | + if (state.insetEndSettleWatchdogActive) { | ||
| 728 | + return; | ||
| 729 | + } | ||
| 730 | + state.insetEndSettleWatchdogActive = true; | ||
| 731 | + state.didUserDrag = false; | ||
| 732 | + // Hold the readyToRender opacity gate until the end landing is stable, so | ||
| 733 | + // the estimated-to-measured settle chase happens before first VISIBLE | ||
| 734 | + // paint instead of in front of the user. Capped so slow measurement can | ||
| 735 | + // never hide content for long. | ||
| 736 | + state.insetEndRevealHold = true; | ||
| 737 | + let frames = 0; | ||
| 738 | + let settledFrames = 0; | ||
| 739 | + let revealStableFrames = 0; | ||
| 740 | + const releaseRevealHold = () => { | ||
| 741 | + if (state.insetEndRevealHold) { | ||
| 742 | + state.insetEndRevealHold = false; | ||
| 743 | + setInitialRenderState(ctx, {}); | ||
| 744 | + } | ||
| 745 | + }; | ||
| 746 | + // Revealing flips adaptive rendering to "normal", which re-renders rows and | ||
| 747 | + // can move the end target one more time — do that upgrade while still | ||
| 748 | + // hidden (phase 1), then require stability again before revealing (phase 2) | ||
| 749 | + // so the first visible frame is the settled one. | ||
| 750 | + let revealPhase = 1; | ||
| 751 | + const onRevealStability = () => { | ||
| 752 | + var _a3; | ||
| 753 | + if (!state.insetEndRevealHold) { | ||
| 754 | + return; | ||
| 755 | + } | ||
| 756 | + if (revealPhase === 1) { | ||
| 757 | + revealPhase = 2; | ||
| 758 | + revealStableFrames = 0; | ||
| 759 | + setAdaptiveRender(ctx, "normal"); | ||
| 760 | + (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state); | ||
| 761 | + return; | ||
| 762 | + } | ||
| 763 | + releaseRevealHold(); | ||
| 764 | + }; | ||
| 765 | + const tick = () => { | ||
| 766 | + // The user owns the scroll from their first drag — a re-pin here would | ||
| 767 | + // pin the list under their finger. | ||
| 768 | + if (frames++ >= INSET_END_SETTLE_WATCHDOG_FRAMES || settledFrames >= INSET_END_SETTLE_WATCHDOG_STABLE_FRAMES || state.didUserDrag || !ctx.state || ctx.state !== state) { | ||
| 769 | + state.insetEndSettleWatchdogActive = false; | ||
| 770 | + releaseRevealHold(); | ||
| 771 | + return; | ||
| 772 | + } | ||
| 773 | + if (frames >= INSET_END_REVEAL_MAX_HOLD_FRAMES) { | ||
| 774 | + releaseRevealHold(); | ||
| 775 | + } | ||
| 776 | + const insetStartAdjustment = getContentInsetStartAdjustment(ctx); | ||
| 777 | + const contentSize = getContentSize(ctx); | ||
| 778 | + const scrollLength = state.scrollLength; | ||
| 779 | + if (insetStartAdjustment > 0 && scrollLength > 0 && Number.isFinite(contentSize) && contentSize > scrollLength && !state.scrollingTo && !state.maintainingScrollAtEnd) { | ||
| 780 | + const endOffset = Math.max(-insetStartAdjustment, contentSize - scrollLength); | ||
| 781 | + const distance = endOffset - state.scroll; | ||
| 782 | + // Estimated row sizes converging to measured ones can strand the initial | ||
| 783 | + // end landing when the library's own end-anchor bookkeeping gives up. | ||
| 784 | + // While still near the end (never fighting a user who scrolled away), | ||
| 785 | + // re-pin to the current true end until sizes stop changing. | ||
| 786 | + if (Math.abs(distance) > 2 && Math.abs(distance) <= scrollLength * 0.5) { | ||
| 787 | + settledFrames = 0; | ||
| 788 | + revealStableFrames = 0; | ||
| 789 | + const scroller = state.refScroller.current; | ||
| 790 | + if (scroller) { | ||
| 791 | + scroller.scrollTo({ animated: false, x: 0, y: endOffset }); | ||
| 792 | + } | ||
| 793 | + } else { | ||
| 794 | + settledFrames++; | ||
| 795 | + revealStableFrames++; | ||
| 796 | + if (revealStableFrames >= INSET_END_REVEAL_STABLE_FRAMES) { | ||
| 797 | + onRevealStability(); | ||
| 798 | + } | ||
| 799 | + } | ||
| 800 | + } else { | ||
| 801 | + // Conditions that make re-pinning unnecessary (underflow, in-flight | ||
| 802 | + // programmatic scroll) count toward stability for the reveal. | ||
| 803 | + revealStableFrames++; | ||
| 804 | + if (revealStableFrames >= INSET_END_REVEAL_STABLE_FRAMES) { | ||
| 805 | + onRevealStability(); | ||
| 806 | + } | ||
| 807 | + } | ||
| 808 | + requestAnimationFrame(tick); | ||
| 809 | + }; | ||
| 810 | + requestAnimationFrame(tick); | ||
| 811 | +} | ||
| 812 | function dispatchInitialScroll(ctx, params) { | ||
| 813 | const { forceScroll, resolvedOffset, target, waitForCompletionFrame } = params; | ||
| 814 | + const insetStartAdjustment = getContentInsetStartAdjustment(ctx); | ||
| 815 | + if (insetStartAdjustment > 0 && ctx.state.props.data.length > 0) { | ||
| 816 | + if (ctx.state.scrollLength <= 0) { | ||
| 817 | + // The scroll length is unknown, so the resolved target is meaningless; | ||
| 818 | + // the initial-scroll machinery re-advances after layout. | ||
| 819 | + return; | ||
| 820 | + } | ||
| 821 | + if (resolvedOffset <= -insetStartAdjustment + 1 && !ctx.state.didDispatchInsetInitialScroll) { | ||
| 822 | + // Content underflows the viewport and nothing has scrolled yet: the | ||
| 823 | + // untouched native rest position (UIKit's adjustedContentInset) IS the | ||
| 824 | + // end position. Dispatching a negative scroll here would race UIKit's | ||
| 825 | + // inset application during screen attach, so just finish the session. | ||
| 826 | + finishInitialScroll(ctx, { | ||
| 827 | + resolvedOffset | ||
| 828 | + }); | ||
| 829 | + return; | ||
| 830 | + } | ||
| 831 | + ctx.state.didDispatchInsetInitialScroll = true; | ||
| 832 | + if (target.viewPosition === 1) { | ||
| 833 | + startInsetEndSettleWatchdog(ctx); | ||
| 834 | + } | ||
| 835 | + } | ||
| 836 | const requestedIndex = target.index; | ||
| 837 | const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0; | ||
| 838 | const itemSize = getItemSizeAtIndex(ctx, index); | ||
| 839 | @@ -2683,7 +2828,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) { | ||
| 840 | return; | ||
| 841 | } | ||
| 842 | if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) { | ||
| 843 | - const shouldKeepEndTargetAlive = isRetargetableBottomAlignedInitialScrollTarget(initialScroll) && peek$(ctx, "isAtEnd"); | ||
| 844 | + const endTargetDistanceFromEnd = getContentSize(ctx) - state.scroll - state.scrollLength - getContentInsetEnd(ctx); | ||
| 845 | + const isNearEndForInsetList = getContentInsetStartAdjustment(ctx) > 0 && Number.isFinite(endTargetDistanceFromEnd) && endTargetDistanceFromEnd <= state.scrollLength * 0.5; | ||
| 846 | + const shouldKeepEndTargetAlive = isRetargetableBottomAlignedInitialScrollTarget(initialScroll) && (peek$(ctx, "isAtEnd") || isNearEndForInsetList); | ||
| 847 | if (!shouldKeepEndTargetAlive) { | ||
| 848 | if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) { | ||
| 849 | clearPendingInitialScrollFooterLayout(ctx, { | ||
| 850 | @@ -4616,7 +4763,8 @@ function maybeUpdateAnchoredEndSpace(ctx) { | ||
| 851 | } | ||
| 852 | contentBelowAnchor += footerSize + stylePaddingBottom; | ||
| 853 | isReady = !hasUnknownTailSize; | ||
| 854 | - nextSize = hasUnknownTailSize ? previousSize || 0 : Math.max(0, state.scrollLength - contentBelowAnchor - anchorOffset); | ||
| 855 | + const knownSizeBound = Math.max(0, state.scrollLength - contentBelowAnchor - anchorOffset); | ||
| 856 | + nextSize = hasUnknownTailSize ? Math.min(previousSize || 0, knownSizeBound) : knownSizeBound; | ||
| 857 | } else if (anchorIndex >= 0) { | ||
| 858 | isReady = false; | ||
| 859 | } | ||
| 860 | @@ -4634,6 +4782,12 @@ function maybeUpdateAnchoredEndSpace(ctx) { | ||
| 861 | updateScroll(ctx, state.scroll, true); | ||
| 862 | } | ||
| 863 | (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize }); | ||
| 864 | + } else if (!isReady && didSizeChange && nextSize < (previousSize || 0)) { | ||
| 865 | + set$(ctx, "anchoredEndSpaceSize", nextSize); | ||
| 866 | + (_a3 = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onSizeChanged) == null ? void 0 : _a3.call(anchoredEndSpace, nextSize); | ||
| 867 | + if (anchoredEndSpace == null ? void 0 : anchoredEndSpace.includeInEndInset) { | ||
| 868 | + updateScroll(ctx, state.scroll, true); | ||
| 869 | + } | ||
| 870 | } | ||
| 871 | return nextSize; | ||
| 872 | } | ||
| 873 | @@ -6939,6 +7093,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 874 | dataVersion, | ||
| 875 | drawDistance = 250, | ||
| 876 | contentInsetEndAdjustment, | ||
| 877 | + contentInsetStartAdjustment, | ||
| 878 | estimatedItemSize = 100, | ||
| 879 | estimatedListSize, | ||
| 880 | extraData, | ||
| 881 | @@ -7055,7 +7210,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 882 | const combinedRef = useCombinedRef(refScroller, refScrollView); | ||
| 883 | const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString()); | ||
| 884 | const stickyHeaderIndices = stickyHeaderIndicesProp; | ||
| 885 | - const contentInsetEndAdjustmentResolved = Platform.OS === "web" ? contentInsetEndAdjustment : void 0; | ||
| 886 | + const contentInsetEndAdjustmentResolved = contentInsetEndAdjustment; | ||
| 887 | const previousContentInsetEndAdjustmentRef = useRef(contentInsetEndAdjustmentResolved); | ||
| 888 | const alwaysRenderIndices = useMemo(() => { | ||
| 889 | const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex); | ||
| 890 | @@ -7194,6 +7349,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 891 | contentContainerAlignItems: contentContainerStyle.alignItems, | ||
| 892 | contentInset, | ||
| 893 | contentInsetEndAdjustment: contentInsetEndAdjustmentResolved, | ||
| 894 | + contentInsetStartAdjustment, | ||
| 895 | data: dataProp, | ||
| 896 | dataKey, | ||
| 897 | dataVersion, | ||
| 898 | @@ -7282,6 +7438,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 899 | return void 0; | ||
| 900 | } | ||
| 901 | const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll); | ||
| 902 | + if (getContentInsetStartAdjustment(ctx) > 0) { | ||
| 903 | + // With a native leading inset (transparent header + automatic insets), | ||
| 904 | + // any mount-time contentOffset races UIKit's adjustedContentInset | ||
| 905 | + // application. The gated initial-scroll dispatch positions the list once | ||
| 906 | + // real sizes are known instead. | ||
| 907 | + return void 0; | ||
| 908 | + } | ||
| 909 | return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset; | ||
| 910 | }, [usesBootstrapInitialScroll]); | ||
| 911 | useLayoutEffect(() => { | ||
| 912 | @@ -7505,6 +7668,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 913 | onScroll: (event) => onScroll(ctx, event), | ||
| 914 | onScrollBeginDrag: (event) => { | ||
| 915 | var _a4, _b2; | ||
| 916 | + ctx.state.didUserDrag = true; | ||
| 917 | prepareReachedEdgeForNextUserScroll(ctx); | ||
| 918 | (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); | ||
| 919 | }, | ||
| 920 | @@ -7534,6 +7698,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded | ||
| 921 | onLayout, | ||
| 922 | onLayoutFooter, | ||
| 923 | onMomentumScrollEnd: fns.onMomentumScrollEnd, | ||
| 924 | + onScrollBeginDrag: fns.onScrollBeginDrag, | ||
| 925 | onScroll: onScrollHandler, | ||
| 926 | recycleItems, | ||
| 927 | refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2.cloneElement(refreshControlElement, { | ||
| 928 | diff --git a/reanimated.d.ts b/reanimated.d.ts | ||
| 929 | index 940da28..28dccbe 100644 | ||
| 930 | --- a/reanimated.d.ts | ||
| 931 | +++ b/reanimated.d.ts | ||
| 932 | @@ -294,6 +294,12 @@ interface LegendListSpecificProps<ItemT, TItemType extends string | undefined> { | ||
| 933 | * The adjustment is also rendered as real content padding so the browser scroll range includes it. | ||
| 934 | */ | ||
| 935 | contentInsetEndAdjustment?: number; | ||
| 936 | + /** | ||
| 937 | + * Width/height of a leading content inset applied natively outside the list's knowledge | ||
| 938 | + * (e.g. iOS contentInsetAdjustmentBehavior="automatic" under a transparent header). | ||
| 939 | + * Programmatic scrolls clamp to -adjustment instead of 0 so content can rest below the header. | ||
| 940 | + */ | ||
| 941 | + contentInsetStartAdjustment?: number; | ||
| 942 | /** | ||
| 943 | * Number of columns to render items in. | ||
| 944 | * @default 1 | ||
| 945 | diff --git a/reanimated.js b/reanimated.js | ||
| 946 | index f1265fa..16dcef0 100644 | ||
| 947 | --- a/reanimated.js | ||
| 948 | +++ b/reanimated.js | ||
| 949 | @@ -116,7 +116,7 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( | ||
| 950 | const [positionValue = POSITION_OUT_OF_VIEW] = useArr$([`containerPosition${id}`]); | ||
| 951 | const prevItemKeyRef = React__namespace.useRef(void 0); | ||
| 952 | let shouldSkipTransitionForRecycleReuse = false; | ||
| 953 | - if (recycleItems && layoutTransition) { | ||
| 954 | + if (layoutTransition) { | ||
| 955 | const itemKeySignal = `containerItemKey${id}`; | ||
| 956 | const itemKey = peek$(ctx, itemKeySignal); | ||
| 957 | shouldSkipTransitionForRecycleReuse = itemKey !== void 0 && prevItemKeyRef.current !== void 0 && prevItemKeyRef.current !== itemKey; | ||
| 958 | @@ -130,10 +130,20 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( | ||
| 959 | () => [style, horizontal ? { left: positionValue } : { top: positionValue }], | ||
| 960 | [horizontal, positionValue, style] | ||
| 961 | ); | ||
| 962 | + // Two kinds of repositions must not animate: (1) MVCP size adjustments, | ||
| 963 | + // which are compensated by an equal contentOffset shift so the row should | ||
| 964 | + // not visibly move — animating turns the invisible correction into a | ||
| 965 | + // lurch-and-settle; (2) any reposition while the user is actively | ||
| 966 | + // scrolling, where rows shifting under the finger reads as jank. The | ||
| 967 | + // transition exists to smooth at-rest shifts (streaming text, work-log | ||
| 968 | + // folds), so gate it to at-rest moments. | ||
| 969 | + const now = Date.now(); | ||
| 970 | + const isMVCPReposition = now - (ctx.state.lastMVCPAdjustTime || 0) < 300 || | ||
| 971 | + now - (ctx.state.lastNativeScrollTime || 0) < 300; | ||
| 972 | return /* @__PURE__ */ React__namespace.createElement( | ||
| 973 | Reanimated__default.default.View, | ||
| 974 | { | ||
| 975 | - layout: shouldSkipTransitionForRecycleReuse ? void 0 : layoutTransition, | ||
| 976 | + layout: shouldSkipTransitionForRecycleReuse || isMVCPReposition ? void 0 : layoutTransition, | ||
| 977 | ref: refView, | ||
| 978 | style: viewStyle, | ||
| 979 | ...rest | ||
| 980 | diff --git a/reanimated.mjs b/reanimated.mjs | ||
| 981 | index 29a00d5..9c25ec5 100644 | ||
| 982 | --- a/reanimated.mjs | ||
| 983 | +++ b/reanimated.mjs | ||
| 984 | @@ -92,7 +92,7 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( | ||
| 985 | const [positionValue = POSITION_OUT_OF_VIEW] = useArr$([`containerPosition${id}`]); | ||
| 986 | const prevItemKeyRef = React.useRef(void 0); | ||
| 987 | let shouldSkipTransitionForRecycleReuse = false; | ||
| 988 | - if (recycleItems && layoutTransition) { | ||
| 989 | + if (layoutTransition) { | ||
| 990 | const itemKeySignal = `containerItemKey${id}`; | ||
| 991 | const itemKey = peek$(ctx, itemKeySignal); | ||
| 992 | shouldSkipTransitionForRecycleReuse = itemKey !== void 0 && prevItemKeyRef.current !== void 0 && prevItemKeyRef.current !== itemKey; | ||
| 993 | @@ -106,10 +106,20 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent( | ||
| 994 | () => [style, horizontal ? { left: positionValue } : { top: positionValue }], | ||
| 995 | [horizontal, positionValue, style] | ||
| 996 | ); | ||
| 997 | + // Two kinds of repositions must not animate: (1) MVCP size adjustments, | ||
| 998 | + // which are compensated by an equal contentOffset shift so the row should | ||
| 999 | + // not visibly move — animating turns the invisible correction into a | ||
| 1000 | + // lurch-and-settle; (2) any reposition while the user is actively | ||
| 1001 | + // scrolling, where rows shifting under the finger reads as jank. The | ||
| 1002 | + // transition exists to smooth at-rest shifts (streaming text, work-log | ||
| 1003 | + // folds), so gate it to at-rest moments. | ||
| 1004 | + const now = Date.now(); | ||
| 1005 | + const isMVCPReposition = now - (ctx.state.lastMVCPAdjustTime || 0) < 300 || | ||
| 1006 | + now - (ctx.state.lastNativeScrollTime || 0) < 300; | ||
| 1007 | return /* @__PURE__ */ React.createElement( | ||
| 1008 | Reanimated.View, | ||
| 1009 | { | ||
| 1010 | - layout: shouldSkipTransitionForRecycleReuse ? void 0 : layoutTransition, | ||
| 1011 | + layout: shouldSkipTransitionForRecycleReuse || isMVCPReposition ? void 0 : layoutTransition, | ||
| 1012 | ref: refView, | ||
| 1013 | style: viewStyle, | ||
| 1014 | ...rest | ||
pnpm-lock.yaml+24-12| ... | @@ -74,7 +74,7 @@ patchedDependencies: | ... | @@ -74,7 +74,7 @@ patchedDependencies: |
| 74 | '@effect/vitest@4.0.0-beta.102': a607339aab944136a084a05f159aa0f5a69776934d4b835dab8f9992f1c13425 | 74 | '@effect/vitest@4.0.0-beta.102': a607339aab944136a084a05f159aa0f5a69776934d4b835dab8f9992f1c13425 |
| 75 | '@expo/metro-config@56.0.14': 8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46 | 75 | '@expo/metro-config@56.0.14': 8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46 |
| 76 | '@ff-labs/fff-node@0.9.4': 2b16019ce7ab61aec6478dd02f79ef468cc1d5c51e9d00764f7d2ab8167210c8 | 76 | '@ff-labs/fff-node@0.9.4': 2b16019ce7ab61aec6478dd02f79ef468cc1d5c51e9d00764f7d2ab8167210c8 |
| 77 | '@legendapp/list@3.2.0': 9203ec3db86574a31e157a265cefd8997b259cc420a0c4c61c580c80ed8cf78d | 77 | '@legendapp/list@3.3.3': d162d67b73933ab077d00627cdf52b18ea88b28c4fae4e8340a854df25026f09 |
| 78 | '@pierre/diffs@1.3.0-beta.10': 7ef7cb0cbabb17c15cdb137554068b36f15f6f5265e73fb51452aa3380db91aa | 78 | '@pierre/diffs@1.3.0-beta.10': 7ef7cb0cbabb17c15cdb137554068b36f15f6f5265e73fb51452aa3380db91aa |
| 79 | '@react-native-menu/menu@2.0.0': 5ea3ae4bf1d9baf5443b65c269bb09621c27a68d556f713778f37b1e8d46aaae | 79 | '@react-native-menu/menu@2.0.0': 5ea3ae4bf1d9baf5443b65c269bb09621c27a68d556f713778f37b1e8d46aaae |
| 80 | '@react-navigation/native-stack@7.17.6': c7fc101b78d434904425e5a24c22fb0042298dec6f807250486e784f3c717273 | 80 | '@react-navigation/native-stack@7.17.6': c7fc101b78d434904425e5a24c22fb0042298dec6f807250486e784f3c717273 |
| ... | @@ -212,8 +212,8 @@ importers: | ... | @@ -212,8 +212,8 @@ importers: |
| 212 | specifier: ~56.0.18 | 212 | specifier: ~56.0.18 |
| 213 | version: 56.0.18(3cdc0dde9f93166d952f1e1bd0cb25c0) | 213 | version: 56.0.18(3cdc0dde9f93166d952f1e1bd0cb25c0) |
| 214 | '@legendapp/list': | 214 | '@legendapp/list': |
| 215 | specifier: 3.2.0 | 215 | specifier: 3.3.3 |
| 216 | 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) | 216 | 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) |
| 217 | '@noble/curves': | 217 | '@noble/curves': |
| 218 | specifier: 'catalog:' | 218 | specifier: 'catalog:' |
| 219 | version: 1.9.1 | 219 | version: 1.9.1 |
| ... | @@ -545,7 +545,7 @@ importers: | ... | @@ -545,7 +545,7 @@ importers: |
| 545 | version: 0.9.0 | 545 | version: 0.9.0 |
| 546 | '@legendapp/list': | 546 | '@legendapp/list': |
| 547 | specifier: 3.2.0 | 547 | specifier: 3.2.0 |
| 548 | version: 3.2.0(patch_hash=9203ec3db86574a31e157a265cefd8997b259cc420a0c4c61c580c80ed8cf78d)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) | 548 | version: 3.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) |
| 549 | '@lexical/react': | 549 | '@lexical/react': |
| 550 | specifier: ^0.41.0 | 550 | specifier: ^0.41.0 |
| 551 | version: 0.41.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(yjs@13.6.31) | 551 | 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: | ... | @@ -2975,6 +2975,18 @@ packages: |
| 2975 | react-native: | 2975 | react-native: |
| 2976 | optional: true | 2976 | optional: true |
| 2977 | 2977 | ||
| 2978 | '@legendapp/list@3.3.3': | ||
| 2979 | resolution: {integrity: sha512-p3g4xG6f//s4XQKhuus2189GCQgOHEIbJXHePqeDxj+6UQQQyij4YBjyArNSCgqoP0c03sxDPSOuCFB128Ql6g==} | ||
| 2980 | peerDependencies: | ||
| 2981 | react: '*' | ||
| 2982 | react-dom: '*' | ||
| 2983 | react-native: '*' | ||
| 2984 | peerDependenciesMeta: | ||
| 2985 | react-dom: | ||
| 2986 | optional: true | ||
| 2987 | react-native: | ||
| 2988 | optional: true | ||
| 2989 | |||
| 2978 | '@lexical/clipboard@0.41.0': | 2990 | '@lexical/clipboard@0.41.0': |
| 2979 | resolution: {integrity: sha512-Ex5lPkb4NBBX1DCPzOAIeHBJFH1bJcmATjREaqpnTfxCbuOeQkt44wchezUA0oDl+iAxNZ3+pLLWiUju9icoSA==} | 2991 | resolution: {integrity: sha512-Ex5lPkb4NBBX1DCPzOAIeHBJFH1bJcmATjREaqpnTfxCbuOeQkt44wchezUA0oDl+iAxNZ3+pLLWiUju9icoSA==} |
| 2980 | 2992 | ||
| ... | @@ -12982,7 +12994,14 @@ snapshots: | ... | @@ -12982,7 +12994,14 @@ snapshots: |
| 12982 | dependencies: | 12994 | dependencies: |
| 12983 | jsbi: 4.3.2 | 12995 | jsbi: 4.3.2 |
| 12984 | 12996 | ||
| 12985 | '@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)': | 12997 | '@legendapp/list@3.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': |
| 12998 | dependencies: | ||
| 12999 | react: 19.2.6 | ||
| 13000 | use-sync-external-store: 1.6.0(react@19.2.6) | ||
| 13001 | optionalDependencies: | ||
| 13002 | react-dom: 19.2.6(react@19.2.6) | ||
| 13003 | |||
| 13004 | '@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)': | ||
| 12986 | dependencies: | 13005 | dependencies: |
| 12987 | react: 19.2.3 | 13006 | react: 19.2.3 |
| 12988 | use-sync-external-store: 1.6.0(react@19.2.3) | 13007 | use-sync-external-store: 1.6.0(react@19.2.3) |
| ... | @@ -12990,13 +13009,6 @@ snapshots: | ... | @@ -12990,13 +13009,6 @@ snapshots: |
| 12990 | react-dom: 19.2.3(react@19.2.3) | 13009 | react-dom: 19.2.3(react@19.2.3) |
| 12991 | 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) | 13010 | 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) |
| 12992 | 13011 | ||
| 12993 | '@legendapp/list@3.2.0(patch_hash=9203ec3db86574a31e157a265cefd8997b259cc420a0c4c61c580c80ed8cf78d)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': | ||
| 12994 | dependencies: | ||
| 12995 | react: 19.2.6 | ||
| 12996 | use-sync-external-store: 1.6.0(react@19.2.6) | ||
| 12997 | optionalDependencies: | ||
| 12998 | react-dom: 19.2.6(react@19.2.6) | ||
| 12999 | |||
| 13000 | '@lexical/clipboard@0.41.0': | 13012 | '@lexical/clipboard@0.41.0': |
| 13001 | dependencies: | 13013 | dependencies: |
| 13002 | '@lexical/html': 0.41.0 | 13014 | '@lexical/html': 0.41.0 |
pnpm-workspace.yaml+1-1| ... | @@ -127,7 +127,7 @@ patchedDependencies: | ... | @@ -127,7 +127,7 @@ patchedDependencies: |
| 127 | "@effect/vitest@4.0.0-beta.102": patches/@effect__vitest@4.0.0-beta.102.patch | 127 | "@effect/vitest@4.0.0-beta.102": patches/@effect__vitest@4.0.0-beta.102.patch |
| 128 | "@expo/metro-config@56.0.14": patches/@expo%2Fmetro-config@56.0.14.patch | 128 | "@expo/metro-config@56.0.14": patches/@expo%2Fmetro-config@56.0.14.patch |
| 129 | "@ff-labs/fff-node@0.9.4": patches/@ff-labs__fff-node@0.9.4.patch | 129 | "@ff-labs/fff-node@0.9.4": patches/@ff-labs__fff-node@0.9.4.patch |
| 130 | "@legendapp/list@3.2.0": patches/@legendapp__list@3.2.0.patch | 130 | "@legendapp/list@3.3.3": patches/@legendapp__list@3.3.3.patch |
| 131 | "@pierre/diffs@1.3.0-beta.10": patches/@pierre%2Fdiffs@1.3.0-beta.10.patch | 131 | "@pierre/diffs@1.3.0-beta.10": patches/@pierre%2Fdiffs@1.3.0-beta.10.patch |
| 132 | "@react-native-menu/menu@2.0.0": patches/@react-native-menu__menu@2.0.0.patch | 132 | "@react-native-menu/menu@2.0.0": patches/@react-native-menu__menu@2.0.0.patch |
| 133 | "@react-navigation/native-stack@7.17.6": patches/@react-navigation%2Fnative-stack@7.17.6.patch | 133 | "@react-navigation/native-stack@7.17.6": patches/@react-navigation%2Fnative-stack@7.17.6.patch |