From 07777abb9a1d06dc2adff40048d50b267216b13f Mon Sep 17 00:00:00 2001 From: Zero Two Date: Tue, 15 Sep 2026 04:01:01 +0000 Subject: [PATCH 1/3] fix(mobile): hide scroll-to-end button while dragging at the bottom --- .../features/threads/ThreadDetailScreen.tsx | 12 +++++- .../src/features/threads/ThreadFeed.tsx | 13 +++++++ .../threads/thread-feed-live-follow.test.ts | 38 +++++++++++++++++++ .../threads/thread-feed-live-follow.ts | 8 ++++ 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index 2f4ee67a187e..78ef6d41aa39 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -106,7 +106,10 @@ import { } from "./ThreadComposer"; import { ThreadFeed } from "./ThreadFeed"; import type { ThreadContentPresentation } from "./threadContentPresentation"; -import { resolveThreadFeedSubmissionAnchor } from "./thread-feed-live-follow"; +import { + resolveThreadFeedSubmissionAnchor, + shouldShowThreadFeedScrollToEnd, +} from "./thread-feed-live-follow"; export interface ThreadDetailScreenProps { readonly selectedThread: OrchestrationThreadShell; @@ -317,6 +320,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread const [anchorMessageId, setAnchorMessageId] = useState(null); const [submittedMessageId, setSubmittedMessageId] = useState(null); const [endFollowEnabled, setEndFollowEnabled] = useState(true); + const [isAtEnd, setIsAtEnd] = useState(true); // Android keys the safe-area padding on keyboard visibility (#5988): the // back gesture closes the keyboard while the editor stays focused, and a // focus-keyed inset would leave the toolbar under the gesture bar. iOS must @@ -679,6 +683,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread setSubmittedMessageId(null); lastScrolledSubmittedMessageIdRef.current = null; setEndFollowEnabled(true); + setIsAtEnd(true); freeze.set(false); }, [freeze, selectedThreadKey]); @@ -818,7 +823,9 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread }); }, [freeze, scrollMessageToEnd]); - const showScrollToEndButton = contentPresentationKind === "ready" && !endFollowEnabled; + const showScrollToEndButton = + contentPresentationKind === "ready" && + shouldShowThreadFeedScrollToEnd({ endFollowEnabled, isAtEnd }); const { themeAppearance, materialYouStyleLayoutActive } = useAppearancePreferences(); const isDarkMode = themeAppearance === "dark"; @@ -899,6 +906,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread usesAutomaticContentInsets={props.usesAutomaticContentInsets} onHeaderMaterialVisibilityChange={props.onHeaderMaterialVisibilityChange} onEndFollowEnabledChange={setEndFollowEnabled} + onIsAtEndChange={setIsAtEnd} skills={selectedProviderSkills} onUseArtifactTemplate={handleUseArtifactTemplate} loadEarlier={props.loadEarlier ?? null} diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index 66c4359b8190..7eed95f07e18 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -260,6 +260,7 @@ export interface ThreadFeedProps { readonly usesAutomaticContentInsets?: boolean; readonly onHeaderMaterialVisibilityChange?: (visible: boolean) => void; readonly onEndFollowEnabledChange?: (enabled: boolean) => void; + readonly onIsAtEndChange?: (isAtEnd: boolean) => void; readonly skills?: ReadonlyArray; readonly onUseArtifactTemplate?: (template: CodexArtifactTemplate) => void; /** Non-null when older turns exist beyond the loaded window. */ @@ -2421,6 +2422,18 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { } }, [listMountKey, props.contentInsetEndAdjustment, props.listRef]); + // Report edge transitions, including content/inset changes without a scroll, + // without rerendering the screen for each scroll event. + useLayoutEffect(() => { + const listState = props.listRef.current?.getState(); + const onIsAtEndChange = props.onIsAtEndChange; + if (!listState || !onIsAtEndChange) { + return; + } + onIsAtEndChange(listState.isAtEnd); + return listState.listen("isAtEnd", onIsAtEndChange); + }, [listMountKey, props.listRef, props.onIsAtEndChange]); + const anchoredEndSpace = useMemo( () => resolveChatListAnchoredEndSpace( diff --git a/apps/mobile/src/features/threads/thread-feed-live-follow.test.ts b/apps/mobile/src/features/threads/thread-feed-live-follow.test.ts index 13e81130823a..1db80e1b53c1 100644 --- a/apps/mobile/src/features/threads/thread-feed-live-follow.test.ts +++ b/apps/mobile/src/features/threads/thread-feed-live-follow.test.ts @@ -5,6 +5,7 @@ import { resolveThreadFeedSubmissionAnchor, resolveThreadWorkGroupInitialScroll, shouldFollowThreadWorkGroupAppend, + shouldShowThreadFeedScrollToEnd, } from "./thread-feed-live-follow"; describe("tool-group scroll restoration", () => { @@ -144,6 +145,43 @@ describe("resolveThreadFeedSubmissionAnchor", () => { }); }); +describe("scroll-to-end visibility", () => { + it("keeps the button hidden when dragging further down at the bottom", () => { + let endFollowEnabled = resolveThreadFeedLiveFollow(true, { type: "user-scroll-begin" }); + expect(endFollowEnabled).toBe(false); + expect(shouldShowThreadFeedScrollToEnd({ endFollowEnabled, isAtEnd: true })).toBe(false); + + endFollowEnabled = resolveThreadFeedLiveFollow(endFollowEnabled, { + type: "scroll", + isAtEnd: true, + userScrollSessionActive: true, + }); + expect(shouldShowThreadFeedScrollToEnd({ endFollowEnabled, isAtEnd: true })).toBe(false); + }); + + it("shows the button after scrolling up and hides it on returning to the bottom", () => { + let endFollowEnabled = resolveThreadFeedLiveFollow(true, { type: "user-scroll-begin" }); + endFollowEnabled = resolveThreadFeedLiveFollow(endFollowEnabled, { + type: "scroll", + isAtEnd: false, + userScrollSessionActive: true, + }); + expect(shouldShowThreadFeedScrollToEnd({ endFollowEnabled, isAtEnd: false })).toBe(true); + + endFollowEnabled = resolveThreadFeedLiveFollow(endFollowEnabled, { + type: "scroll", + isAtEnd: true, + userScrollSessionActive: true, + }); + expect(endFollowEnabled).toBe(false); + expect(shouldShowThreadFeedScrollToEnd({ endFollowEnabled, isAtEnd: true })).toBe(false); + }); + + it("keeps the button hidden while following streaming content", () => { + expect(shouldShowThreadFeedScrollToEnd({ endFollowEnabled: true, isAtEnd: false })).toBe(false); + }); +}); + describe("resolveThreadFeedLiveFollow", () => { it("pauses immediately when the user starts scrolling", () => { expect(resolveThreadFeedLiveFollow(true, { type: "user-scroll-begin" })).toBe(false); diff --git a/apps/mobile/src/features/threads/thread-feed-live-follow.ts b/apps/mobile/src/features/threads/thread-feed-live-follow.ts index 431dda7550d1..b0b257d5eb36 100644 --- a/apps/mobile/src/features/threads/thread-feed-live-follow.ts +++ b/apps/mobile/src/features/threads/thread-feed-live-follow.ts @@ -65,6 +65,14 @@ export function resolveThreadFeedSubmissionAnchor(input: { return input.queuedMessageCount > 0 ? null : input.submittedMessageId; } +export function shouldShowThreadFeedScrollToEnd(input: { + readonly endFollowEnabled: boolean; + readonly isAtEnd: boolean; +}) { + // A drag pauses live-follow before moving away from the end. + return !input.endFollowEnabled && !input.isAtEnd; +} + export function resolveThreadFeedLiveFollow( current: boolean, event: ThreadFeedLiveFollowEvent, From 6e168cbb6ea84c25d0b7e90c55f3e4ecdff2d062 Mon Sep 17 00:00:00 2001 From: Zero Two Date: Tue, 15 Sep 2026 13:37:23 +0000 Subject: [PATCH 2/3] fix(mobile): refresh end position after work row resizing --- .../threads/legend-list-end-position.test.ts | 109 ++++++++++++++++++ patches/@legendapp__list@3.3.5.patch | 94 +++++++++------ pnpm-lock.yaml | 10 +- 3 files changed, 170 insertions(+), 43 deletions(-) create mode 100644 apps/mobile/src/features/threads/legend-list-end-position.test.ts diff --git a/apps/mobile/src/features/threads/legend-list-end-position.test.ts b/apps/mobile/src/features/threads/legend-list-end-position.test.ts new file mode 100644 index 000000000000..87873e44ccd5 --- /dev/null +++ b/apps/mobile/src/features/threads/legend-list-end-position.test.ts @@ -0,0 +1,109 @@ +// @effect-diagnostics nodeBuiltinImport:off - exercises the installed native bundle without React Native. +import * as NodeFS from "node:fs"; +import * as NodeVM from "node:vm"; +import { describe, expect, it, vi } from "vite-plus/test"; + +import { + resolveThreadFeedLiveFollow, + shouldShowThreadFeedScrollToEnd, +} from "./thread-feed-live-follow"; + +function createList(bundle: string) { + const source = NodeFS.readFileSync( + new URL(`../../../node_modules/@legendapp/list/${bundle}`, import.meta.url), + "utf8", + ); + const slice = (start: string, end: string) => + source.slice(source.indexOf(start), source.indexOf(end, source.indexOf(start))); + const state = { queuedInitialLayout: true, scroll: 400, scrollLength: 800 }; + const ctx = { + state, + values: new Map([["isAtEnd", true]]), + listeners: new Map void>>(), + }; + let contentSize = 1200; + const maintainScrollAtEnd = vi.fn(); + const api = NodeVM.runInNewContext( + [ + slice("function listen$(", "function listenPosition$("), + slice("function getIsAtEnd(", "// src/utils/checkAtBottom.ts"), + slice("function flushItemSizeUpdates(", "function updateItemSizes("), + "({ listen$, getIsAtEnd, flushItemSizeUpdates })", + ].join("\n"), + { + getContentSize: () => contentSize, + getContentInsetEnd: () => 0, + EDGE_POSITION_EPSILON: 1, + maybeUpdateAnchoredEndSpace: vi.fn(), + doMaintainScrollAtEnd: maintainScrollAtEnd, + }, + ) as { + listen$: (context: typeof ctx, key: string, callback: (value: boolean) => void) => () => void; + getIsAtEnd: (context: typeof ctx) => boolean; + flushItemSizeUpdates: ( + context: typeof ctx, + result: { didChange: boolean; shouldMaintainScrollAtEnd: boolean }, + ) => void; + }; + return { + state, + maintainScrollAtEnd, + isAtEnd: () => api.getIsAtEnd(ctx), + listen: (callback: (value: boolean) => void) => api.listen$(ctx, "isAtEnd", callback), + resize(size: number, follow = false) { + const didChange = contentSize !== size; + contentSize = size; + api.flushItemSizeUpdates(ctx, { didChange, shouldMaintainScrollAtEnd: follow }); + }, + }; +} + +for (const bundle of ["react-native.js", "react-native.mjs"]) { + describe(`end position after row layout (${bundle})`, () => { + it("shows the button after expanding a work row without scrolling and hides it on collapse", () => { + const list = createList(bundle); + let isAtEnd = list.isAtEnd(); + list.listen((value) => { + isAtEnd = value; + }); + list.resize(1600); + const endFollowEnabled = resolveThreadFeedLiveFollow(true, { + type: "disclosure-settled", + isAtEnd: list.isAtEnd(), + userScrollSessionActive: false, + }); + expect(endFollowEnabled).toBe(false); + expect(shouldShowThreadFeedScrollToEnd({ endFollowEnabled, isAtEnd })).toBe(true); + expect(list.state.scroll).toBe(400); + expect(list.maintainScrollAtEnd).not.toHaveBeenCalled(); + + list.resize(1200); + expect(shouldShowThreadFeedScrollToEnd({ endFollowEnabled, isAtEnd })).toBe(false); + }); + + it("reports only edge transitions and unsubscribes", () => { + const list = createList(bundle); + const changed = vi.fn(); + const unsubscribe = list.listen(changed); + list.resize(1600); + list.resize(1800); + list.resize(1800); + list.resize(1200); + expect(changed.mock.calls).toEqual([[false], [true]]); + unsubscribe(); + list.resize(1600); + expect(changed).toHaveBeenCalledTimes(2); + }); + + it("keeps overscroll at the end and preserves requested live-follow", () => { + const list = createList(bundle); + list.state.scroll = 600; + const changed = vi.fn(); + list.listen(changed); + list.resize(1300, true); + expect(list.isAtEnd()).toBe(true); + expect(changed).not.toHaveBeenCalled(); + expect(list.maintainScrollAtEnd).toHaveBeenCalledTimes(1); + }); + }); +} diff --git a/patches/@legendapp__list@3.3.5.patch b/patches/@legendapp__list@3.3.5.patch index 88b739824b01..3d76396b0bb0 100644 --- a/patches/@legendapp__list@3.3.5.patch +++ b/patches/@legendapp__list@3.3.5.patch @@ -241,7 +241,7 @@ index ce1fe00..3ccf6f1 100644 * Number of columns to render items in. * @default 1 diff --git a/react-native.js b/react-native.js -index b3c5a30..5ac6bbe 100644 +index b3c5a30..6176e5b 100644 --- a/react-native.js +++ b/react-native.js @@ -717,6 +717,15 @@ function hasActiveInitialScroll(state) { @@ -606,7 +606,16 @@ index b3c5a30..5ac6bbe 100644 } return nextSize; } -@@ -5715,6 +5895,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ +@@ -4770,6 +4950,8 @@ function flushItemSizeUpdates(ctx, result) { + } + if (result.didChange) { + maybeUpdateAnchoredEndSpace(ctx); ++ // Row expansion can move the end without a native scroll event. ++ set$(ctx, "isAtEnd", getIsAtEnd(ctx)); + } + if (result.didChange && result.shouldMaintainScrollAtEnd) { + doMaintainScrollAtEnd(ctx); +@@ -5715,6 +5897,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ horizontal }) { const ctx = useStateContext(); @@ -614,7 +623,7 @@ index b3c5a30..5ac6bbe 100644 const columnWrapperStyle = ctx.columnWrapperStyle; const animSize = useValue$("totalSize"); const [readyToRender, numColumns, otherAxisSize = 0] = useArr$(["readyToRender", "numColumns", "otherAxisSize"]); -@@ -5725,6 +5906,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ +@@ -5725,6 +5908,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ opacity: isVisible ? 1 : 0, width: animSize } : { height: animSize, minWidth: otherAxisSize, opacity: isVisible ? 1 : 0 }; @@ -628,7 +637,7 @@ index b3c5a30..5ac6bbe 100644 if (columnWrapperStyle) { const { columnGap, rowGap, gap } = columnWrapperStyle; const gapX = columnGap || gap || 0; -@@ -5745,7 +5933,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ +@@ -5745,7 +5935,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ } } } @@ -638,7 +647,7 @@ index b3c5a30..5ac6bbe 100644 }); var Containers = typedMemo(function Containers2({ freshDataTransitionEpoch, -@@ -5896,7 +6085,12 @@ var StyleSheet = ReactNative.StyleSheet; +@@ -5896,7 +6087,12 @@ var StyleSheet = ReactNative.StyleSheet; // src/components/ListComponent.tsx var AlignItemsAtEndSpacer = typedMemo(function AlignItemsAtEndSpacer2({ horizontal }) { @@ -651,7 +660,7 @@ index b3c5a30..5ac6bbe 100644 if (alignItemsAtEndPadding <= 0) { return null; } -@@ -5929,8 +6123,12 @@ var ListComponent = typedMemo(function ListComponent2({ +@@ -5929,8 +6125,12 @@ var ListComponent = typedMemo(function ListComponent2({ refScrollView, renderScrollComponent, onLayoutFooter, @@ -664,7 +673,7 @@ index b3c5a30..5ac6bbe 100644 scrollAdjustHandler, snapToIndices, stickyHeaderConfig, -@@ -6001,7 +6199,17 @@ var ListComponent = typedMemo(function ListComponent2({ +@@ -6001,7 +6201,17 @@ var ListComponent = typedMemo(function ListComponent2({ SnapOrScroll, { ...rest, @@ -683,7 +692,7 @@ index b3c5a30..5ac6bbe 100644 contentContainerStyle: [ horizontal ? { height: "100%" } : {}, contentContainerStyle, -@@ -6010,7 +6218,10 @@ var ListComponent = typedMemo(function ListComponent2({ +@@ -6010,7 +6220,10 @@ var ListComponent = typedMemo(function ListComponent2({ ], contentOffset: initialContentOffset !== void 0 ? horizontal ? { x: initialContentOffset, y: 0 } : { x: 0, y: initialContentOffset } : void 0, horizontal, @@ -695,7 +704,7 @@ index b3c5a30..5ac6bbe 100644 onLayout, onScroll: onScroll2, ref: refScrollView, -@@ -6751,7 +6962,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { +@@ -6751,7 +6964,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { endBuffered: state.endBuffered, getAverageItemSizes: () => getAverageItemSizes(state), indexByKey: (key) => state.indexByKey.get(key), @@ -704,7 +713,7 @@ index b3c5a30..5ac6bbe 100644 isAtStart: peek$(ctx, "isAtStart"), isEndReached: state.isEndReached, isNearEnd: peek$(ctx, "isNearEnd"), -@@ -7075,6 +7286,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7075,6 +7288,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded dataVersion, drawDistance = 250, contentInsetEndAdjustment, @@ -712,7 +721,7 @@ index b3c5a30..5ac6bbe 100644 estimatedItemSize = 100, estimatedListSize, extraData, -@@ -7132,10 +7344,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7132,10 +7346,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded const animatedPropsInternal = props.animatedPropsInternal; const anchoredEndSpaceOwner = (_a3 = props.anchoredEndSpaceOwnerInternal) != null ? _a3 : "list"; const positionComponentInternal = props.positionComponentInternal; @@ -725,7 +734,7 @@ index b3c5a30..5ac6bbe 100644 stickyPositionComponentInternal: _stickyPositionComponentInternal, ...restProps } = rest; -@@ -7200,7 +7414,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7200,7 +7416,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded const combinedRef = useCombinedRef(refScroller, refScrollView); const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString()); const stickyHeaderIndices = stickyHeaderIndicesProp; @@ -734,7 +743,7 @@ index b3c5a30..5ac6bbe 100644 const previousContentInsetEndAdjustmentRef = React2.useRef(contentInsetEndAdjustmentResolved); const alwaysRenderIndices = React2.useMemo(() => { const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex); -@@ -7341,6 +7555,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7341,6 +7557,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded contentContainerAlignItems: contentContainerStyle.alignItems, contentInset, contentInsetEndAdjustment: contentInsetEndAdjustmentResolved, @@ -742,7 +751,7 @@ index b3c5a30..5ac6bbe 100644 data: dataProp, dataKey, dataVersion, -@@ -7372,6 +7587,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7372,6 +7589,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded renderItem, rtl, snapToIndices, @@ -750,7 +759,7 @@ index b3c5a30..5ac6bbe 100644 stickyHeaderIndicesArr: stickyHeaderIndices != null ? stickyHeaderIndices : [], stickyHeaderIndicesSet: React2.useMemo(() => new Set(stickyHeaderIndices != null ? stickyHeaderIndices : []), [stickyHeaderIndices == null ? void 0 : stickyHeaderIndices.join(",")]), stickyPositionComponentInternal, -@@ -7423,6 +7639,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7423,6 +7641,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded return void 0; } const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll); @@ -764,7 +773,7 @@ index b3c5a30..5ac6bbe 100644 return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset; }, [usesBootstrapInitialScroll]); React2.useLayoutEffect(() => { -@@ -7547,6 +7770,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7547,6 +7772,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded [ dataKey, dataVersion, @@ -772,7 +781,7 @@ index b3c5a30..5ac6bbe 100644 memoizedLastItemKeys.join(","), numColumnsProp, nextScrollAxisGap, -@@ -7643,6 +7867,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7643,6 +7869,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded () => ({ getRenderedItem: (key) => getRenderedItem(ctx, key), onMomentumScrollEnd: (event) => { @@ -780,7 +789,7 @@ index b3c5a30..5ac6bbe 100644 checkFinishedScrollFallback(ctx); if (state.props.onMomentumScrollEnd) { state.props.onMomentumScrollEnd(event); -@@ -7651,6 +7876,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7651,6 +7878,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScroll: (event) => onScroll(ctx, event), onScrollBeginDrag: (event) => { var _a4, _b2; @@ -789,7 +798,7 @@ index b3c5a30..5ac6bbe 100644 prepareReachedEdgeForNextUserScroll(ctx); (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); }, -@@ -7676,11 +7903,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7676,11 +7905,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded ListFooterComponent, ListFooterComponentStyle, ListHeaderComponent, @@ -809,7 +818,7 @@ index b3c5a30..5ac6bbe 100644 recycleItems, refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2__namespace.cloneElement(refreshControlElement, { diff --git a/react-native.mjs b/react-native.mjs -index 40e87cd..93aac74 100644 +index 40e87cd..7ca12cb 100644 --- a/react-native.mjs +++ b/react-native.mjs @@ -696,6 +696,15 @@ function hasActiveInitialScroll(state) { @@ -1174,7 +1183,16 @@ index 40e87cd..93aac74 100644 } return nextSize; } -@@ -5694,6 +5874,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ +@@ -4749,6 +4929,8 @@ function flushItemSizeUpdates(ctx, result) { + } + if (result.didChange) { + maybeUpdateAnchoredEndSpace(ctx); ++ // Row expansion can move the end without a native scroll event. ++ set$(ctx, "isAtEnd", getIsAtEnd(ctx)); + } + if (result.didChange && result.shouldMaintainScrollAtEnd) { + doMaintainScrollAtEnd(ctx); +@@ -5694,6 +5876,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ horizontal }) { const ctx = useStateContext(); @@ -1182,7 +1200,7 @@ index 40e87cd..93aac74 100644 const columnWrapperStyle = ctx.columnWrapperStyle; const animSize = useValue$("totalSize"); const [readyToRender, numColumns, otherAxisSize = 0] = useArr$(["readyToRender", "numColumns", "otherAxisSize"]); -@@ -5704,6 +5885,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ +@@ -5704,6 +5887,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ opacity: isVisible ? 1 : 0, width: animSize } : { height: animSize, minWidth: otherAxisSize, opacity: isVisible ? 1 : 0 }; @@ -1196,7 +1214,7 @@ index 40e87cd..93aac74 100644 if (columnWrapperStyle) { const { columnGap, rowGap, gap } = columnWrapperStyle; const gapX = columnGap || gap || 0; -@@ -5724,7 +5912,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ +@@ -5724,7 +5914,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ } } } @@ -1206,7 +1224,7 @@ index 40e87cd..93aac74 100644 }); var Containers = typedMemo(function Containers2({ freshDataTransitionEpoch, -@@ -5875,7 +6064,12 @@ var StyleSheet = StyleSheet$1; +@@ -5875,7 +6066,12 @@ var StyleSheet = StyleSheet$1; // src/components/ListComponent.tsx var AlignItemsAtEndSpacer = typedMemo(function AlignItemsAtEndSpacer2({ horizontal }) { @@ -1219,7 +1237,7 @@ index 40e87cd..93aac74 100644 if (alignItemsAtEndPadding <= 0) { return null; } -@@ -5908,8 +6102,12 @@ var ListComponent = typedMemo(function ListComponent2({ +@@ -5908,8 +6104,12 @@ var ListComponent = typedMemo(function ListComponent2({ refScrollView, renderScrollComponent, onLayoutFooter, @@ -1232,7 +1250,7 @@ index 40e87cd..93aac74 100644 scrollAdjustHandler, snapToIndices, stickyHeaderConfig, -@@ -5980,7 +6178,17 @@ var ListComponent = typedMemo(function ListComponent2({ +@@ -5980,7 +6180,17 @@ var ListComponent = typedMemo(function ListComponent2({ SnapOrScroll, { ...rest, @@ -1251,7 +1269,7 @@ index 40e87cd..93aac74 100644 contentContainerStyle: [ horizontal ? { height: "100%" } : {}, contentContainerStyle, -@@ -5989,7 +6197,10 @@ var ListComponent = typedMemo(function ListComponent2({ +@@ -5989,7 +6199,10 @@ var ListComponent = typedMemo(function ListComponent2({ ], contentOffset: initialContentOffset !== void 0 ? horizontal ? { x: initialContentOffset, y: 0 } : { x: 0, y: initialContentOffset } : void 0, horizontal, @@ -1263,7 +1281,7 @@ index 40e87cd..93aac74 100644 onLayout, onScroll: onScroll2, ref: refScrollView, -@@ -6730,7 +6941,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { +@@ -6730,7 +6943,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { endBuffered: state.endBuffered, getAverageItemSizes: () => getAverageItemSizes(state), indexByKey: (key) => state.indexByKey.get(key), @@ -1272,7 +1290,7 @@ index 40e87cd..93aac74 100644 isAtStart: peek$(ctx, "isAtStart"), isEndReached: state.isEndReached, isNearEnd: peek$(ctx, "isNearEnd"), -@@ -7054,6 +7265,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7054,6 +7267,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded dataVersion, drawDistance = 250, contentInsetEndAdjustment, @@ -1280,7 +1298,7 @@ index 40e87cd..93aac74 100644 estimatedItemSize = 100, estimatedListSize, extraData, -@@ -7111,10 +7323,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7111,10 +7325,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded const animatedPropsInternal = props.animatedPropsInternal; const anchoredEndSpaceOwner = (_a3 = props.anchoredEndSpaceOwnerInternal) != null ? _a3 : "list"; const positionComponentInternal = props.positionComponentInternal; @@ -1293,7 +1311,7 @@ index 40e87cd..93aac74 100644 stickyPositionComponentInternal: _stickyPositionComponentInternal, ...restProps } = rest; -@@ -7179,7 +7393,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7179,7 +7395,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded const combinedRef = useCombinedRef(refScroller, refScrollView); const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString()); const stickyHeaderIndices = stickyHeaderIndicesProp; @@ -1302,7 +1320,7 @@ index 40e87cd..93aac74 100644 const previousContentInsetEndAdjustmentRef = useRef(contentInsetEndAdjustmentResolved); const alwaysRenderIndices = useMemo(() => { const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex); -@@ -7320,6 +7534,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7320,6 +7536,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded contentContainerAlignItems: contentContainerStyle.alignItems, contentInset, contentInsetEndAdjustment: contentInsetEndAdjustmentResolved, @@ -1310,7 +1328,7 @@ index 40e87cd..93aac74 100644 data: dataProp, dataKey, dataVersion, -@@ -7351,6 +7566,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7351,6 +7568,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded renderItem, rtl, snapToIndices, @@ -1318,7 +1336,7 @@ index 40e87cd..93aac74 100644 stickyHeaderIndicesArr: stickyHeaderIndices != null ? stickyHeaderIndices : [], stickyHeaderIndicesSet: useMemo(() => new Set(stickyHeaderIndices != null ? stickyHeaderIndices : []), [stickyHeaderIndices == null ? void 0 : stickyHeaderIndices.join(",")]), stickyPositionComponentInternal, -@@ -7402,6 +7618,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7402,6 +7620,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded return void 0; } const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll); @@ -1332,7 +1350,7 @@ index 40e87cd..93aac74 100644 return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset; }, [usesBootstrapInitialScroll]); useLayoutEffect(() => { -@@ -7526,6 +7749,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7526,6 +7751,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded [ dataKey, dataVersion, @@ -1340,7 +1358,7 @@ index 40e87cd..93aac74 100644 memoizedLastItemKeys.join(","), numColumnsProp, nextScrollAxisGap, -@@ -7622,6 +7846,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7622,6 +7848,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded () => ({ getRenderedItem: (key) => getRenderedItem(ctx, key), onMomentumScrollEnd: (event) => { @@ -1348,7 +1366,7 @@ index 40e87cd..93aac74 100644 checkFinishedScrollFallback(ctx); if (state.props.onMomentumScrollEnd) { state.props.onMomentumScrollEnd(event); -@@ -7630,6 +7855,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7630,6 +7857,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScroll: (event) => onScroll(ctx, event), onScrollBeginDrag: (event) => { var _a4, _b2; @@ -1357,7 +1375,7 @@ index 40e87cd..93aac74 100644 prepareReachedEdgeForNextUserScroll(ctx); (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); }, -@@ -7655,11 +7882,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7655,11 +7884,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded ListFooterComponent, ListFooterComponentStyle, ListHeaderComponent, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7254a454328c..45e067635127 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,7 +89,7 @@ patchedDependencies: '@effect/vitest@4.0.0-rc.112': a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b '@expo/metro-config@57.0.12': 96f1a75347e6ea02dc4b7034ace815d8ee39e18b8166ebfb573d9e58328f0dc2 '@ff-labs/fff-node@0.9.4': c4e3cc2420ceb9dc650f9d189e9c24baf7e83f342998bacda5f459a4ce7927a8 - '@legendapp/list@3.3.5': 680cc6a5c5b4a4032e467e7b3fde22f89a84c0ee2e6eac6fda737d6277cc0806 + '@legendapp/list@3.3.5': 0bbbd4b76670b65cf0912ba5451047f7ce9ba0dbe13a34987fc235de4c657100 '@pierre/diffs@1.3.0-beta.10': 0ccee155b93b63d810e2c1a40c1fd676fb6fbcfa72cf6430dcedf1a3ae475ab4 '@react-native-ai/apple@0.12.0': 2d09870c2848d185cb05b53ed823a46e12dba519324d8dd8e584e28731990f9d '@react-native-menu/menu@2.0.0': f63d256bf6a97a873b5e628eb595bd6ef0075ddd5bdd890fc920f7a6024290dd @@ -246,7 +246,7 @@ importers: version: 57.0.14(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@legendapp/list': specifier: 'catalog:' - version: 3.3.5(patch_hash=680cc6a5c5b4a4032e467e7b3fde22f89a84c0ee2e6eac6fda737d6277cc0806)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 3.3.5(patch_hash=0bbbd4b76670b65cf0912ba5451047f7ce9ba0dbe13a34987fc235de4c657100)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@material/material-color-utilities': specifier: 0.3.0 version: 0.3.0 @@ -599,7 +599,7 @@ importers: version: 0.9.0 '@legendapp/list': specifier: 'catalog:' - version: 3.3.5(patch_hash=680cc6a5c5b4a4032e467e7b3fde22f89a84c0ee2e6eac6fda737d6277cc0806)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 3.3.5(patch_hash=0bbbd4b76670b65cf0912ba5451047f7ce9ba0dbe13a34987fc235de4c657100)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@lexical/react': specifier: ^0.41.0 version: 0.41.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(yjs@13.6.31) @@ -14024,7 +14024,7 @@ snapshots: dependencies: jsbi: 4.3.2 - '@legendapp/list@3.3.5(patch_hash=680cc6a5c5b4a4032e467e7b3fde22f89a84c0ee2e6eac6fda737d6277cc0806)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@legendapp/list@3.3.5(patch_hash=0bbbd4b76670b65cf0912ba5451047f7ce9ba0dbe13a34987fc235de4c657100)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: react: 19.2.3 use-sync-external-store: 1.6.0(react@19.2.3) @@ -14032,7 +14032,7 @@ snapshots: react-dom: 19.2.3(react@19.2.3) react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - '@legendapp/list@3.3.5(patch_hash=680cc6a5c5b4a4032e467e7b3fde22f89a84c0ee2e6eac6fda737d6277cc0806)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@legendapp/list@3.3.5(patch_hash=0bbbd4b76670b65cf0912ba5451047f7ce9ba0dbe13a34987fc235de4c657100)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: react: 19.2.6 use-sync-external-store: 1.6.0(react@19.2.6) From 9cc7992265bfbee1d8423ea69c1da128b1116914 Mon Sep 17 00:00:00 2001 From: Zero Two Date: Tue, 15 Sep 2026 15:40:51 +0000 Subject: [PATCH 3/3] fix(mobile): reconcile scroll button after disclosure layout --- .../src/features/threads/ThreadFeed.tsx | 7 +- .../threads/legend-list-end-position.test.ts | 109 ------------------ patches/@legendapp__list@3.3.5.patch | 94 ++++++--------- pnpm-lock.yaml | 10 +- 4 files changed, 47 insertions(+), 173 deletions(-) delete mode 100644 apps/mobile/src/features/threads/legend-list-end-position.test.ts diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index 7eed95f07e18..c3718a28252d 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -2422,8 +2422,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { } }, [listMountKey, props.contentInsetEndAdjustment, props.listRef]); - // Report edge transitions, including content/inset changes without a scroll, - // without rerendering the screen for each scroll event. + // Subscribe to edge transitions without updating the screen on every scroll. useLayoutEffect(() => { const listState = props.listRef.current?.getState(); const onIsAtEndChange = props.onIsAtEndChange; @@ -2506,6 +2505,8 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { // Reconcile follow before a later layout or resume can re-pin it. const listState = props.listRef.current?.getState(); if (listState) { + // Row resizing can change the end without notifying the edge subscription. + props.onIsAtEndChange?.(listState.isAtEnd); transitionEndFollow({ type: "disclosure-settled", isAtEnd: listState.isAtEnd, @@ -2518,7 +2519,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { disclosureSettleSecondFrameRef.current = null; }); }); - }, [props.listRef, transitionEndFollow]); + }, [props.listRef, props.onIsAtEndChange, transitionEndFollow]); const suspendEndScrollMaintenanceForDisclosure = useCallback((anchorKey: string | null) => { disclosureAnchorKeyRef.current = anchorKey; diff --git a/apps/mobile/src/features/threads/legend-list-end-position.test.ts b/apps/mobile/src/features/threads/legend-list-end-position.test.ts deleted file mode 100644 index 87873e44ccd5..000000000000 --- a/apps/mobile/src/features/threads/legend-list-end-position.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -// @effect-diagnostics nodeBuiltinImport:off - exercises the installed native bundle without React Native. -import * as NodeFS from "node:fs"; -import * as NodeVM from "node:vm"; -import { describe, expect, it, vi } from "vite-plus/test"; - -import { - resolveThreadFeedLiveFollow, - shouldShowThreadFeedScrollToEnd, -} from "./thread-feed-live-follow"; - -function createList(bundle: string) { - const source = NodeFS.readFileSync( - new URL(`../../../node_modules/@legendapp/list/${bundle}`, import.meta.url), - "utf8", - ); - const slice = (start: string, end: string) => - source.slice(source.indexOf(start), source.indexOf(end, source.indexOf(start))); - const state = { queuedInitialLayout: true, scroll: 400, scrollLength: 800 }; - const ctx = { - state, - values: new Map([["isAtEnd", true]]), - listeners: new Map void>>(), - }; - let contentSize = 1200; - const maintainScrollAtEnd = vi.fn(); - const api = NodeVM.runInNewContext( - [ - slice("function listen$(", "function listenPosition$("), - slice("function getIsAtEnd(", "// src/utils/checkAtBottom.ts"), - slice("function flushItemSizeUpdates(", "function updateItemSizes("), - "({ listen$, getIsAtEnd, flushItemSizeUpdates })", - ].join("\n"), - { - getContentSize: () => contentSize, - getContentInsetEnd: () => 0, - EDGE_POSITION_EPSILON: 1, - maybeUpdateAnchoredEndSpace: vi.fn(), - doMaintainScrollAtEnd: maintainScrollAtEnd, - }, - ) as { - listen$: (context: typeof ctx, key: string, callback: (value: boolean) => void) => () => void; - getIsAtEnd: (context: typeof ctx) => boolean; - flushItemSizeUpdates: ( - context: typeof ctx, - result: { didChange: boolean; shouldMaintainScrollAtEnd: boolean }, - ) => void; - }; - return { - state, - maintainScrollAtEnd, - isAtEnd: () => api.getIsAtEnd(ctx), - listen: (callback: (value: boolean) => void) => api.listen$(ctx, "isAtEnd", callback), - resize(size: number, follow = false) { - const didChange = contentSize !== size; - contentSize = size; - api.flushItemSizeUpdates(ctx, { didChange, shouldMaintainScrollAtEnd: follow }); - }, - }; -} - -for (const bundle of ["react-native.js", "react-native.mjs"]) { - describe(`end position after row layout (${bundle})`, () => { - it("shows the button after expanding a work row without scrolling and hides it on collapse", () => { - const list = createList(bundle); - let isAtEnd = list.isAtEnd(); - list.listen((value) => { - isAtEnd = value; - }); - list.resize(1600); - const endFollowEnabled = resolveThreadFeedLiveFollow(true, { - type: "disclosure-settled", - isAtEnd: list.isAtEnd(), - userScrollSessionActive: false, - }); - expect(endFollowEnabled).toBe(false); - expect(shouldShowThreadFeedScrollToEnd({ endFollowEnabled, isAtEnd })).toBe(true); - expect(list.state.scroll).toBe(400); - expect(list.maintainScrollAtEnd).not.toHaveBeenCalled(); - - list.resize(1200); - expect(shouldShowThreadFeedScrollToEnd({ endFollowEnabled, isAtEnd })).toBe(false); - }); - - it("reports only edge transitions and unsubscribes", () => { - const list = createList(bundle); - const changed = vi.fn(); - const unsubscribe = list.listen(changed); - list.resize(1600); - list.resize(1800); - list.resize(1800); - list.resize(1200); - expect(changed.mock.calls).toEqual([[false], [true]]); - unsubscribe(); - list.resize(1600); - expect(changed).toHaveBeenCalledTimes(2); - }); - - it("keeps overscroll at the end and preserves requested live-follow", () => { - const list = createList(bundle); - list.state.scroll = 600; - const changed = vi.fn(); - list.listen(changed); - list.resize(1300, true); - expect(list.isAtEnd()).toBe(true); - expect(changed).not.toHaveBeenCalled(); - expect(list.maintainScrollAtEnd).toHaveBeenCalledTimes(1); - }); - }); -} diff --git a/patches/@legendapp__list@3.3.5.patch b/patches/@legendapp__list@3.3.5.patch index 3d76396b0bb0..88b739824b01 100644 --- a/patches/@legendapp__list@3.3.5.patch +++ b/patches/@legendapp__list@3.3.5.patch @@ -241,7 +241,7 @@ index ce1fe00..3ccf6f1 100644 * Number of columns to render items in. * @default 1 diff --git a/react-native.js b/react-native.js -index b3c5a30..6176e5b 100644 +index b3c5a30..5ac6bbe 100644 --- a/react-native.js +++ b/react-native.js @@ -717,6 +717,15 @@ function hasActiveInitialScroll(state) { @@ -606,16 +606,7 @@ index b3c5a30..6176e5b 100644 } return nextSize; } -@@ -4770,6 +4950,8 @@ function flushItemSizeUpdates(ctx, result) { - } - if (result.didChange) { - maybeUpdateAnchoredEndSpace(ctx); -+ // Row expansion can move the end without a native scroll event. -+ set$(ctx, "isAtEnd", getIsAtEnd(ctx)); - } - if (result.didChange && result.shouldMaintainScrollAtEnd) { - doMaintainScrollAtEnd(ctx); -@@ -5715,6 +5897,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ +@@ -5715,6 +5895,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ horizontal }) { const ctx = useStateContext(); @@ -623,7 +614,7 @@ index b3c5a30..6176e5b 100644 const columnWrapperStyle = ctx.columnWrapperStyle; const animSize = useValue$("totalSize"); const [readyToRender, numColumns, otherAxisSize = 0] = useArr$(["readyToRender", "numColumns", "otherAxisSize"]); -@@ -5725,6 +5908,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ +@@ -5725,6 +5906,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ opacity: isVisible ? 1 : 0, width: animSize } : { height: animSize, minWidth: otherAxisSize, opacity: isVisible ? 1 : 0 }; @@ -637,7 +628,7 @@ index b3c5a30..6176e5b 100644 if (columnWrapperStyle) { const { columnGap, rowGap, gap } = columnWrapperStyle; const gapX = columnGap || gap || 0; -@@ -5745,7 +5935,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ +@@ -5745,7 +5933,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ } } } @@ -647,7 +638,7 @@ index b3c5a30..6176e5b 100644 }); var Containers = typedMemo(function Containers2({ freshDataTransitionEpoch, -@@ -5896,7 +6087,12 @@ var StyleSheet = ReactNative.StyleSheet; +@@ -5896,7 +6085,12 @@ var StyleSheet = ReactNative.StyleSheet; // src/components/ListComponent.tsx var AlignItemsAtEndSpacer = typedMemo(function AlignItemsAtEndSpacer2({ horizontal }) { @@ -660,7 +651,7 @@ index b3c5a30..6176e5b 100644 if (alignItemsAtEndPadding <= 0) { return null; } -@@ -5929,8 +6125,12 @@ var ListComponent = typedMemo(function ListComponent2({ +@@ -5929,8 +6123,12 @@ var ListComponent = typedMemo(function ListComponent2({ refScrollView, renderScrollComponent, onLayoutFooter, @@ -673,7 +664,7 @@ index b3c5a30..6176e5b 100644 scrollAdjustHandler, snapToIndices, stickyHeaderConfig, -@@ -6001,7 +6201,17 @@ var ListComponent = typedMemo(function ListComponent2({ +@@ -6001,7 +6199,17 @@ var ListComponent = typedMemo(function ListComponent2({ SnapOrScroll, { ...rest, @@ -692,7 +683,7 @@ index b3c5a30..6176e5b 100644 contentContainerStyle: [ horizontal ? { height: "100%" } : {}, contentContainerStyle, -@@ -6010,7 +6220,10 @@ var ListComponent = typedMemo(function ListComponent2({ +@@ -6010,7 +6218,10 @@ var ListComponent = typedMemo(function ListComponent2({ ], contentOffset: initialContentOffset !== void 0 ? horizontal ? { x: initialContentOffset, y: 0 } : { x: 0, y: initialContentOffset } : void 0, horizontal, @@ -704,7 +695,7 @@ index b3c5a30..6176e5b 100644 onLayout, onScroll: onScroll2, ref: refScrollView, -@@ -6751,7 +6964,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { +@@ -6751,7 +6962,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { endBuffered: state.endBuffered, getAverageItemSizes: () => getAverageItemSizes(state), indexByKey: (key) => state.indexByKey.get(key), @@ -713,7 +704,7 @@ index b3c5a30..6176e5b 100644 isAtStart: peek$(ctx, "isAtStart"), isEndReached: state.isEndReached, isNearEnd: peek$(ctx, "isNearEnd"), -@@ -7075,6 +7288,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7075,6 +7286,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded dataVersion, drawDistance = 250, contentInsetEndAdjustment, @@ -721,7 +712,7 @@ index b3c5a30..6176e5b 100644 estimatedItemSize = 100, estimatedListSize, extraData, -@@ -7132,10 +7346,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7132,10 +7344,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded const animatedPropsInternal = props.animatedPropsInternal; const anchoredEndSpaceOwner = (_a3 = props.anchoredEndSpaceOwnerInternal) != null ? _a3 : "list"; const positionComponentInternal = props.positionComponentInternal; @@ -734,7 +725,7 @@ index b3c5a30..6176e5b 100644 stickyPositionComponentInternal: _stickyPositionComponentInternal, ...restProps } = rest; -@@ -7200,7 +7416,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7200,7 +7414,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded const combinedRef = useCombinedRef(refScroller, refScrollView); const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString()); const stickyHeaderIndices = stickyHeaderIndicesProp; @@ -743,7 +734,7 @@ index b3c5a30..6176e5b 100644 const previousContentInsetEndAdjustmentRef = React2.useRef(contentInsetEndAdjustmentResolved); const alwaysRenderIndices = React2.useMemo(() => { const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex); -@@ -7341,6 +7557,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7341,6 +7555,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded contentContainerAlignItems: contentContainerStyle.alignItems, contentInset, contentInsetEndAdjustment: contentInsetEndAdjustmentResolved, @@ -751,7 +742,7 @@ index b3c5a30..6176e5b 100644 data: dataProp, dataKey, dataVersion, -@@ -7372,6 +7589,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7372,6 +7587,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded renderItem, rtl, snapToIndices, @@ -759,7 +750,7 @@ index b3c5a30..6176e5b 100644 stickyHeaderIndicesArr: stickyHeaderIndices != null ? stickyHeaderIndices : [], stickyHeaderIndicesSet: React2.useMemo(() => new Set(stickyHeaderIndices != null ? stickyHeaderIndices : []), [stickyHeaderIndices == null ? void 0 : stickyHeaderIndices.join(",")]), stickyPositionComponentInternal, -@@ -7423,6 +7641,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7423,6 +7639,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded return void 0; } const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll); @@ -773,7 +764,7 @@ index b3c5a30..6176e5b 100644 return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset; }, [usesBootstrapInitialScroll]); React2.useLayoutEffect(() => { -@@ -7547,6 +7772,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7547,6 +7770,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded [ dataKey, dataVersion, @@ -781,7 +772,7 @@ index b3c5a30..6176e5b 100644 memoizedLastItemKeys.join(","), numColumnsProp, nextScrollAxisGap, -@@ -7643,6 +7869,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7643,6 +7867,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded () => ({ getRenderedItem: (key) => getRenderedItem(ctx, key), onMomentumScrollEnd: (event) => { @@ -789,7 +780,7 @@ index b3c5a30..6176e5b 100644 checkFinishedScrollFallback(ctx); if (state.props.onMomentumScrollEnd) { state.props.onMomentumScrollEnd(event); -@@ -7651,6 +7878,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7651,6 +7876,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScroll: (event) => onScroll(ctx, event), onScrollBeginDrag: (event) => { var _a4, _b2; @@ -798,7 +789,7 @@ index b3c5a30..6176e5b 100644 prepareReachedEdgeForNextUserScroll(ctx); (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); }, -@@ -7676,11 +7905,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7676,11 +7903,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded ListFooterComponent, ListFooterComponentStyle, ListHeaderComponent, @@ -818,7 +809,7 @@ index b3c5a30..6176e5b 100644 recycleItems, refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2__namespace.cloneElement(refreshControlElement, { diff --git a/react-native.mjs b/react-native.mjs -index 40e87cd..7ca12cb 100644 +index 40e87cd..93aac74 100644 --- a/react-native.mjs +++ b/react-native.mjs @@ -696,6 +696,15 @@ function hasActiveInitialScroll(state) { @@ -1183,16 +1174,7 @@ index 40e87cd..7ca12cb 100644 } return nextSize; } -@@ -4749,6 +4929,8 @@ function flushItemSizeUpdates(ctx, result) { - } - if (result.didChange) { - maybeUpdateAnchoredEndSpace(ctx); -+ // Row expansion can move the end without a native scroll event. -+ set$(ctx, "isAtEnd", getIsAtEnd(ctx)); - } - if (result.didChange && result.shouldMaintainScrollAtEnd) { - doMaintainScrollAtEnd(ctx); -@@ -5694,6 +5876,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ +@@ -5694,6 +5874,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ horizontal }) { const ctx = useStateContext(); @@ -1200,7 +1182,7 @@ index 40e87cd..7ca12cb 100644 const columnWrapperStyle = ctx.columnWrapperStyle; const animSize = useValue$("totalSize"); const [readyToRender, numColumns, otherAxisSize = 0] = useArr$(["readyToRender", "numColumns", "otherAxisSize"]); -@@ -5704,6 +5887,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ +@@ -5704,6 +5885,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ opacity: isVisible ? 1 : 0, width: animSize } : { height: animSize, minWidth: otherAxisSize, opacity: isVisible ? 1 : 0 }; @@ -1214,7 +1196,7 @@ index 40e87cd..7ca12cb 100644 if (columnWrapperStyle) { const { columnGap, rowGap, gap } = columnWrapperStyle; const gapX = columnGap || gap || 0; -@@ -5724,7 +5914,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ +@@ -5724,7 +5912,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({ } } } @@ -1224,7 +1206,7 @@ index 40e87cd..7ca12cb 100644 }); var Containers = typedMemo(function Containers2({ freshDataTransitionEpoch, -@@ -5875,7 +6066,12 @@ var StyleSheet = StyleSheet$1; +@@ -5875,7 +6064,12 @@ var StyleSheet = StyleSheet$1; // src/components/ListComponent.tsx var AlignItemsAtEndSpacer = typedMemo(function AlignItemsAtEndSpacer2({ horizontal }) { @@ -1237,7 +1219,7 @@ index 40e87cd..7ca12cb 100644 if (alignItemsAtEndPadding <= 0) { return null; } -@@ -5908,8 +6104,12 @@ var ListComponent = typedMemo(function ListComponent2({ +@@ -5908,8 +6102,12 @@ var ListComponent = typedMemo(function ListComponent2({ refScrollView, renderScrollComponent, onLayoutFooter, @@ -1250,7 +1232,7 @@ index 40e87cd..7ca12cb 100644 scrollAdjustHandler, snapToIndices, stickyHeaderConfig, -@@ -5980,7 +6180,17 @@ var ListComponent = typedMemo(function ListComponent2({ +@@ -5980,7 +6178,17 @@ var ListComponent = typedMemo(function ListComponent2({ SnapOrScroll, { ...rest, @@ -1269,7 +1251,7 @@ index 40e87cd..7ca12cb 100644 contentContainerStyle: [ horizontal ? { height: "100%" } : {}, contentContainerStyle, -@@ -5989,7 +6199,10 @@ var ListComponent = typedMemo(function ListComponent2({ +@@ -5989,7 +6197,10 @@ var ListComponent = typedMemo(function ListComponent2({ ], contentOffset: initialContentOffset !== void 0 ? horizontal ? { x: initialContentOffset, y: 0 } : { x: 0, y: initialContentOffset } : void 0, horizontal, @@ -1281,7 +1263,7 @@ index 40e87cd..7ca12cb 100644 onLayout, onScroll: onScroll2, ref: refScrollView, -@@ -6730,7 +6943,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { +@@ -6730,7 +6941,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) { endBuffered: state.endBuffered, getAverageItemSizes: () => getAverageItemSizes(state), indexByKey: (key) => state.indexByKey.get(key), @@ -1290,7 +1272,7 @@ index 40e87cd..7ca12cb 100644 isAtStart: peek$(ctx, "isAtStart"), isEndReached: state.isEndReached, isNearEnd: peek$(ctx, "isNearEnd"), -@@ -7054,6 +7267,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7054,6 +7265,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded dataVersion, drawDistance = 250, contentInsetEndAdjustment, @@ -1298,7 +1280,7 @@ index 40e87cd..7ca12cb 100644 estimatedItemSize = 100, estimatedListSize, extraData, -@@ -7111,10 +7325,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7111,10 +7323,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded const animatedPropsInternal = props.animatedPropsInternal; const anchoredEndSpaceOwner = (_a3 = props.anchoredEndSpaceOwnerInternal) != null ? _a3 : "list"; const positionComponentInternal = props.positionComponentInternal; @@ -1311,7 +1293,7 @@ index 40e87cd..7ca12cb 100644 stickyPositionComponentInternal: _stickyPositionComponentInternal, ...restProps } = rest; -@@ -7179,7 +7395,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7179,7 +7393,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded const combinedRef = useCombinedRef(refScroller, refScrollView); const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString()); const stickyHeaderIndices = stickyHeaderIndicesProp; @@ -1320,7 +1302,7 @@ index 40e87cd..7ca12cb 100644 const previousContentInsetEndAdjustmentRef = useRef(contentInsetEndAdjustmentResolved); const alwaysRenderIndices = useMemo(() => { const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex); -@@ -7320,6 +7536,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7320,6 +7534,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded contentContainerAlignItems: contentContainerStyle.alignItems, contentInset, contentInsetEndAdjustment: contentInsetEndAdjustmentResolved, @@ -1328,7 +1310,7 @@ index 40e87cd..7ca12cb 100644 data: dataProp, dataKey, dataVersion, -@@ -7351,6 +7568,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7351,6 +7566,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded renderItem, rtl, snapToIndices, @@ -1336,7 +1318,7 @@ index 40e87cd..7ca12cb 100644 stickyHeaderIndicesArr: stickyHeaderIndices != null ? stickyHeaderIndices : [], stickyHeaderIndicesSet: useMemo(() => new Set(stickyHeaderIndices != null ? stickyHeaderIndices : []), [stickyHeaderIndices == null ? void 0 : stickyHeaderIndices.join(",")]), stickyPositionComponentInternal, -@@ -7402,6 +7620,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7402,6 +7618,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded return void 0; } const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll); @@ -1350,7 +1332,7 @@ index 40e87cd..7ca12cb 100644 return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset; }, [usesBootstrapInitialScroll]); useLayoutEffect(() => { -@@ -7526,6 +7751,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7526,6 +7749,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded [ dataKey, dataVersion, @@ -1358,7 +1340,7 @@ index 40e87cd..7ca12cb 100644 memoizedLastItemKeys.join(","), numColumnsProp, nextScrollAxisGap, -@@ -7622,6 +7848,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7622,6 +7846,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded () => ({ getRenderedItem: (key) => getRenderedItem(ctx, key), onMomentumScrollEnd: (event) => { @@ -1366,7 +1348,7 @@ index 40e87cd..7ca12cb 100644 checkFinishedScrollFallback(ctx); if (state.props.onMomentumScrollEnd) { state.props.onMomentumScrollEnd(event); -@@ -7630,6 +7857,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7630,6 +7855,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded onScroll: (event) => onScroll(ctx, event), onScrollBeginDrag: (event) => { var _a4, _b2; @@ -1375,7 +1357,7 @@ index 40e87cd..7ca12cb 100644 prepareReachedEdgeForNextUserScroll(ctx); (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event); }, -@@ -7655,11 +7884,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded +@@ -7655,11 +7882,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded ListFooterComponent, ListFooterComponentStyle, ListHeaderComponent, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 45e067635127..7254a454328c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,7 +89,7 @@ patchedDependencies: '@effect/vitest@4.0.0-rc.112': a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b '@expo/metro-config@57.0.12': 96f1a75347e6ea02dc4b7034ace815d8ee39e18b8166ebfb573d9e58328f0dc2 '@ff-labs/fff-node@0.9.4': c4e3cc2420ceb9dc650f9d189e9c24baf7e83f342998bacda5f459a4ce7927a8 - '@legendapp/list@3.3.5': 0bbbd4b76670b65cf0912ba5451047f7ce9ba0dbe13a34987fc235de4c657100 + '@legendapp/list@3.3.5': 680cc6a5c5b4a4032e467e7b3fde22f89a84c0ee2e6eac6fda737d6277cc0806 '@pierre/diffs@1.3.0-beta.10': 0ccee155b93b63d810e2c1a40c1fd676fb6fbcfa72cf6430dcedf1a3ae475ab4 '@react-native-ai/apple@0.12.0': 2d09870c2848d185cb05b53ed823a46e12dba519324d8dd8e584e28731990f9d '@react-native-menu/menu@2.0.0': f63d256bf6a97a873b5e628eb595bd6ef0075ddd5bdd890fc920f7a6024290dd @@ -246,7 +246,7 @@ importers: version: 57.0.14(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@legendapp/list': specifier: 'catalog:' - version: 3.3.5(patch_hash=0bbbd4b76670b65cf0912ba5451047f7ce9ba0dbe13a34987fc235de4c657100)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 3.3.5(patch_hash=680cc6a5c5b4a4032e467e7b3fde22f89a84c0ee2e6eac6fda737d6277cc0806)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@material/material-color-utilities': specifier: 0.3.0 version: 0.3.0 @@ -599,7 +599,7 @@ importers: version: 0.9.0 '@legendapp/list': specifier: 'catalog:' - version: 3.3.5(patch_hash=0bbbd4b76670b65cf0912ba5451047f7ce9ba0dbe13a34987fc235de4c657100)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 3.3.5(patch_hash=680cc6a5c5b4a4032e467e7b3fde22f89a84c0ee2e6eac6fda737d6277cc0806)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@lexical/react': specifier: ^0.41.0 version: 0.41.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(yjs@13.6.31) @@ -14024,7 +14024,7 @@ snapshots: dependencies: jsbi: 4.3.2 - '@legendapp/list@3.3.5(patch_hash=0bbbd4b76670b65cf0912ba5451047f7ce9ba0dbe13a34987fc235de4c657100)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@legendapp/list@3.3.5(patch_hash=680cc6a5c5b4a4032e467e7b3fde22f89a84c0ee2e6eac6fda737d6277cc0806)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: react: 19.2.3 use-sync-external-store: 1.6.0(react@19.2.3) @@ -14032,7 +14032,7 @@ snapshots: react-dom: 19.2.3(react@19.2.3) react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - '@legendapp/list@3.3.5(patch_hash=0bbbd4b76670b65cf0912ba5451047f7ce9ba0dbe13a34987fc235de4c657100)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@legendapp/list@3.3.5(patch_hash=680cc6a5c5b4a4032e467e7b3fde22f89a84c0ee2e6eac6fda737d6277cc0806)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: react: 19.2.6 use-sync-external-store: 1.6.0(react@19.2.6)