From 32a035e1152c22e1a8e8a36ee148588ecf003270 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 11 Nov 2026 13:20:05 +0100 Subject: [PATCH 1/3] chore: rebuild the LegendList patches from their upstream PRs Each LegendList patch is now the build output of its upstream PR, so the patches change exactly what the PRs change, in every bundle the PRs touch. - 001 and 003 now also patch the native bundles, as their PRs change shared code. - 002 adds the explicit maintainVisibleContentPosition anchor option that LegendApp/legend-list#597 now proposes, instead of anchoring the last visible item in every list with alignItemsAtEnd. - 004 contains LegendApp/legend-list#600, which now also skips deferred adjustments that a later native scroll already includes and moves the initial scroll target with each adjustment. - 006 keeps only the silent-retry fix from LegendApp/legend-list#604. Co-Authored-By: Claude Opus 5.5 --- ...+001+apply-pending-anchor-correction.patch | 60 ++- ...p+list+3.7.2+002+add-mvcp-end-anchor.patch | 482 ++++++++++++++++++ ...d-aligned-lists-to-last-visible-item.patch | 108 ---- ...chor-data-changes-after-scroll-jumps.patch | 44 +- ...+land-initial-mount-adjustments-once.patch | 472 +++++++++++++++++ ...ist-after-initial-scroll-adjustments.patch | 240 --------- ...05+keep-scroll-adjust-anchor-precise.patch | 12 +- ...finish-silent-initial-scroll-retries.patch | 282 ++++++++++ ...keep-native-initial-scroll-on-target.patch | 228 --------- patches/@legendapp/list/details.md | 36 +- 10 files changed, 1351 insertions(+), 613 deletions(-) create mode 100644 patches/@legendapp/list/@legendapp+list+3.7.2+002+add-mvcp-end-anchor.patch delete mode 100644 patches/@legendapp/list/@legendapp+list+3.7.2+002+anchor-end-aligned-lists-to-last-visible-item.patch create mode 100644 patches/@legendapp/list/@legendapp+list+3.7.2+004+land-initial-mount-adjustments-once.patch delete mode 100644 patches/@legendapp/list/@legendapp+list+3.7.2+004+reveal-list-after-initial-scroll-adjustments.patch create mode 100644 patches/@legendapp/list/@legendapp+list+3.7.2+006+finish-silent-initial-scroll-retries.patch delete mode 100644 patches/@legendapp/list/@legendapp+list+3.7.2+006+keep-native-initial-scroll-on-target.patch diff --git a/patches/@legendapp/list/@legendapp+list+3.7.2+001+apply-pending-anchor-correction.patch b/patches/@legendapp/list/@legendapp+list+3.7.2+001+apply-pending-anchor-correction.patch index 19a3139e8df6..19e8ee2cd261 100644 --- a/patches/@legendapp/list/@legendapp+list+3.7.2+001+apply-pending-anchor-correction.patch +++ b/patches/@legendapp/list/@legendapp+list+3.7.2+001+apply-pending-anchor-correction.patch @@ -1,5 +1,57 @@ +diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js +index 4af685dfb96..92b387b085f 100644 +--- a/node_modules/@legendapp/list/react-native.js ++++ b/node_modules/@legendapp/list/react-native.js +@@ -4769,7 +4769,8 @@ function calculateItemsInView(ctx, params = {}) { + const stickyHeaderIndicesArr = state.props.stickyHeaderIndicesArr || []; + const stickyHeaderIndicesSet = state.props.stickyHeaderIndicesSet || /* @__PURE__ */ new Set(); + const drawDistance = getEffectiveDrawDistance(ctx, params.drawDistanceMode); +- const { dataChanged, doMVCP, forceFullItemPositions, mvcpAdjustmentSource } = params; ++ const { dataChanged, forceFullItemPositions } = params; ++ let { doMVCP, mvcpAdjustmentSource } = params; + const bootstrapInitialScrollState = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; + const suppressInitialScrollSideEffects = !!bootstrapInitialScrollState; + const prevNumContainers = peek$(ctx, "numContainers"); +@@ -4869,6 +4870,11 @@ function calculateItemsInView(ctx, params = {}) { + } + } + } ++ if (!doMVCP && state.scheduledWork.has("mvcpRecalculate")) { ++ state.scheduledWork.cancel("mvcpRecalculate"); ++ doMVCP = true; ++ mvcpAdjustmentSource = "item-size"; ++ } + const checkMVCP = doMVCP && !suppressInitialScrollSideEffects ? prepareMVCP(ctx, dataChanged, mvcpAdjustmentSource) : void 0; + if (dataChanged) { + resetLayoutCachesForDataChange(state); +diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs +index 9ebfddc183a..30105d17d72 100644 +--- a/node_modules/@legendapp/list/react-native.mjs ++++ b/node_modules/@legendapp/list/react-native.mjs +@@ -4748,7 +4748,8 @@ function calculateItemsInView(ctx, params = {}) { + const stickyHeaderIndicesArr = state.props.stickyHeaderIndicesArr || []; + const stickyHeaderIndicesSet = state.props.stickyHeaderIndicesSet || /* @__PURE__ */ new Set(); + const drawDistance = getEffectiveDrawDistance(ctx, params.drawDistanceMode); +- const { dataChanged, doMVCP, forceFullItemPositions, mvcpAdjustmentSource } = params; ++ const { dataChanged, forceFullItemPositions } = params; ++ let { doMVCP, mvcpAdjustmentSource } = params; + const bootstrapInitialScrollState = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0; + const suppressInitialScrollSideEffects = !!bootstrapInitialScrollState; + const prevNumContainers = peek$(ctx, "numContainers"); +@@ -4848,6 +4849,11 @@ function calculateItemsInView(ctx, params = {}) { + } + } + } ++ if (!doMVCP && state.scheduledWork.has("mvcpRecalculate")) { ++ state.scheduledWork.cancel("mvcpRecalculate"); ++ doMVCP = true; ++ mvcpAdjustmentSource = "item-size"; ++ } + const checkMVCP = doMVCP && !suppressInitialScrollSideEffects ? prepareMVCP(ctx, dataChanged, mvcpAdjustmentSource) : void 0; + if (dataChanged) { + resetLayoutCachesForDataChange(state); diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js -index 5155454..c102661 100644 +index 515545471e0..c102661fc18 100644 --- a/node_modules/@legendapp/list/react-native.web.js +++ b/node_modules/@legendapp/list/react-native.web.js @@ -4843,7 +4843,8 @@ function calculateItemsInView(ctx, params = {}) { @@ -25,7 +77,7 @@ index 5155454..c102661 100644 if (dataChanged) { resetLayoutCachesForDataChange(state); diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs -index ce08abe..ff71fd6 100644 +index ce08abe7ee6..ff71fd62640 100644 --- a/node_modules/@legendapp/list/react-native.web.mjs +++ b/node_modules/@legendapp/list/react-native.web.mjs @@ -4822,7 +4822,8 @@ function calculateItemsInView(ctx, params = {}) { @@ -51,7 +103,7 @@ index ce08abe..ff71fd6 100644 if (dataChanged) { resetLayoutCachesForDataChange(state); diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js -index 5155454..c102661 100644 +index 515545471e0..c102661fc18 100644 --- a/node_modules/@legendapp/list/react.js +++ b/node_modules/@legendapp/list/react.js @@ -4843,7 +4843,8 @@ function calculateItemsInView(ctx, params = {}) { @@ -77,7 +129,7 @@ index 5155454..c102661 100644 if (dataChanged) { resetLayoutCachesForDataChange(state); diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs -index ce08abe..ff71fd6 100644 +index ce08abe7ee6..ff71fd62640 100644 --- a/node_modules/@legendapp/list/react.mjs +++ b/node_modules/@legendapp/list/react.mjs @@ -4822,7 +4822,8 @@ function calculateItemsInView(ctx, params = {}) { diff --git a/patches/@legendapp/list/@legendapp+list+3.7.2+002+add-mvcp-end-anchor.patch b/patches/@legendapp/list/@legendapp+list+3.7.2+002+add-mvcp-end-anchor.patch new file mode 100644 index 000000000000..a7e06efae9a0 --- /dev/null +++ b/patches/@legendapp/list/@legendapp+list+3.7.2+002+add-mvcp-end-anchor.patch @@ -0,0 +1,482 @@ +diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js +index 92b387b085f..e687e9c0a41 100644 +--- a/node_modules/@legendapp/list/react-native.js ++++ b/node_modules/@legendapp/list/react-native.js +@@ -1800,9 +1800,11 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { + const state = ctx.state; + const { idsInView, positions, props } = state; + const { +- maintainVisibleContentPosition: { data: mvcpData, size: mvcpScroll, shouldRestorePosition } ++ maintainVisibleContentPosition: { anchor, data: mvcpData, size: mvcpScroll, shouldRestorePosition } + } = props; + const isWeb = Platform.OS === "web"; ++ const isEndAnchor = anchor === "end"; ++ const getAnchorCandidateId = (i) => idsInView[isEndAnchor ? idsInView.length - 1 - i : i]; + const now = Date.now(); + const enableMVCPAnchorLock = isWeb && (!!dataChanged || !!state.mvcpAnchorLock); + const scrollingTo = state.scrollingTo; +@@ -1847,11 +1849,16 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { + } + targetId = (_c = scrollingTo == null ? void 0 : scrollingTo.key) != null ? _c : getId(state, scrollTarget); + } else if (idsInView.length > 0 && state.didContainersLayout && !dataChanged) { +- targetId = idsInView.find((id) => indexByKey.get(id) !== void 0); ++ for (let i = 0; i < idsInView.length && targetId === void 0; i++) { ++ const id = getAnchorCandidateId(i); ++ if (indexByKey.get(id) !== void 0) { ++ targetId = id; ++ } ++ } + } + if (dataChanged && idsInView.length > 0 && state.didContainersLayout) { + for (let i = 0; i < idsInView.length; i++) { +- const id = idsInView[i]; ++ const id = getAnchorCandidateId(i); + const index = previousIndexByKey.get(id); + if (index !== void 0) { + const position = positions[index]; +@@ -8236,6 +8243,7 @@ function normalizeMaintainVisibleContentPosition(value) { + } + if (value && typeof value === "object") { + return { ++ anchor: value.anchor, + data: (_a3 = value.data) != null ? _a3 : false, + shouldRestorePosition: value.shouldRestorePosition, + size: (_b = value.size) != null ? _b : true +diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs +index 30105d17d72..6271d782ea7 100644 +--- a/node_modules/@legendapp/list/react-native.mjs ++++ b/node_modules/@legendapp/list/react-native.mjs +@@ -1779,9 +1779,11 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { + const state = ctx.state; + const { idsInView, positions, props } = state; + const { +- maintainVisibleContentPosition: { data: mvcpData, size: mvcpScroll, shouldRestorePosition } ++ maintainVisibleContentPosition: { anchor, data: mvcpData, size: mvcpScroll, shouldRestorePosition } + } = props; + const isWeb = Platform.OS === "web"; ++ const isEndAnchor = anchor === "end"; ++ const getAnchorCandidateId = (i) => idsInView[isEndAnchor ? idsInView.length - 1 - i : i]; + const now = Date.now(); + const enableMVCPAnchorLock = isWeb && (!!dataChanged || !!state.mvcpAnchorLock); + const scrollingTo = state.scrollingTo; +@@ -1826,11 +1828,16 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { + } + targetId = (_c = scrollingTo == null ? void 0 : scrollingTo.key) != null ? _c : getId(state, scrollTarget); + } else if (idsInView.length > 0 && state.didContainersLayout && !dataChanged) { +- targetId = idsInView.find((id) => indexByKey.get(id) !== void 0); ++ for (let i = 0; i < idsInView.length && targetId === void 0; i++) { ++ const id = getAnchorCandidateId(i); ++ if (indexByKey.get(id) !== void 0) { ++ targetId = id; ++ } ++ } + } + if (dataChanged && idsInView.length > 0 && state.didContainersLayout) { + for (let i = 0; i < idsInView.length; i++) { +- const id = idsInView[i]; ++ const id = getAnchorCandidateId(i); + const index = previousIndexByKey.get(id); + if (index !== void 0) { + const position = positions[index]; +@@ -8215,6 +8222,7 @@ function normalizeMaintainVisibleContentPosition(value) { + } + if (value && typeof value === "object") { + return { ++ anchor: value.anchor, + data: (_a3 = value.data) != null ? _a3 : false, + shouldRestorePosition: value.shouldRestorePosition, + size: (_b = value.size) != null ? _b : true +diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js +index c102661fc18..2b3171a6f4c 100644 +--- a/node_modules/@legendapp/list/react-native.web.js ++++ b/node_modules/@legendapp/list/react-native.web.js +@@ -1704,8 +1704,10 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { + const state = ctx.state; + const { idsInView, positions, props } = state; + const { +- maintainVisibleContentPosition: { data: mvcpData, size: mvcpScroll, shouldRestorePosition } ++ maintainVisibleContentPosition: { anchor, data: mvcpData, size: mvcpScroll, shouldRestorePosition } + } = props; ++ const isEndAnchor = anchor === "end"; ++ const getAnchorCandidateId = (i) => idsInView[isEndAnchor ? idsInView.length - 1 - i : i]; + const now = Date.now(); + const enableMVCPAnchorLock = (!!dataChanged || !!state.mvcpAnchorLock); + const scrollingTo = state.scrollingTo; +@@ -1743,11 +1745,16 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { + } else if (scrollTarget !== void 0) { + targetId = (_c = scrollingTo == null ? void 0 : scrollingTo.key) != null ? _c : getId(state, scrollTarget); + } else if (idsInView.length > 0 && state.didContainersLayout && !dataChanged) { +- targetId = idsInView.find((id) => indexByKey.get(id) !== void 0); ++ for (let i = 0; i < idsInView.length && targetId === void 0; i++) { ++ const id = getAnchorCandidateId(i); ++ if (indexByKey.get(id) !== void 0) { ++ targetId = id; ++ } ++ } + } + if (dataChanged && idsInView.length > 0 && state.didContainersLayout) { + for (let i = 0; i < idsInView.length; i++) { +- const id = idsInView[i]; ++ const id = getAnchorCandidateId(i); + const index = previousIndexByKey.get(id); + if (index !== void 0) { + const position = positions[index]; +@@ -9018,6 +9025,7 @@ function normalizeMaintainVisibleContentPosition(value) { + } + if (value && typeof value === "object") { + return { ++ anchor: value.anchor, + data: (_a3 = value.data) != null ? _a3 : false, + shouldRestorePosition: value.shouldRestorePosition, + size: (_b = value.size) != null ? _b : true +diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs +index ff71fd62640..dbd6fbbf3e8 100644 +--- a/node_modules/@legendapp/list/react-native.web.mjs ++++ b/node_modules/@legendapp/list/react-native.web.mjs +@@ -1683,8 +1683,10 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { + const state = ctx.state; + const { idsInView, positions, props } = state; + const { +- maintainVisibleContentPosition: { data: mvcpData, size: mvcpScroll, shouldRestorePosition } ++ maintainVisibleContentPosition: { anchor, data: mvcpData, size: mvcpScroll, shouldRestorePosition } + } = props; ++ const isEndAnchor = anchor === "end"; ++ const getAnchorCandidateId = (i) => idsInView[isEndAnchor ? idsInView.length - 1 - i : i]; + const now = Date.now(); + const enableMVCPAnchorLock = (!!dataChanged || !!state.mvcpAnchorLock); + const scrollingTo = state.scrollingTo; +@@ -1722,11 +1724,16 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { + } else if (scrollTarget !== void 0) { + targetId = (_c = scrollingTo == null ? void 0 : scrollingTo.key) != null ? _c : getId(state, scrollTarget); + } else if (idsInView.length > 0 && state.didContainersLayout && !dataChanged) { +- targetId = idsInView.find((id) => indexByKey.get(id) !== void 0); ++ for (let i = 0; i < idsInView.length && targetId === void 0; i++) { ++ const id = getAnchorCandidateId(i); ++ if (indexByKey.get(id) !== void 0) { ++ targetId = id; ++ } ++ } + } + if (dataChanged && idsInView.length > 0 && state.didContainersLayout) { + for (let i = 0; i < idsInView.length; i++) { +- const id = idsInView[i]; ++ const id = getAnchorCandidateId(i); + const index = previousIndexByKey.get(id); + if (index !== void 0) { + const position = positions[index]; +@@ -8997,6 +9004,7 @@ function normalizeMaintainVisibleContentPosition(value) { + } + if (value && typeof value === "object") { + return { ++ anchor: value.anchor, + data: (_a3 = value.data) != null ? _a3 : false, + shouldRestorePosition: value.shouldRestorePosition, + size: (_b = value.size) != null ? _b : true +diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js +index c102661fc18..2b3171a6f4c 100644 +--- a/node_modules/@legendapp/list/react.js ++++ b/node_modules/@legendapp/list/react.js +@@ -1704,8 +1704,10 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { + const state = ctx.state; + const { idsInView, positions, props } = state; + const { +- maintainVisibleContentPosition: { data: mvcpData, size: mvcpScroll, shouldRestorePosition } ++ maintainVisibleContentPosition: { anchor, data: mvcpData, size: mvcpScroll, shouldRestorePosition } + } = props; ++ const isEndAnchor = anchor === "end"; ++ const getAnchorCandidateId = (i) => idsInView[isEndAnchor ? idsInView.length - 1 - i : i]; + const now = Date.now(); + const enableMVCPAnchorLock = (!!dataChanged || !!state.mvcpAnchorLock); + const scrollingTo = state.scrollingTo; +@@ -1743,11 +1745,16 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { + } else if (scrollTarget !== void 0) { + targetId = (_c = scrollingTo == null ? void 0 : scrollingTo.key) != null ? _c : getId(state, scrollTarget); + } else if (idsInView.length > 0 && state.didContainersLayout && !dataChanged) { +- targetId = idsInView.find((id) => indexByKey.get(id) !== void 0); ++ for (let i = 0; i < idsInView.length && targetId === void 0; i++) { ++ const id = getAnchorCandidateId(i); ++ if (indexByKey.get(id) !== void 0) { ++ targetId = id; ++ } ++ } + } + if (dataChanged && idsInView.length > 0 && state.didContainersLayout) { + for (let i = 0; i < idsInView.length; i++) { +- const id = idsInView[i]; ++ const id = getAnchorCandidateId(i); + const index = previousIndexByKey.get(id); + if (index !== void 0) { + const position = positions[index]; +@@ -9018,6 +9025,7 @@ function normalizeMaintainVisibleContentPosition(value) { + } + if (value && typeof value === "object") { + return { ++ anchor: value.anchor, + data: (_a3 = value.data) != null ? _a3 : false, + shouldRestorePosition: value.shouldRestorePosition, + size: (_b = value.size) != null ? _b : true +diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs +index ff71fd62640..dbd6fbbf3e8 100644 +--- a/node_modules/@legendapp/list/react.mjs ++++ b/node_modules/@legendapp/list/react.mjs +@@ -1683,8 +1683,10 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { + const state = ctx.state; + const { idsInView, positions, props } = state; + const { +- maintainVisibleContentPosition: { data: mvcpData, size: mvcpScroll, shouldRestorePosition } ++ maintainVisibleContentPosition: { anchor, data: mvcpData, size: mvcpScroll, shouldRestorePosition } + } = props; ++ const isEndAnchor = anchor === "end"; ++ const getAnchorCandidateId = (i) => idsInView[isEndAnchor ? idsInView.length - 1 - i : i]; + const now = Date.now(); + const enableMVCPAnchorLock = (!!dataChanged || !!state.mvcpAnchorLock); + const scrollingTo = state.scrollingTo; +@@ -1722,11 +1724,16 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { + } else if (scrollTarget !== void 0) { + targetId = (_c = scrollingTo == null ? void 0 : scrollingTo.key) != null ? _c : getId(state, scrollTarget); + } else if (idsInView.length > 0 && state.didContainersLayout && !dataChanged) { +- targetId = idsInView.find((id) => indexByKey.get(id) !== void 0); ++ for (let i = 0; i < idsInView.length && targetId === void 0; i++) { ++ const id = getAnchorCandidateId(i); ++ if (indexByKey.get(id) !== void 0) { ++ targetId = id; ++ } ++ } + } + if (dataChanged && idsInView.length > 0 && state.didContainersLayout) { + for (let i = 0; i < idsInView.length; i++) { +- const id = idsInView[i]; ++ const id = getAnchorCandidateId(i); + const index = previousIndexByKey.get(id); + if (index !== void 0) { + const position = positions[index]; +@@ -8997,6 +9004,7 @@ function normalizeMaintainVisibleContentPosition(value) { + } + if (value && typeof value === "object") { + return { ++ anchor: value.anchor, + data: (_a3 = value.data) != null ? _a3 : false, + shouldRestorePosition: value.shouldRestorePosition, + size: (_b = value.size) != null ? _b : true +diff --git a/node_modules/@legendapp/list/react-native.d.ts b/node_modules/@legendapp/list/react-native.d.ts +index 983e937cf5c..72e2d573b77 100644 +--- a/node_modules/@legendapp/list/react-native.d.ts ++++ b/node_modules/@legendapp/list/react-native.d.ts +@@ -3,6 +3,7 @@ import { Key, Dispatch, SetStateAction } from 'react'; + import { ScrollViewProps, NativeSyntheticEvent as NativeSyntheticEvent$1, NativeScrollEvent as NativeScrollEvent$1, ScrollView, StyleProp as StyleProp$1, ViewStyle as ViewStyle$1, ScrollViewComponent, ScrollResponderMixin, Insets as Insets$1, View } from 'react-native'; + + interface MaintainVisibleContentPositionNormalized { ++ anchor?: "start" | "end"; + data: boolean; + size: boolean; + shouldRestorePosition?: (item: ItemT, index: number, data: readonly ItemT[]) => boolean; +@@ -292,6 +293,7 @@ interface LegendListSpecificProps { + * - size (default: true) stabilizes during size/layout changes while scrolling. + * - data (default: false) stabilizes when the data array changes. + * - shouldRestorePosition can opt out specific items from data-change anchoring. ++ * - anchor (default: "start") picks the visible item that stays in place: the first, or the last with "end". + * - undefined (default) enables scroll stabilization but skips data-change anchoring. + * - true enables both JS behaviors; false disables both. + * Native iOS maintainVisibleContentPosition is enabled when either size or data stabilization is enabled, +@@ -494,6 +496,12 @@ type LegendListPropsBase, TItemTyp + interface MaintainVisibleContentPositionConfig { + data?: boolean; + size?: boolean; ++ /** ++ * Which visible item stays in place when sizes or data change. "start" (default) keeps the first visible ++ * item in place, so changes move the content below it. "end" keeps the last visible item in place, so changes ++ * move the content above it, like in an inverted list. Use "end" for chat-style lists read from the bottom. ++ */ ++ anchor?: "start" | "end"; + shouldRestorePosition?: (item: ItemT, index: number, data: readonly ItemT[]) => boolean; + } + interface AnchoredEndSpaceReadyInfo { +diff --git a/node_modules/@legendapp/list/react-native.web.d.ts b/node_modules/@legendapp/list/react-native.web.d.ts +index 358fcd7faa5..455c5857a8b 100644 +--- a/node_modules/@legendapp/list/react-native.web.d.ts ++++ b/node_modules/@legendapp/list/react-native.web.d.ts +@@ -30,6 +30,7 @@ interface ScrollViewMethods { + } + + interface MaintainVisibleContentPositionNormalized { ++ anchor?: "start" | "end"; + data: boolean; + size: boolean; + shouldRestorePosition?: (item: ItemT, index: number, data: readonly ItemT[]) => boolean; +@@ -319,6 +320,7 @@ interface LegendListSpecificProps { + * - size (default: true) stabilizes during size/layout changes while scrolling. + * - data (default: false) stabilizes when the data array changes. + * - shouldRestorePosition can opt out specific items from data-change anchoring. ++ * - anchor (default: "start") picks the visible item that stays in place: the first, or the last with "end". + * - undefined (default) enables scroll stabilization but skips data-change anchoring. + * - true enables both JS behaviors; false disables both. + * Native iOS maintainVisibleContentPosition is enabled when either size or data stabilization is enabled, +@@ -521,6 +523,12 @@ type LegendListPropsBase, TItemTyp + interface MaintainVisibleContentPositionConfig { + data?: boolean; + size?: boolean; ++ /** ++ * Which visible item stays in place when sizes or data change. "start" (default) keeps the first visible ++ * item in place, so changes move the content below it. "end" keeps the last visible item in place, so changes ++ * move the content above it, like in an inverted list. Use "end" for chat-style lists read from the bottom. ++ */ ++ anchor?: "start" | "end"; + shouldRestorePosition?: (item: ItemT, index: number, data: readonly ItemT[]) => boolean; + } + interface AnchoredEndSpaceReadyInfo { +diff --git a/node_modules/@legendapp/list/react.d.ts b/node_modules/@legendapp/list/react.d.ts +index 358fcd7faa5..455c5857a8b 100644 +--- a/node_modules/@legendapp/list/react.d.ts ++++ b/node_modules/@legendapp/list/react.d.ts +@@ -30,6 +30,7 @@ interface ScrollViewMethods { + } + + interface MaintainVisibleContentPositionNormalized { ++ anchor?: "start" | "end"; + data: boolean; + size: boolean; + shouldRestorePosition?: (item: ItemT, index: number, data: readonly ItemT[]) => boolean; +@@ -319,6 +320,7 @@ interface LegendListSpecificProps { + * - size (default: true) stabilizes during size/layout changes while scrolling. + * - data (default: false) stabilizes when the data array changes. + * - shouldRestorePosition can opt out specific items from data-change anchoring. ++ * - anchor (default: "start") picks the visible item that stays in place: the first, or the last with "end". + * - undefined (default) enables scroll stabilization but skips data-change anchoring. + * - true enables both JS behaviors; false disables both. + * Native iOS maintainVisibleContentPosition is enabled when either size or data stabilization is enabled, +@@ -521,6 +523,12 @@ type LegendListPropsBase, TItemTyp + interface MaintainVisibleContentPositionConfig { + data?: boolean; + size?: boolean; ++ /** ++ * Which visible item stays in place when sizes or data change. "start" (default) keeps the first visible ++ * item in place, so changes move the content below it. "end" keeps the last visible item in place, so changes ++ * move the content above it, like in an inverted list. Use "end" for chat-style lists read from the bottom. ++ */ ++ anchor?: "start" | "end"; + shouldRestorePosition?: (item: ItemT, index: number, data: readonly ItemT[]) => boolean; + } + interface AnchoredEndSpaceReadyInfo { +diff --git a/node_modules/@legendapp/list/animated.d.ts b/node_modules/@legendapp/list/animated.d.ts +index c519061902a..1397ff21793 100644 +--- a/node_modules/@legendapp/list/animated.d.ts ++++ b/node_modules/@legendapp/list/animated.d.ts +@@ -3,6 +3,7 @@ import { Key } from 'react'; + import { ScrollViewProps, NativeSyntheticEvent as NativeSyntheticEvent$1, NativeScrollEvent as NativeScrollEvent$1, ScrollView, StyleProp as StyleProp$1, ViewStyle as ViewStyle$1, ScrollViewComponent, ScrollResponderMixin, Insets as Insets$1, Animated } from 'react-native'; + + interface MaintainVisibleContentPositionNormalized { ++ anchor?: "start" | "end"; + data: boolean; + size: boolean; + shouldRestorePosition?: (item: ItemT, index: number, data: readonly ItemT[]) => boolean; +@@ -292,6 +293,7 @@ interface LegendListSpecificProps { + * - size (default: true) stabilizes during size/layout changes while scrolling. + * - data (default: false) stabilizes when the data array changes. + * - shouldRestorePosition can opt out specific items from data-change anchoring. ++ * - anchor (default: "start") picks the visible item that stays in place: the first, or the last with "end". + * - undefined (default) enables scroll stabilization but skips data-change anchoring. + * - true enables both JS behaviors; false disables both. + * Native iOS maintainVisibleContentPosition is enabled when either size or data stabilization is enabled, +@@ -494,6 +496,12 @@ type LegendListPropsBase, TItemTyp + interface MaintainVisibleContentPositionConfig { + data?: boolean; + size?: boolean; ++ /** ++ * Which visible item stays in place when sizes or data change. "start" (default) keeps the first visible ++ * item in place, so changes move the content below it. "end" keeps the last visible item in place, so changes ++ * move the content above it, like in an inverted list. Use "end" for chat-style lists read from the bottom. ++ */ ++ anchor?: "start" | "end"; + shouldRestorePosition?: (item: ItemT, index: number, data: readonly ItemT[]) => boolean; + } + interface AnchoredEndSpaceReadyInfo { +diff --git a/node_modules/@legendapp/list/keyboard.d.ts b/node_modules/@legendapp/list/keyboard.d.ts +index 2aa690be174..32f72309087 100644 +--- a/node_modules/@legendapp/list/keyboard.d.ts ++++ b/node_modules/@legendapp/list/keyboard.d.ts +@@ -6,6 +6,7 @@ import { SharedValue } from 'react-native-reanimated'; + import { AnimatedLegendListProps } from '@legendapp/list/reanimated'; + + interface MaintainVisibleContentPositionNormalized { ++ anchor?: "start" | "end"; + data: boolean; + size: boolean; + shouldRestorePosition?: (item: ItemT, index: number, data: readonly ItemT[]) => boolean; +diff --git a/node_modules/@legendapp/list/keyboard-legacy.d.ts b/node_modules/@legendapp/list/keyboard-legacy.d.ts +index 0d2e865f8e7..61e36137d52 100644 +--- a/node_modules/@legendapp/list/keyboard-legacy.d.ts ++++ b/node_modules/@legendapp/list/keyboard-legacy.d.ts +@@ -5,6 +5,7 @@ import { ScrollEvent, ScrollHandlerProcessed } from 'react-native-reanimated'; + import { AnimatedLegendListProps } from '@legendapp/list/reanimated'; + + interface MaintainVisibleContentPositionNormalized { ++ anchor?: "start" | "end"; + data: boolean; + size: boolean; + shouldRestorePosition?: (item: ItemT, index: number, data: readonly ItemT[]) => boolean; +diff --git a/node_modules/@legendapp/list/reanimated.d.ts b/node_modules/@legendapp/list/reanimated.d.ts +index 5b568f2d397..f6aa70389be 100644 +--- a/node_modules/@legendapp/list/reanimated.d.ts ++++ b/node_modules/@legendapp/list/reanimated.d.ts +@@ -4,6 +4,7 @@ import Reanimated, { SharedValue } from 'react-native-reanimated'; + import { ScrollViewProps, NativeSyntheticEvent as NativeSyntheticEvent$1, NativeScrollEvent as NativeScrollEvent$1, ScrollView, StyleProp as StyleProp$1, ViewStyle as ViewStyle$1, ScrollViewComponent, ScrollResponderMixin, Insets as Insets$1 } from 'react-native'; + + interface MaintainVisibleContentPositionNormalized { ++ anchor?: "start" | "end"; + data: boolean; + size: boolean; + shouldRestorePosition?: (item: ItemT, index: number, data: readonly ItemT[]) => boolean; +@@ -293,6 +294,7 @@ interface LegendListSpecificProps { + * - size (default: true) stabilizes during size/layout changes while scrolling. + * - data (default: false) stabilizes when the data array changes. + * - shouldRestorePosition can opt out specific items from data-change anchoring. ++ * - anchor (default: "start") picks the visible item that stays in place: the first, or the last with "end". + * - undefined (default) enables scroll stabilization but skips data-change anchoring. + * - true enables both JS behaviors; false disables both. + * Native iOS maintainVisibleContentPosition is enabled when either size or data stabilization is enabled, +@@ -495,6 +497,12 @@ type LegendListPropsBase, TItemTyp + interface MaintainVisibleContentPositionConfig { + data?: boolean; + size?: boolean; ++ /** ++ * Which visible item stays in place when sizes or data change. "start" (default) keeps the first visible ++ * item in place, so changes move the content below it. "end" keeps the last visible item in place, so changes ++ * move the content above it, like in an inverted list. Use "end" for chat-style lists read from the bottom. ++ */ ++ anchor?: "start" | "end"; + shouldRestorePosition?: (item: ItemT, index: number, data: readonly ItemT[]) => boolean; + } + interface AnchoredEndSpaceReadyInfo { +diff --git a/node_modules/@legendapp/list/section-list.d.ts b/node_modules/@legendapp/list/section-list.d.ts +index 94edf333f0d..aab8b29dc8d 100644 +--- a/node_modules/@legendapp/list/section-list.d.ts ++++ b/node_modules/@legendapp/list/section-list.d.ts +@@ -60,6 +60,7 @@ type BuildSectionListDataResult = { + }; + + interface MaintainVisibleContentPositionNormalized { ++ anchor?: "start" | "end"; + data: boolean; + size: boolean; + shouldRestorePosition?: (item: ItemT, index: number, data: readonly ItemT[]) => boolean; +@@ -349,6 +350,7 @@ interface LegendListSpecificProps { + * - size (default: true) stabilizes during size/layout changes while scrolling. + * - data (default: false) stabilizes when the data array changes. + * - shouldRestorePosition can opt out specific items from data-change anchoring. ++ * - anchor (default: "start") picks the visible item that stays in place: the first, or the last with "end". + * - undefined (default) enables scroll stabilization but skips data-change anchoring. + * - true enables both JS behaviors; false disables both. + * Native iOS maintainVisibleContentPosition is enabled when either size or data stabilization is enabled, +@@ -551,6 +553,12 @@ type LegendListPropsBase, TItemTyp + interface MaintainVisibleContentPositionConfig { + data?: boolean; + size?: boolean; ++ /** ++ * Which visible item stays in place when sizes or data change. "start" (default) keeps the first visible ++ * item in place, so changes move the content below it. "end" keeps the last visible item in place, so changes ++ * move the content above it, like in an inverted list. Use "end" for chat-style lists read from the bottom. ++ */ ++ anchor?: "start" | "end"; + shouldRestorePosition?: (item: ItemT, index: number, data: readonly ItemT[]) => boolean; + } + interface AnchoredEndSpaceReadyInfo { diff --git a/patches/@legendapp/list/@legendapp+list+3.7.2+002+anchor-end-aligned-lists-to-last-visible-item.patch b/patches/@legendapp/list/@legendapp+list+3.7.2+002+anchor-end-aligned-lists-to-last-visible-item.patch deleted file mode 100644 index bad48a60d7d7..000000000000 --- a/patches/@legendapp/list/@legendapp+list+3.7.2+002+anchor-end-aligned-lists-to-last-visible-item.patch +++ /dev/null @@ -1,108 +0,0 @@ -diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js -index c102661..99cac83 100644 ---- a/node_modules/@legendapp/list/react-native.web.js -+++ b/node_modules/@legendapp/list/react-native.web.js -@@ -1706,6 +1706,7 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { - const { - maintainVisibleContentPosition: { data: mvcpData, size: mvcpScroll, shouldRestorePosition } - } = props; -+ const anchorIdsInView = props.alignItemsAtEnd ? [...idsInView].reverse() : idsInView; - const now = Date.now(); - const enableMVCPAnchorLock = (!!dataChanged || !!state.mvcpAnchorLock); - const scrollingTo = state.scrollingTo; -@@ -1743,11 +1744,11 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { - } else if (scrollTarget !== void 0) { - targetId = (_c = scrollingTo == null ? void 0 : scrollingTo.key) != null ? _c : getId(state, scrollTarget); - } else if (idsInView.length > 0 && state.didContainersLayout && !dataChanged) { -- targetId = idsInView.find((id) => indexByKey.get(id) !== void 0); -+ targetId = anchorIdsInView.find((id) => indexByKey.get(id) !== void 0); - } - if (dataChanged && idsInView.length > 0 && state.didContainersLayout) { -- for (let i = 0; i < idsInView.length; i++) { -- const id = idsInView[i]; -+ for (let i = 0; i < anchorIdsInView.length; i++) { -+ const id = anchorIdsInView[i]; - const index = previousIndexByKey.get(id); - if (index !== void 0) { - const position = positions[index]; -diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs -index ff71fd6..a7a89dd 100644 ---- a/node_modules/@legendapp/list/react-native.web.mjs -+++ b/node_modules/@legendapp/list/react-native.web.mjs -@@ -1685,6 +1685,7 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { - const { - maintainVisibleContentPosition: { data: mvcpData, size: mvcpScroll, shouldRestorePosition } - } = props; -+ const anchorIdsInView = props.alignItemsAtEnd ? [...idsInView].reverse() : idsInView; - const now = Date.now(); - const enableMVCPAnchorLock = (!!dataChanged || !!state.mvcpAnchorLock); - const scrollingTo = state.scrollingTo; -@@ -1722,11 +1723,11 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { - } else if (scrollTarget !== void 0) { - targetId = (_c = scrollingTo == null ? void 0 : scrollingTo.key) != null ? _c : getId(state, scrollTarget); - } else if (idsInView.length > 0 && state.didContainersLayout && !dataChanged) { -- targetId = idsInView.find((id) => indexByKey.get(id) !== void 0); -+ targetId = anchorIdsInView.find((id) => indexByKey.get(id) !== void 0); - } - if (dataChanged && idsInView.length > 0 && state.didContainersLayout) { -- for (let i = 0; i < idsInView.length; i++) { -- const id = idsInView[i]; -+ for (let i = 0; i < anchorIdsInView.length; i++) { -+ const id = anchorIdsInView[i]; - const index = previousIndexByKey.get(id); - if (index !== void 0) { - const position = positions[index]; -diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js -index c102661..99cac83 100644 ---- a/node_modules/@legendapp/list/react.js -+++ b/node_modules/@legendapp/list/react.js -@@ -1706,6 +1706,7 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { - const { - maintainVisibleContentPosition: { data: mvcpData, size: mvcpScroll, shouldRestorePosition } - } = props; -+ const anchorIdsInView = props.alignItemsAtEnd ? [...idsInView].reverse() : idsInView; - const now = Date.now(); - const enableMVCPAnchorLock = (!!dataChanged || !!state.mvcpAnchorLock); - const scrollingTo = state.scrollingTo; -@@ -1743,11 +1744,11 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { - } else if (scrollTarget !== void 0) { - targetId = (_c = scrollingTo == null ? void 0 : scrollingTo.key) != null ? _c : getId(state, scrollTarget); - } else if (idsInView.length > 0 && state.didContainersLayout && !dataChanged) { -- targetId = idsInView.find((id) => indexByKey.get(id) !== void 0); -+ targetId = anchorIdsInView.find((id) => indexByKey.get(id) !== void 0); - } - if (dataChanged && idsInView.length > 0 && state.didContainersLayout) { -- for (let i = 0; i < idsInView.length; i++) { -- const id = idsInView[i]; -+ for (let i = 0; i < anchorIdsInView.length; i++) { -+ const id = anchorIdsInView[i]; - const index = previousIndexByKey.get(id); - if (index !== void 0) { - const position = positions[index]; -diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs -index ff71fd6..a7a89dd 100644 ---- a/node_modules/@legendapp/list/react.mjs -+++ b/node_modules/@legendapp/list/react.mjs -@@ -1685,6 +1685,7 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { - const { - maintainVisibleContentPosition: { data: mvcpData, size: mvcpScroll, shouldRestorePosition } - } = props; -+ const anchorIdsInView = props.alignItemsAtEnd ? [...idsInView].reverse() : idsInView; - const now = Date.now(); - const enableMVCPAnchorLock = (!!dataChanged || !!state.mvcpAnchorLock); - const scrollingTo = state.scrollingTo; -@@ -1722,11 +1723,11 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { - } else if (scrollTarget !== void 0) { - targetId = (_c = scrollingTo == null ? void 0 : scrollingTo.key) != null ? _c : getId(state, scrollTarget); - } else if (idsInView.length > 0 && state.didContainersLayout && !dataChanged) { -- targetId = idsInView.find((id) => indexByKey.get(id) !== void 0); -+ targetId = anchorIdsInView.find((id) => indexByKey.get(id) !== void 0); - } - if (dataChanged && idsInView.length > 0 && state.didContainersLayout) { -- for (let i = 0; i < idsInView.length; i++) { -- const id = idsInView[i]; -+ for (let i = 0; i < anchorIdsInView.length; i++) { -+ const id = anchorIdsInView[i]; - const index = previousIndexByKey.get(id); - if (index !== void 0) { - const position = positions[index]; diff --git a/patches/@legendapp/list/@legendapp+list+3.7.2+003+anchor-data-changes-after-scroll-jumps.patch b/patches/@legendapp/list/@legendapp+list+3.7.2+003+anchor-data-changes-after-scroll-jumps.patch index 86a890e5d799..22a71ea829e0 100644 --- a/patches/@legendapp/list/@legendapp+list+3.7.2+003+anchor-data-changes-after-scroll-jumps.patch +++ b/patches/@legendapp/list/@legendapp+list+3.7.2+003+anchor-data-changes-after-scroll-jumps.patch @@ -1,8 +1,36 @@ +diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js +index e687e9c0a41..e08a0f5d005 100644 +--- a/node_modules/@legendapp/list/react-native.js ++++ b/node_modules/@legendapp/list/react-native.js +@@ -7048,6 +7048,9 @@ function checkResetContainers(ctx, dataProp, { didColumnsChange = false } = {}) + state.scrollForNextCalculateItemsInView = void 0; + } + state.positionsAreCurrent = false; ++ if (state.props.maintainVisibleContentPosition.data) { ++ state.userScrollAnchorReset = void 0; ++ } + calculateItemsInView(ctx, { dataChanged: true, doMVCP: true }); + if ((_a3 = state.pendingEndMeasurements) == null ? void 0 : _a3.size) { + const removedMeasurements = new Set(state.pendingEndMeasurements); +diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs +index 6271d782ea7..5eac366737d 100644 +--- a/node_modules/@legendapp/list/react-native.mjs ++++ b/node_modules/@legendapp/list/react-native.mjs +@@ -7027,6 +7027,9 @@ function checkResetContainers(ctx, dataProp, { didColumnsChange = false } = {}) + state.scrollForNextCalculateItemsInView = void 0; + } + state.positionsAreCurrent = false; ++ if (state.props.maintainVisibleContentPosition.data) { ++ state.userScrollAnchorReset = void 0; ++ } + calculateItemsInView(ctx, { dataChanged: true, doMVCP: true }); + if ((_a3 = state.pendingEndMeasurements) == null ? void 0 : _a3.size) { + const removedMeasurements = new Set(state.pendingEndMeasurements); diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js -index 99cac83..30d9757 100644 +index 2b3171a6f4c..b7c3ecb0d71 100644 --- a/node_modules/@legendapp/list/react-native.web.js +++ b/node_modules/@legendapp/list/react-native.web.js -@@ -7880,6 +7880,9 @@ function checkResetContainers(ctx, dataProp, { didColumnsChange = false } = {}) +@@ -7886,6 +7886,9 @@ function checkResetContainers(ctx, dataProp, { didColumnsChange = false } = {}) state.scrollForNextCalculateItemsInView = void 0; } state.positionsAreCurrent = false; @@ -13,10 +41,10 @@ index 99cac83..30d9757 100644 if ((_a3 = state.pendingEndMeasurements) == null ? void 0 : _a3.size) { const removedMeasurements = new Set(state.pendingEndMeasurements); diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs -index a7a89dd..f55dea9 100644 +index dbd6fbbf3e8..0831a154ba8 100644 --- a/node_modules/@legendapp/list/react-native.web.mjs +++ b/node_modules/@legendapp/list/react-native.web.mjs -@@ -7859,6 +7859,9 @@ function checkResetContainers(ctx, dataProp, { didColumnsChange = false } = {}) +@@ -7865,6 +7865,9 @@ function checkResetContainers(ctx, dataProp, { didColumnsChange = false } = {}) state.scrollForNextCalculateItemsInView = void 0; } state.positionsAreCurrent = false; @@ -27,10 +55,10 @@ index a7a89dd..f55dea9 100644 if ((_a3 = state.pendingEndMeasurements) == null ? void 0 : _a3.size) { const removedMeasurements = new Set(state.pendingEndMeasurements); diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js -index 99cac83..30d9757 100644 +index 2b3171a6f4c..b7c3ecb0d71 100644 --- a/node_modules/@legendapp/list/react.js +++ b/node_modules/@legendapp/list/react.js -@@ -7880,6 +7880,9 @@ function checkResetContainers(ctx, dataProp, { didColumnsChange = false } = {}) +@@ -7886,6 +7886,9 @@ function checkResetContainers(ctx, dataProp, { didColumnsChange = false } = {}) state.scrollForNextCalculateItemsInView = void 0; } state.positionsAreCurrent = false; @@ -41,10 +69,10 @@ index 99cac83..30d9757 100644 if ((_a3 = state.pendingEndMeasurements) == null ? void 0 : _a3.size) { const removedMeasurements = new Set(state.pendingEndMeasurements); diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs -index a7a89dd..f55dea9 100644 +index dbd6fbbf3e8..0831a154ba8 100644 --- a/node_modules/@legendapp/list/react.mjs +++ b/node_modules/@legendapp/list/react.mjs -@@ -7859,6 +7859,9 @@ function checkResetContainers(ctx, dataProp, { didColumnsChange = false } = {}) +@@ -7865,6 +7865,9 @@ function checkResetContainers(ctx, dataProp, { didColumnsChange = false } = {}) state.scrollForNextCalculateItemsInView = void 0; } state.positionsAreCurrent = false; diff --git a/patches/@legendapp/list/@legendapp+list+3.7.2+004+land-initial-mount-adjustments-once.patch b/patches/@legendapp/list/@legendapp+list+3.7.2+004+land-initial-mount-adjustments-once.patch new file mode 100644 index 000000000000..c08c1313b69e --- /dev/null +++ b/patches/@legendapp/list/@legendapp+list+3.7.2+004+land-initial-mount-adjustments-once.patch @@ -0,0 +1,472 @@ +diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js +index e08a0f5d005..c9df90ee7b9 100644 +--- a/node_modules/@legendapp/list/react-native.js ++++ b/node_modules/@legendapp/list/react-native.js +@@ -1063,9 +1063,6 @@ function requestAdjust(ctx, positionDiff, source) { + ctx.doScrollTo({ horizontal: state.props.horizontal, isAdjustment: true, offset: state.scroll }); + } else { + state.scrollAdjustHandler.requestAdjust(positionDiff); +- if (state.adjustingFromInitialMount) { +- state.adjustingFromInitialMount--; +- } + } + }; + state.scroll += positionDiff; +@@ -1073,6 +1070,7 @@ function requestAdjust(ctx, positionDiff, source) { + state.userScrollContentSize += positionDiff; + } + state.scrollForNextCalculateItemsInView = void 0; ++ shiftNativeInitialScrollTarget(state, positionDiff); + const readyToRender = peek$(ctx, "readyToRender"); + if (readyToRender) { + doit(); +@@ -1104,10 +1102,33 @@ function requestAdjust(ctx, positionDiff, source) { + } + } else { + state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; +- requestAnimationFrame(doit); ++ const nativeScrollEpoch = state.nativeScrollEpoch; ++ requestAnimationFrame(() => { ++ if (state.nativeScrollEpoch === nativeScrollEpoch) { ++ doit(); ++ } ++ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 1) - 1; ++ if (!state.adjustingFromInitialMount) { ++ setInitialRenderState(ctx, {}); ++ } ++ }); + } + } + } ++function shiftNativeInitialScrollTarget(state, positionDiff) { ++ const scrollingTo = state.scrollingTo; ++ if (Platform.OS === "web" || !(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) || scrollingTo.animated) { ++ return; ++ } ++ scrollingTo.offset += positionDiff; ++ if (scrollingTo.targetOffset !== void 0) { ++ scrollingTo.targetOffset += positionDiff; ++ } ++ const watchdog = initialScrollWatchdog.get(state); ++ if (watchdog) { ++ watchdog.targetOffset += positionDiff; ++ } ++} + + // src/core/finishMaintainScrollAtEnd.ts + function interruptMaintainScrollAtEnd(ctx) { +@@ -1529,7 +1550,9 @@ function setInitialRenderState(ctx, { + if (didInitialScroll) { + state.didFinishInitialScroll = true; + } +- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); ++ const isReadyToRender = Boolean( ++ state.didContainersLayout && state.didFinishInitialScroll && !state.adjustingFromInitialMount ++ ); + if (isReadyToRender && !peek$(ctx, "readyToRender")) { + set$(ctx, "readyToRender", true); + setAdaptiveRender(ctx, "normal", "ready"); +@@ -2560,8 +2583,11 @@ function checkFinishedScrollFrame(ctx) { + } + } + function scrollToFallbackOffset(ctx, offset, animated = false) { +- var _a3; +- (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({ animated, ...getNativeScrollPoint(ctx, offset) }); ++ var _a3, _b; ++ if (Platform.OS !== "web") { ++ ctx.state.nativeScrollEpoch = ((_a3 = ctx.state.nativeScrollEpoch) != null ? _a3 : 0) + 1; ++ } ++ (_b = ctx.state.refScroller.current) == null ? void 0 : _b.scrollTo({ animated, ...getNativeScrollPoint(ctx, offset) }); + } + function checkFinishedScrollFallback(ctx) { + const state = ctx.state; +@@ -2642,6 +2668,7 @@ function checkFinishedScrollFallback(ctx) { + + // src/core/doScrollTo.native.ts + function doScrollTo(ctx, params) { ++ var _a3; + const state = ctx.state; + const { animated, horizontal, isInitialScroll, offset } = params; + const isAnimated = !!animated; +@@ -2652,6 +2679,7 @@ function doScrollTo(ctx, params) { + } + if (!params.isAdjustment) { + state.userScrollContentSize = void 0; ++ state.nativeScrollEpoch = ((_a3 = state.nativeScrollEpoch) != null ? _a3 : 0) + 1; + } + scroller.scrollTo({ animated: isAnimated, ...getNativeScrollPoint(ctx, offset, horizontal) }); + if (isInitialScroll) { +diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs +index 5eac366737d..7de20403d1d 100644 +--- a/node_modules/@legendapp/list/react-native.mjs ++++ b/node_modules/@legendapp/list/react-native.mjs +@@ -1042,9 +1042,6 @@ function requestAdjust(ctx, positionDiff, source) { + ctx.doScrollTo({ horizontal: state.props.horizontal, isAdjustment: true, offset: state.scroll }); + } else { + state.scrollAdjustHandler.requestAdjust(positionDiff); +- if (state.adjustingFromInitialMount) { +- state.adjustingFromInitialMount--; +- } + } + }; + state.scroll += positionDiff; +@@ -1052,6 +1049,7 @@ function requestAdjust(ctx, positionDiff, source) { + state.userScrollContentSize += positionDiff; + } + state.scrollForNextCalculateItemsInView = void 0; ++ shiftNativeInitialScrollTarget(state, positionDiff); + const readyToRender = peek$(ctx, "readyToRender"); + if (readyToRender) { + doit(); +@@ -1083,10 +1081,33 @@ function requestAdjust(ctx, positionDiff, source) { + } + } else { + state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; +- requestAnimationFrame(doit); ++ const nativeScrollEpoch = state.nativeScrollEpoch; ++ requestAnimationFrame(() => { ++ if (state.nativeScrollEpoch === nativeScrollEpoch) { ++ doit(); ++ } ++ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 1) - 1; ++ if (!state.adjustingFromInitialMount) { ++ setInitialRenderState(ctx, {}); ++ } ++ }); + } + } + } ++function shiftNativeInitialScrollTarget(state, positionDiff) { ++ const scrollingTo = state.scrollingTo; ++ if (Platform.OS === "web" || !(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) || scrollingTo.animated) { ++ return; ++ } ++ scrollingTo.offset += positionDiff; ++ if (scrollingTo.targetOffset !== void 0) { ++ scrollingTo.targetOffset += positionDiff; ++ } ++ const watchdog = initialScrollWatchdog.get(state); ++ if (watchdog) { ++ watchdog.targetOffset += positionDiff; ++ } ++} + + // src/core/finishMaintainScrollAtEnd.ts + function interruptMaintainScrollAtEnd(ctx) { +@@ -1508,7 +1529,9 @@ function setInitialRenderState(ctx, { + if (didInitialScroll) { + state.didFinishInitialScroll = true; + } +- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); ++ const isReadyToRender = Boolean( ++ state.didContainersLayout && state.didFinishInitialScroll && !state.adjustingFromInitialMount ++ ); + if (isReadyToRender && !peek$(ctx, "readyToRender")) { + set$(ctx, "readyToRender", true); + setAdaptiveRender(ctx, "normal", "ready"); +@@ -2539,8 +2562,11 @@ function checkFinishedScrollFrame(ctx) { + } + } + function scrollToFallbackOffset(ctx, offset, animated = false) { +- var _a3; +- (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({ animated, ...getNativeScrollPoint(ctx, offset) }); ++ var _a3, _b; ++ if (Platform.OS !== "web") { ++ ctx.state.nativeScrollEpoch = ((_a3 = ctx.state.nativeScrollEpoch) != null ? _a3 : 0) + 1; ++ } ++ (_b = ctx.state.refScroller.current) == null ? void 0 : _b.scrollTo({ animated, ...getNativeScrollPoint(ctx, offset) }); + } + function checkFinishedScrollFallback(ctx) { + const state = ctx.state; +@@ -2621,6 +2647,7 @@ function checkFinishedScrollFallback(ctx) { + + // src/core/doScrollTo.native.ts + function doScrollTo(ctx, params) { ++ var _a3; + const state = ctx.state; + const { animated, horizontal, isInitialScroll, offset } = params; + const isAnimated = !!animated; +@@ -2631,6 +2658,7 @@ function doScrollTo(ctx, params) { + } + if (!params.isAdjustment) { + state.userScrollContentSize = void 0; ++ state.nativeScrollEpoch = ((_a3 = state.nativeScrollEpoch) != null ? _a3 : 0) + 1; + } + scroller.scrollTo({ animated: isAnimated, ...getNativeScrollPoint(ctx, offset, horizontal) }); + if (isInitialScroll) { +diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js +index b7c3ecb0d71..10dd9b9c9bb 100644 +--- a/node_modules/@legendapp/list/react-native.web.js ++++ b/node_modules/@legendapp/list/react-native.web.js +@@ -1001,9 +1001,6 @@ function requestAdjust(ctx, positionDiff, source) { + const doit = () => { + { + state.scrollAdjustHandler.requestAdjust(positionDiff); +- if (state.adjustingFromInitialMount) { +- state.adjustingFromInitialMount--; +- } + } + }; + state.scroll += positionDiff; +@@ -1011,15 +1008,31 @@ function requestAdjust(ctx, positionDiff, source) { + state.userScrollContentSize += positionDiff; + } + state.scrollForNextCalculateItemsInView = void 0; ++ shiftNativeInitialScrollTarget(state); + const readyToRender = peek$(ctx, "readyToRender"); + if (readyToRender) { + doit(); + } else { + state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; +- requestAnimationFrame(doit); ++ const nativeScrollEpoch = state.nativeScrollEpoch; ++ requestAnimationFrame(() => { ++ if (state.nativeScrollEpoch === nativeScrollEpoch) { ++ doit(); ++ } ++ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 1) - 1; ++ if (!state.adjustingFromInitialMount) { ++ setInitialRenderState(ctx, {}); ++ } ++ }); + } + } + } ++function shiftNativeInitialScrollTarget(state, positionDiff) { ++ state.scrollingTo; ++ { ++ return; ++ } ++} + + // src/core/finishMaintainScrollAtEnd.ts + function interruptMaintainScrollAtEnd(ctx) { +@@ -1435,7 +1448,9 @@ function setInitialRenderState(ctx, { + if (didInitialScroll) { + state.didFinishInitialScroll = true; + } +- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); ++ const isReadyToRender = Boolean( ++ state.didContainersLayout && state.didFinishInitialScroll && !state.adjustingFromInitialMount ++ ); + if (isReadyToRender && !peek$(ctx, "readyToRender")) { + set$(ctx, "readyToRender", true); + setAdaptiveRender(ctx, "normal", "ready"); +@@ -2449,8 +2464,8 @@ function checkFinishedScrollFrame(ctx) { + } + } + function scrollToFallbackOffset(ctx, offset, animated = false) { +- var _a3; +- (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({ animated, ...getNativeScrollPoint(ctx, offset) }); ++ var _b; ++ (_b = ctx.state.refScroller.current) == null ? void 0 : _b.scrollTo({ animated, ...getNativeScrollPoint(ctx, offset) }); + } + function checkFinishedScrollFallback(ctx) { + const state = ctx.state; +diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs +index 0831a154ba8..0ba06dac531 100644 +--- a/node_modules/@legendapp/list/react-native.web.mjs ++++ b/node_modules/@legendapp/list/react-native.web.mjs +@@ -980,9 +980,6 @@ function requestAdjust(ctx, positionDiff, source) { + const doit = () => { + { + state.scrollAdjustHandler.requestAdjust(positionDiff); +- if (state.adjustingFromInitialMount) { +- state.adjustingFromInitialMount--; +- } + } + }; + state.scroll += positionDiff; +@@ -990,15 +987,31 @@ function requestAdjust(ctx, positionDiff, source) { + state.userScrollContentSize += positionDiff; + } + state.scrollForNextCalculateItemsInView = void 0; ++ shiftNativeInitialScrollTarget(state); + const readyToRender = peek$(ctx, "readyToRender"); + if (readyToRender) { + doit(); + } else { + state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; +- requestAnimationFrame(doit); ++ const nativeScrollEpoch = state.nativeScrollEpoch; ++ requestAnimationFrame(() => { ++ if (state.nativeScrollEpoch === nativeScrollEpoch) { ++ doit(); ++ } ++ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 1) - 1; ++ if (!state.adjustingFromInitialMount) { ++ setInitialRenderState(ctx, {}); ++ } ++ }); + } + } + } ++function shiftNativeInitialScrollTarget(state, positionDiff) { ++ state.scrollingTo; ++ { ++ return; ++ } ++} + + // src/core/finishMaintainScrollAtEnd.ts + function interruptMaintainScrollAtEnd(ctx) { +@@ -1414,7 +1427,9 @@ function setInitialRenderState(ctx, { + if (didInitialScroll) { + state.didFinishInitialScroll = true; + } +- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); ++ const isReadyToRender = Boolean( ++ state.didContainersLayout && state.didFinishInitialScroll && !state.adjustingFromInitialMount ++ ); + if (isReadyToRender && !peek$(ctx, "readyToRender")) { + set$(ctx, "readyToRender", true); + setAdaptiveRender(ctx, "normal", "ready"); +@@ -2428,8 +2443,8 @@ function checkFinishedScrollFrame(ctx) { + } + } + function scrollToFallbackOffset(ctx, offset, animated = false) { +- var _a3; +- (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({ animated, ...getNativeScrollPoint(ctx, offset) }); ++ var _b; ++ (_b = ctx.state.refScroller.current) == null ? void 0 : _b.scrollTo({ animated, ...getNativeScrollPoint(ctx, offset) }); + } + function checkFinishedScrollFallback(ctx) { + const state = ctx.state; +diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js +index b7c3ecb0d71..10dd9b9c9bb 100644 +--- a/node_modules/@legendapp/list/react.js ++++ b/node_modules/@legendapp/list/react.js +@@ -1001,9 +1001,6 @@ function requestAdjust(ctx, positionDiff, source) { + const doit = () => { + { + state.scrollAdjustHandler.requestAdjust(positionDiff); +- if (state.adjustingFromInitialMount) { +- state.adjustingFromInitialMount--; +- } + } + }; + state.scroll += positionDiff; +@@ -1011,15 +1008,31 @@ function requestAdjust(ctx, positionDiff, source) { + state.userScrollContentSize += positionDiff; + } + state.scrollForNextCalculateItemsInView = void 0; ++ shiftNativeInitialScrollTarget(state); + const readyToRender = peek$(ctx, "readyToRender"); + if (readyToRender) { + doit(); + } else { + state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; +- requestAnimationFrame(doit); ++ const nativeScrollEpoch = state.nativeScrollEpoch; ++ requestAnimationFrame(() => { ++ if (state.nativeScrollEpoch === nativeScrollEpoch) { ++ doit(); ++ } ++ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 1) - 1; ++ if (!state.adjustingFromInitialMount) { ++ setInitialRenderState(ctx, {}); ++ } ++ }); + } + } + } ++function shiftNativeInitialScrollTarget(state, positionDiff) { ++ state.scrollingTo; ++ { ++ return; ++ } ++} + + // src/core/finishMaintainScrollAtEnd.ts + function interruptMaintainScrollAtEnd(ctx) { +@@ -1435,7 +1448,9 @@ function setInitialRenderState(ctx, { + if (didInitialScroll) { + state.didFinishInitialScroll = true; + } +- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); ++ const isReadyToRender = Boolean( ++ state.didContainersLayout && state.didFinishInitialScroll && !state.adjustingFromInitialMount ++ ); + if (isReadyToRender && !peek$(ctx, "readyToRender")) { + set$(ctx, "readyToRender", true); + setAdaptiveRender(ctx, "normal", "ready"); +@@ -2449,8 +2464,8 @@ function checkFinishedScrollFrame(ctx) { + } + } + function scrollToFallbackOffset(ctx, offset, animated = false) { +- var _a3; +- (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({ animated, ...getNativeScrollPoint(ctx, offset) }); ++ var _b; ++ (_b = ctx.state.refScroller.current) == null ? void 0 : _b.scrollTo({ animated, ...getNativeScrollPoint(ctx, offset) }); + } + function checkFinishedScrollFallback(ctx) { + const state = ctx.state; +diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs +index 0831a154ba8..0ba06dac531 100644 +--- a/node_modules/@legendapp/list/react.mjs ++++ b/node_modules/@legendapp/list/react.mjs +@@ -980,9 +980,6 @@ function requestAdjust(ctx, positionDiff, source) { + const doit = () => { + { + state.scrollAdjustHandler.requestAdjust(positionDiff); +- if (state.adjustingFromInitialMount) { +- state.adjustingFromInitialMount--; +- } + } + }; + state.scroll += positionDiff; +@@ -990,15 +987,31 @@ function requestAdjust(ctx, positionDiff, source) { + state.userScrollContentSize += positionDiff; + } + state.scrollForNextCalculateItemsInView = void 0; ++ shiftNativeInitialScrollTarget(state); + const readyToRender = peek$(ctx, "readyToRender"); + if (readyToRender) { + doit(); + } else { + state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; +- requestAnimationFrame(doit); ++ const nativeScrollEpoch = state.nativeScrollEpoch; ++ requestAnimationFrame(() => { ++ if (state.nativeScrollEpoch === nativeScrollEpoch) { ++ doit(); ++ } ++ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 1) - 1; ++ if (!state.adjustingFromInitialMount) { ++ setInitialRenderState(ctx, {}); ++ } ++ }); + } + } + } ++function shiftNativeInitialScrollTarget(state, positionDiff) { ++ state.scrollingTo; ++ { ++ return; ++ } ++} + + // src/core/finishMaintainScrollAtEnd.ts + function interruptMaintainScrollAtEnd(ctx) { +@@ -1414,7 +1427,9 @@ function setInitialRenderState(ctx, { + if (didInitialScroll) { + state.didFinishInitialScroll = true; + } +- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); ++ const isReadyToRender = Boolean( ++ state.didContainersLayout && state.didFinishInitialScroll && !state.adjustingFromInitialMount ++ ); + if (isReadyToRender && !peek$(ctx, "readyToRender")) { + set$(ctx, "readyToRender", true); + setAdaptiveRender(ctx, "normal", "ready"); +@@ -2428,8 +2443,8 @@ function checkFinishedScrollFrame(ctx) { + } + } + function scrollToFallbackOffset(ctx, offset, animated = false) { +- var _a3; +- (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({ animated, ...getNativeScrollPoint(ctx, offset) }); ++ var _b; ++ (_b = ctx.state.refScroller.current) == null ? void 0 : _b.scrollTo({ animated, ...getNativeScrollPoint(ctx, offset) }); + } + function checkFinishedScrollFallback(ctx) { + const state = ctx.state; diff --git a/patches/@legendapp/list/@legendapp+list+3.7.2+004+reveal-list-after-initial-scroll-adjustments.patch b/patches/@legendapp/list/@legendapp+list+3.7.2+004+reveal-list-after-initial-scroll-adjustments.patch deleted file mode 100644 index 3ced246136c8..000000000000 --- a/patches/@legendapp/list/@legendapp+list+3.7.2+004+reveal-list-after-initial-scroll-adjustments.patch +++ /dev/null @@ -1,240 +0,0 @@ -diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js -index 4af685d..8fd2025 100644 ---- a/node_modules/@legendapp/list/react-native.js -+++ b/node_modules/@legendapp/list/react-native.js -@@ -1063,9 +1063,6 @@ function requestAdjust(ctx, positionDiff, source) { - ctx.doScrollTo({ horizontal: state.props.horizontal, isAdjustment: true, offset: state.scroll }); - } else { - state.scrollAdjustHandler.requestAdjust(positionDiff); -- if (state.adjustingFromInitialMount) { -- state.adjustingFromInitialMount--; -- } - } - }; - state.scroll += positionDiff; -@@ -1104,7 +1101,13 @@ function requestAdjust(ctx, positionDiff, source) { - } - } else { - state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; -- requestAnimationFrame(doit); -+ requestAnimationFrame(() => { -+ doit(); -+ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 1) - 1; -+ if (!state.adjustingFromInitialMount) { -+ setInitialRenderState(ctx, {}); -+ } -+ }); - } - } - } -@@ -1529,7 +1532,9 @@ function setInitialRenderState(ctx, { - if (didInitialScroll) { - state.didFinishInitialScroll = true; - } -- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); -+ const isReadyToRender = Boolean( -+ state.didContainersLayout && state.didFinishInitialScroll && !state.adjustingFromInitialMount -+ ); - if (isReadyToRender && !peek$(ctx, "readyToRender")) { - set$(ctx, "readyToRender", true); - setAdaptiveRender(ctx, "normal", "ready"); -diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs -index 9ebfddc..53fb116 100644 ---- a/node_modules/@legendapp/list/react-native.mjs -+++ b/node_modules/@legendapp/list/react-native.mjs -@@ -1042,9 +1042,6 @@ function requestAdjust(ctx, positionDiff, source) { - ctx.doScrollTo({ horizontal: state.props.horizontal, isAdjustment: true, offset: state.scroll }); - } else { - state.scrollAdjustHandler.requestAdjust(positionDiff); -- if (state.adjustingFromInitialMount) { -- state.adjustingFromInitialMount--; -- } - } - }; - state.scroll += positionDiff; -@@ -1083,7 +1080,13 @@ function requestAdjust(ctx, positionDiff, source) { - } - } else { - state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; -- requestAnimationFrame(doit); -+ requestAnimationFrame(() => { -+ doit(); -+ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 1) - 1; -+ if (!state.adjustingFromInitialMount) { -+ setInitialRenderState(ctx, {}); -+ } -+ }); - } - } - } -@@ -1508,7 +1511,9 @@ function setInitialRenderState(ctx, { - if (didInitialScroll) { - state.didFinishInitialScroll = true; - } -- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); -+ const isReadyToRender = Boolean( -+ state.didContainersLayout && state.didFinishInitialScroll && !state.adjustingFromInitialMount -+ ); - if (isReadyToRender && !peek$(ctx, "readyToRender")) { - set$(ctx, "readyToRender", true); - setAdaptiveRender(ctx, "normal", "ready"); -diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js -index 30d9757..b782503 100644 ---- a/node_modules/@legendapp/list/react-native.web.js -+++ b/node_modules/@legendapp/list/react-native.web.js -@@ -1001,9 +1001,6 @@ function requestAdjust(ctx, positionDiff, source) { - const doit = () => { - { - state.scrollAdjustHandler.requestAdjust(positionDiff); -- if (state.adjustingFromInitialMount) { -- state.adjustingFromInitialMount--; -- } - } - }; - state.scroll += positionDiff; -@@ -1016,7 +1013,13 @@ function requestAdjust(ctx, positionDiff, source) { - doit(); - } else { - state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; -- requestAnimationFrame(doit); -+ requestAnimationFrame(() => { -+ doit(); -+ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 1) - 1; -+ if (!state.adjustingFromInitialMount) { -+ setInitialRenderState(ctx, {}); -+ } -+ }); - } - } - } -@@ -1435,7 +1438,9 @@ function setInitialRenderState(ctx, { - if (didInitialScroll) { - state.didFinishInitialScroll = true; - } -- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); -+ const isReadyToRender = Boolean( -+ state.didContainersLayout && state.didFinishInitialScroll && !state.adjustingFromInitialMount -+ ); - if (isReadyToRender && !peek$(ctx, "readyToRender")) { - set$(ctx, "readyToRender", true); - setAdaptiveRender(ctx, "normal", "ready"); -diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs -index f55dea9..a1bcd90 100644 ---- a/node_modules/@legendapp/list/react-native.web.mjs -+++ b/node_modules/@legendapp/list/react-native.web.mjs -@@ -980,9 +980,6 @@ function requestAdjust(ctx, positionDiff, source) { - const doit = () => { - { - state.scrollAdjustHandler.requestAdjust(positionDiff); -- if (state.adjustingFromInitialMount) { -- state.adjustingFromInitialMount--; -- } - } - }; - state.scroll += positionDiff; -@@ -995,7 +992,13 @@ function requestAdjust(ctx, positionDiff, source) { - doit(); - } else { - state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; -- requestAnimationFrame(doit); -+ requestAnimationFrame(() => { -+ doit(); -+ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 1) - 1; -+ if (!state.adjustingFromInitialMount) { -+ setInitialRenderState(ctx, {}); -+ } -+ }); - } - } - } -@@ -1414,7 +1417,9 @@ function setInitialRenderState(ctx, { - if (didInitialScroll) { - state.didFinishInitialScroll = true; - } -- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); -+ const isReadyToRender = Boolean( -+ state.didContainersLayout && state.didFinishInitialScroll && !state.adjustingFromInitialMount -+ ); - if (isReadyToRender && !peek$(ctx, "readyToRender")) { - set$(ctx, "readyToRender", true); - setAdaptiveRender(ctx, "normal", "ready"); -diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js -index 30d9757..b782503 100644 ---- a/node_modules/@legendapp/list/react.js -+++ b/node_modules/@legendapp/list/react.js -@@ -1001,9 +1001,6 @@ function requestAdjust(ctx, positionDiff, source) { - const doit = () => { - { - state.scrollAdjustHandler.requestAdjust(positionDiff); -- if (state.adjustingFromInitialMount) { -- state.adjustingFromInitialMount--; -- } - } - }; - state.scroll += positionDiff; -@@ -1016,7 +1013,13 @@ function requestAdjust(ctx, positionDiff, source) { - doit(); - } else { - state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; -- requestAnimationFrame(doit); -+ requestAnimationFrame(() => { -+ doit(); -+ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 1) - 1; -+ if (!state.adjustingFromInitialMount) { -+ setInitialRenderState(ctx, {}); -+ } -+ }); - } - } - } -@@ -1435,7 +1438,9 @@ function setInitialRenderState(ctx, { - if (didInitialScroll) { - state.didFinishInitialScroll = true; - } -- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); -+ const isReadyToRender = Boolean( -+ state.didContainersLayout && state.didFinishInitialScroll && !state.adjustingFromInitialMount -+ ); - if (isReadyToRender && !peek$(ctx, "readyToRender")) { - set$(ctx, "readyToRender", true); - setAdaptiveRender(ctx, "normal", "ready"); -diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs -index f55dea9..a1bcd90 100644 ---- a/node_modules/@legendapp/list/react.mjs -+++ b/node_modules/@legendapp/list/react.mjs -@@ -980,9 +980,6 @@ function requestAdjust(ctx, positionDiff, source) { - const doit = () => { - { - state.scrollAdjustHandler.requestAdjust(positionDiff); -- if (state.adjustingFromInitialMount) { -- state.adjustingFromInitialMount--; -- } - } - }; - state.scroll += positionDiff; -@@ -995,7 +992,13 @@ function requestAdjust(ctx, positionDiff, source) { - doit(); - } else { - state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; -- requestAnimationFrame(doit); -+ requestAnimationFrame(() => { -+ doit(); -+ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 1) - 1; -+ if (!state.adjustingFromInitialMount) { -+ setInitialRenderState(ctx, {}); -+ } -+ }); - } - } - } -@@ -1414,7 +1417,9 @@ function setInitialRenderState(ctx, { - if (didInitialScroll) { - state.didFinishInitialScroll = true; - } -- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll); -+ const isReadyToRender = Boolean( -+ state.didContainersLayout && state.didFinishInitialScroll && !state.adjustingFromInitialMount -+ ); - if (isReadyToRender && !peek$(ctx, "readyToRender")) { - set$(ctx, "readyToRender", true); - setAdaptiveRender(ctx, "normal", "ready"); diff --git a/patches/@legendapp/list/@legendapp+list+3.7.2+005+keep-scroll-adjust-anchor-precise.patch b/patches/@legendapp/list/@legendapp+list+3.7.2+005+keep-scroll-adjust-anchor-precise.patch index 8c8072e4d7a7..1d0eeaac24e7 100644 --- a/patches/@legendapp/list/@legendapp+list+3.7.2+005+keep-scroll-adjust-anchor-precise.patch +++ b/patches/@legendapp/list/@legendapp+list+3.7.2+005+keep-scroll-adjust-anchor-precise.patch @@ -1,29 +1,25 @@ diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js +index c9df90ee7b9..17727711406 100644 --- a/node_modules/@legendapp/list/react-native.js +++ b/node_modules/@legendapp/list/react-native.js -@@ -6636,7 +6636,10 @@ +@@ -6672,7 +6672,7 @@ function getAutoOtherAxisStyle({ function ScrollAdjust() { var _a3; const ctx = useStateContext(); - const bias = 1e7; -+ // The bias keeps this anchor below the scroll offset, so native maintainVisibleContentPosition always anchors to it. -+ // Native layout stores positions as 32-bit floats, which hold 10,000,000 only to the nearest whole point. That -+ // rounded each adjustment by up to half a point, and the visible items moved by the difference. + const bias = 1e6; const [scrollAdjust, scrollAdjustUserOffset] = useArr$(["scrollAdjust", "scrollAdjustUserOffset"]); const scrollOffset = (scrollAdjust || 0) + (scrollAdjustUserOffset || 0) + bias; const horizontal = !!((_a3 = ctx.state) == null ? void 0 : _a3.props.horizontal); diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs +index 7de20403d1d..a895a1aad01 100644 --- a/node_modules/@legendapp/list/react-native.mjs +++ b/node_modules/@legendapp/list/react-native.mjs -@@ -6615,7 +6615,10 @@ +@@ -6651,7 +6651,7 @@ function getAutoOtherAxisStyle({ function ScrollAdjust() { var _a3; const ctx = useStateContext(); - const bias = 1e7; -+ // The bias keeps this anchor below the scroll offset, so native maintainVisibleContentPosition always anchors to it. -+ // Native layout stores positions as 32-bit floats, which hold 10,000,000 only to the nearest whole point. That -+ // rounded each adjustment by up to half a point, and the visible items moved by the difference. + const bias = 1e6; const [scrollAdjust, scrollAdjustUserOffset] = useArr$(["scrollAdjust", "scrollAdjustUserOffset"]); const scrollOffset = (scrollAdjust || 0) + (scrollAdjustUserOffset || 0) + bias; diff --git a/patches/@legendapp/list/@legendapp+list+3.7.2+006+finish-silent-initial-scroll-retries.patch b/patches/@legendapp/list/@legendapp+list+3.7.2+006+finish-silent-initial-scroll-retries.patch new file mode 100644 index 000000000000..9eccd66abcac --- /dev/null +++ b/patches/@legendapp/list/@legendapp+list+3.7.2+006+finish-silent-initial-scroll-retries.patch @@ -0,0 +1,282 @@ +diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js +index 17727711406..3ccffa79110 100644 +--- a/node_modules/@legendapp/list/react-native.js ++++ b/node_modules/@legendapp/list/react-native.js +@@ -518,7 +518,9 @@ var ENABLE_DEVMODE = IS_DEV && false; + var ENABLE_DEBUG_VIEW = IS_DEV && false; + + // src/core/cancelImperativeScroll.ts +-function cancelScrollCompletionChecks({ scheduledWork }) { ++function cancelScrollCompletionChecks(state) { ++ const { scheduledWork } = state; ++ state.silentInitialScrollReturn = void 0; + scheduledWork.cancel("checkFinishedScrollFrame"); + scheduledWork.cancel("checkFinishedScrollRetryFrame"); + scheduledWork.cancel("checkFinishedScrollFallback"); +@@ -2460,11 +2462,12 @@ function doMaintainScrollAtEnd(ctx, options) { + + // src/core/finishScrollTo.ts + function finishScrollTo(ctx) { +- var _a3, _b, _c; ++ var _a3, _b, _c, _d; + const state = ctx.state; + if (state == null ? void 0 : state.scrollingTo) { ++ (_a3 = state.silentInitialScrollReturn) == null ? void 0 : _a3.call(state); + cancelScrollCompletionChecks(state); +- const resolvePendingScroll = ((_a3 = ctx.scrollRequestTracker) == null ? void 0 : _a3.isWaitingToRun()) ? void 0 : state.pendingScrollResolve; ++ const resolvePendingScroll = ((_b = ctx.scrollRequestTracker) == null ? void 0 : _b.isWaitingToRun()) ? void 0 : state.pendingScrollResolve; + if (resolvePendingScroll) { + state.pendingScrollResolve = void 0; + } +@@ -2487,8 +2490,8 @@ function finishScrollTo(ctx) { + } + }; + if (scrollingTo.isInitialScroll || state.initialScroll) { +- const isOffsetSession = ((_b = state.initialScrollSession) == null ? void 0 : _b.kind) === "offset"; +- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_c = state.initialScroll) == null ? void 0 : _c.viewPosition) === 1; ++ const isOffsetSession = ((_c = state.initialScrollSession) == null ? void 0 : _c.kind) === "offset"; ++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_d = state.initialScroll) == null ? void 0 : _d.viewPosition) === 1; + finishInitialScroll(ctx, { + onFinished: complete, + preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, +@@ -2636,10 +2639,13 @@ function checkFinishedScrollFallback(ctx) { + const jiggleOffset = targetOffset >= SILENT_INITIAL_SCROLL_TARGET_EPSILON ? targetOffset - SILENT_INITIAL_SCROLL_TARGET_EPSILON : targetOffset + SILENT_INITIAL_SCROLL_TARGET_EPSILON; + initialScrollCompletion.markSilentInitialScrollRetry(state); + scrollToFallbackOffset(ctx, jiggleOffset); +- state.scheduledWork.frame( +- () => scrollToFallbackOffset(ctx, targetOffset), +- "checkFinishedScrollRetryFrame" +- ); ++ const returnToTarget = () => { ++ var _a4, _b2; ++ state.silentInitialScrollReturn = void 0; ++ scrollToFallbackOffset(ctx, (_b2 = (_a4 = initialScrollWatchdog.get(state)) == null ? void 0 : _a4.targetOffset) != null ? _b2 : targetOffset); ++ }; ++ state.silentInitialScrollReturn = returnToTarget; ++ state.scheduledWork.frame(returnToTarget, "checkFinishedScrollRetryFrame"); + scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); + } else if (shouldRetryUnalignedEndScroll) { + scrollToFallbackOffset(ctx, completionState.clampedTargetOffset, !!isStillScrollingTo.animated); +diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs +index a895a1aad01..676c117a45a 100644 +--- a/node_modules/@legendapp/list/react-native.mjs ++++ b/node_modules/@legendapp/list/react-native.mjs +@@ -497,7 +497,9 @@ var ENABLE_DEVMODE = IS_DEV && false; + var ENABLE_DEBUG_VIEW = IS_DEV && false; + + // src/core/cancelImperativeScroll.ts +-function cancelScrollCompletionChecks({ scheduledWork }) { ++function cancelScrollCompletionChecks(state) { ++ const { scheduledWork } = state; ++ state.silentInitialScrollReturn = void 0; + scheduledWork.cancel("checkFinishedScrollFrame"); + scheduledWork.cancel("checkFinishedScrollRetryFrame"); + scheduledWork.cancel("checkFinishedScrollFallback"); +@@ -2439,11 +2441,12 @@ function doMaintainScrollAtEnd(ctx, options) { + + // src/core/finishScrollTo.ts + function finishScrollTo(ctx) { +- var _a3, _b, _c; ++ var _a3, _b, _c, _d; + const state = ctx.state; + if (state == null ? void 0 : state.scrollingTo) { ++ (_a3 = state.silentInitialScrollReturn) == null ? void 0 : _a3.call(state); + cancelScrollCompletionChecks(state); +- const resolvePendingScroll = ((_a3 = ctx.scrollRequestTracker) == null ? void 0 : _a3.isWaitingToRun()) ? void 0 : state.pendingScrollResolve; ++ const resolvePendingScroll = ((_b = ctx.scrollRequestTracker) == null ? void 0 : _b.isWaitingToRun()) ? void 0 : state.pendingScrollResolve; + if (resolvePendingScroll) { + state.pendingScrollResolve = void 0; + } +@@ -2466,8 +2469,8 @@ function finishScrollTo(ctx) { + } + }; + if (scrollingTo.isInitialScroll || state.initialScroll) { +- const isOffsetSession = ((_b = state.initialScrollSession) == null ? void 0 : _b.kind) === "offset"; +- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_c = state.initialScroll) == null ? void 0 : _c.viewPosition) === 1; ++ const isOffsetSession = ((_c = state.initialScrollSession) == null ? void 0 : _c.kind) === "offset"; ++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_d = state.initialScroll) == null ? void 0 : _d.viewPosition) === 1; + finishInitialScroll(ctx, { + onFinished: complete, + preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, +@@ -2615,10 +2618,13 @@ function checkFinishedScrollFallback(ctx) { + const jiggleOffset = targetOffset >= SILENT_INITIAL_SCROLL_TARGET_EPSILON ? targetOffset - SILENT_INITIAL_SCROLL_TARGET_EPSILON : targetOffset + SILENT_INITIAL_SCROLL_TARGET_EPSILON; + initialScrollCompletion.markSilentInitialScrollRetry(state); + scrollToFallbackOffset(ctx, jiggleOffset); +- state.scheduledWork.frame( +- () => scrollToFallbackOffset(ctx, targetOffset), +- "checkFinishedScrollRetryFrame" +- ); ++ const returnToTarget = () => { ++ var _a4, _b2; ++ state.silentInitialScrollReturn = void 0; ++ scrollToFallbackOffset(ctx, (_b2 = (_a4 = initialScrollWatchdog.get(state)) == null ? void 0 : _a4.targetOffset) != null ? _b2 : targetOffset); ++ }; ++ state.silentInitialScrollReturn = returnToTarget; ++ state.scheduledWork.frame(returnToTarget, "checkFinishedScrollRetryFrame"); + scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); + } else if (shouldRetryUnalignedEndScroll) { + scrollToFallbackOffset(ctx, completionState.clampedTargetOffset, !!isStillScrollingTo.animated); +diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js +index 10dd9b9c9bb..b6f2da18e38 100644 +--- a/node_modules/@legendapp/list/react-native.web.js ++++ b/node_modules/@legendapp/list/react-native.web.js +@@ -459,7 +459,9 @@ function calculateOffsetForIndex(ctx, index) { + } + + // src/core/cancelImperativeScroll.ts +-function cancelScrollCompletionChecks({ scheduledWork }) { ++function cancelScrollCompletionChecks(state) { ++ const { scheduledWork } = state; ++ state.silentInitialScrollReturn = void 0; + scheduledWork.cancel("checkFinishedScrollFrame"); + scheduledWork.cancel("checkFinishedScrollRetryFrame"); + scheduledWork.cancel("checkFinishedScrollFallback"); +@@ -2342,11 +2344,12 @@ function doMaintainScrollAtEnd(ctx, options) { + + // src/core/finishScrollTo.ts + function finishScrollTo(ctx) { +- var _a3, _b, _c; ++ var _a3, _b, _c, _d; + const state = ctx.state; + if (state == null ? void 0 : state.scrollingTo) { ++ (_a3 = state.silentInitialScrollReturn) == null ? void 0 : _a3.call(state); + cancelScrollCompletionChecks(state); +- const resolvePendingScroll = ((_a3 = ctx.scrollRequestTracker) == null ? void 0 : _a3.isWaitingToRun()) ? void 0 : state.pendingScrollResolve; ++ const resolvePendingScroll = ((_b = ctx.scrollRequestTracker) == null ? void 0 : _b.isWaitingToRun()) ? void 0 : state.pendingScrollResolve; + if (resolvePendingScroll) { + state.pendingScrollResolve = void 0; + } +@@ -2369,8 +2372,8 @@ function finishScrollTo(ctx) { + } + }; + if (scrollingTo.isInitialScroll || state.initialScroll) { +- const isOffsetSession = ((_b = state.initialScrollSession) == null ? void 0 : _b.kind) === "offset"; +- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_c = state.initialScroll) == null ? void 0 : _c.viewPosition) === 1; ++ const isOffsetSession = ((_c = state.initialScrollSession) == null ? void 0 : _c.kind) === "offset"; ++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_d = state.initialScroll) == null ? void 0 : _d.viewPosition) === 1; + finishInitialScroll(ctx, { + onFinished: complete, + preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, +diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs +index 0ba06dac531..7413734a88e 100644 +--- a/node_modules/@legendapp/list/react-native.web.mjs ++++ b/node_modules/@legendapp/list/react-native.web.mjs +@@ -438,7 +438,9 @@ function calculateOffsetForIndex(ctx, index) { + } + + // src/core/cancelImperativeScroll.ts +-function cancelScrollCompletionChecks({ scheduledWork }) { ++function cancelScrollCompletionChecks(state) { ++ const { scheduledWork } = state; ++ state.silentInitialScrollReturn = void 0; + scheduledWork.cancel("checkFinishedScrollFrame"); + scheduledWork.cancel("checkFinishedScrollRetryFrame"); + scheduledWork.cancel("checkFinishedScrollFallback"); +@@ -2321,11 +2323,12 @@ function doMaintainScrollAtEnd(ctx, options) { + + // src/core/finishScrollTo.ts + function finishScrollTo(ctx) { +- var _a3, _b, _c; ++ var _a3, _b, _c, _d; + const state = ctx.state; + if (state == null ? void 0 : state.scrollingTo) { ++ (_a3 = state.silentInitialScrollReturn) == null ? void 0 : _a3.call(state); + cancelScrollCompletionChecks(state); +- const resolvePendingScroll = ((_a3 = ctx.scrollRequestTracker) == null ? void 0 : _a3.isWaitingToRun()) ? void 0 : state.pendingScrollResolve; ++ const resolvePendingScroll = ((_b = ctx.scrollRequestTracker) == null ? void 0 : _b.isWaitingToRun()) ? void 0 : state.pendingScrollResolve; + if (resolvePendingScroll) { + state.pendingScrollResolve = void 0; + } +@@ -2348,8 +2351,8 @@ function finishScrollTo(ctx) { + } + }; + if (scrollingTo.isInitialScroll || state.initialScroll) { +- const isOffsetSession = ((_b = state.initialScrollSession) == null ? void 0 : _b.kind) === "offset"; +- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_c = state.initialScroll) == null ? void 0 : _c.viewPosition) === 1; ++ const isOffsetSession = ((_c = state.initialScrollSession) == null ? void 0 : _c.kind) === "offset"; ++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_d = state.initialScroll) == null ? void 0 : _d.viewPosition) === 1; + finishInitialScroll(ctx, { + onFinished: complete, + preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, +diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js +index 10dd9b9c9bb..b6f2da18e38 100644 +--- a/node_modules/@legendapp/list/react.js ++++ b/node_modules/@legendapp/list/react.js +@@ -459,7 +459,9 @@ function calculateOffsetForIndex(ctx, index) { + } + + // src/core/cancelImperativeScroll.ts +-function cancelScrollCompletionChecks({ scheduledWork }) { ++function cancelScrollCompletionChecks(state) { ++ const { scheduledWork } = state; ++ state.silentInitialScrollReturn = void 0; + scheduledWork.cancel("checkFinishedScrollFrame"); + scheduledWork.cancel("checkFinishedScrollRetryFrame"); + scheduledWork.cancel("checkFinishedScrollFallback"); +@@ -2342,11 +2344,12 @@ function doMaintainScrollAtEnd(ctx, options) { + + // src/core/finishScrollTo.ts + function finishScrollTo(ctx) { +- var _a3, _b, _c; ++ var _a3, _b, _c, _d; + const state = ctx.state; + if (state == null ? void 0 : state.scrollingTo) { ++ (_a3 = state.silentInitialScrollReturn) == null ? void 0 : _a3.call(state); + cancelScrollCompletionChecks(state); +- const resolvePendingScroll = ((_a3 = ctx.scrollRequestTracker) == null ? void 0 : _a3.isWaitingToRun()) ? void 0 : state.pendingScrollResolve; ++ const resolvePendingScroll = ((_b = ctx.scrollRequestTracker) == null ? void 0 : _b.isWaitingToRun()) ? void 0 : state.pendingScrollResolve; + if (resolvePendingScroll) { + state.pendingScrollResolve = void 0; + } +@@ -2369,8 +2372,8 @@ function finishScrollTo(ctx) { + } + }; + if (scrollingTo.isInitialScroll || state.initialScroll) { +- const isOffsetSession = ((_b = state.initialScrollSession) == null ? void 0 : _b.kind) === "offset"; +- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_c = state.initialScroll) == null ? void 0 : _c.viewPosition) === 1; ++ const isOffsetSession = ((_c = state.initialScrollSession) == null ? void 0 : _c.kind) === "offset"; ++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_d = state.initialScroll) == null ? void 0 : _d.viewPosition) === 1; + finishInitialScroll(ctx, { + onFinished: complete, + preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, +diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs +index 0ba06dac531..7413734a88e 100644 +--- a/node_modules/@legendapp/list/react.mjs ++++ b/node_modules/@legendapp/list/react.mjs +@@ -438,7 +438,9 @@ function calculateOffsetForIndex(ctx, index) { + } + + // src/core/cancelImperativeScroll.ts +-function cancelScrollCompletionChecks({ scheduledWork }) { ++function cancelScrollCompletionChecks(state) { ++ const { scheduledWork } = state; ++ state.silentInitialScrollReturn = void 0; + scheduledWork.cancel("checkFinishedScrollFrame"); + scheduledWork.cancel("checkFinishedScrollRetryFrame"); + scheduledWork.cancel("checkFinishedScrollFallback"); +@@ -2321,11 +2323,12 @@ function doMaintainScrollAtEnd(ctx, options) { + + // src/core/finishScrollTo.ts + function finishScrollTo(ctx) { +- var _a3, _b, _c; ++ var _a3, _b, _c, _d; + const state = ctx.state; + if (state == null ? void 0 : state.scrollingTo) { ++ (_a3 = state.silentInitialScrollReturn) == null ? void 0 : _a3.call(state); + cancelScrollCompletionChecks(state); +- const resolvePendingScroll = ((_a3 = ctx.scrollRequestTracker) == null ? void 0 : _a3.isWaitingToRun()) ? void 0 : state.pendingScrollResolve; ++ const resolvePendingScroll = ((_b = ctx.scrollRequestTracker) == null ? void 0 : _b.isWaitingToRun()) ? void 0 : state.pendingScrollResolve; + if (resolvePendingScroll) { + state.pendingScrollResolve = void 0; + } +@@ -2348,8 +2351,8 @@ function finishScrollTo(ctx) { + } + }; + if (scrollingTo.isInitialScroll || state.initialScroll) { +- const isOffsetSession = ((_b = state.initialScrollSession) == null ? void 0 : _b.kind) === "offset"; +- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_c = state.initialScroll) == null ? void 0 : _c.viewPosition) === 1; ++ const isOffsetSession = ((_c = state.initialScrollSession) == null ? void 0 : _c.kind) === "offset"; ++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_d = state.initialScroll) == null ? void 0 : _d.viewPosition) === 1; + finishInitialScroll(ctx, { + onFinished: complete, + preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget, diff --git a/patches/@legendapp/list/@legendapp+list+3.7.2+006+keep-native-initial-scroll-on-target.patch b/patches/@legendapp/list/@legendapp+list+3.7.2+006+keep-native-initial-scroll-on-target.patch deleted file mode 100644 index 2bc501906ab1..000000000000 --- a/patches/@legendapp/list/@legendapp+list+3.7.2+006+keep-native-initial-scroll-on-target.patch +++ /dev/null @@ -1,228 +0,0 @@ -diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js ---- a/node_modules/@legendapp/list/react-native.js -+++ b/node_modules/@legendapp/list/react-native.js -@@ -518,7 +518,9 @@ - var ENABLE_DEBUG_VIEW = IS_DEV && false; - - // src/core/cancelImperativeScroll.ts --function cancelScrollCompletionChecks({ scheduledWork }) { -+function cancelScrollCompletionChecks(state) { -+ const { scheduledWork } = state; -+ state.silentInitialScrollReturn = void 0; - scheduledWork.cancel("checkFinishedScrollFrame"); - scheduledWork.cancel("checkFinishedScrollRetryFrame"); - scheduledWork.cancel("checkFinishedScrollFallback"); -@@ -1051,6 +1053,20 @@ - } - - // src/utils/requestAdjust.ts -+function shiftNativeInitialScrollTarget(state, positionDiff) { -+ const scrollingTo = state.scrollingTo; -+ if (Platform.OS === "web" || !(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) || scrollingTo.animated) { -+ return; -+ } -+ scrollingTo.offset += positionDiff; -+ if (scrollingTo.targetOffset !== void 0) { -+ scrollingTo.targetOffset += positionDiff; -+ } -+ const watchdog = initialScrollWatchdog.get(state); -+ if (watchdog) { -+ watchdog.targetOffset += positionDiff; -+ } -+} - function requestAdjust(ctx, positionDiff, source) { - var _a3; - const state = ctx.state; -@@ -1070,6 +1086,7 @@ - state.userScrollContentSize += positionDiff; - } - state.scrollForNextCalculateItemsInView = void 0; -+ shiftNativeInitialScrollTarget(state, positionDiff); - const readyToRender = peek$(ctx, "readyToRender"); - if (readyToRender) { - doit(); -@@ -1101,8 +1118,11 @@ - } - } else { - state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; -+ const nativeScrollEpoch = state.nativeScrollEpoch; - requestAnimationFrame(() => { -- doit(); -+ if (state.nativeScrollEpoch === nativeScrollEpoch) { -+ doit(); -+ } - state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 1) - 1; - if (!state.adjustingFromInitialMount) { - setInitialRenderState(ctx, {}); -@@ -2435,9 +2455,10 @@ - - // src/core/finishScrollTo.ts - function finishScrollTo(ctx) { -- var _a3, _b, _c; -+ var _a3, _b, _c, _d; - const state = ctx.state; - if (state == null ? void 0 : state.scrollingTo) { -+ (_d = state.silentInitialScrollReturn) == null ? void 0 : _d.call(state); - cancelScrollCompletionChecks(state); - const resolvePendingScroll = ((_a3 = ctx.scrollRequestTracker) == null ? void 0 : _a3.isWaitingToRun()) ? void 0 : state.pendingScrollResolve; - if (resolvePendingScroll) { -@@ -2558,7 +2579,10 @@ - } - } - function scrollToFallbackOffset(ctx, offset, animated = false) { -- var _a3; -+ var _a3, _b; -+ if (Platform.OS !== "web") { -+ ctx.state.nativeScrollEpoch = ((_b = ctx.state.nativeScrollEpoch) != null ? _b : 0) + 1; -+ } - (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({ animated, ...getNativeScrollPoint(ctx, offset) }); - } - function checkFinishedScrollFallback(ctx) { -@@ -2608,10 +2632,13 @@ - const jiggleOffset = targetOffset >= SILENT_INITIAL_SCROLL_TARGET_EPSILON ? targetOffset - SILENT_INITIAL_SCROLL_TARGET_EPSILON : targetOffset + SILENT_INITIAL_SCROLL_TARGET_EPSILON; - initialScrollCompletion.markSilentInitialScrollRetry(state); - scrollToFallbackOffset(ctx, jiggleOffset); -- state.scheduledWork.frame( -- () => scrollToFallbackOffset(ctx, targetOffset), -- "checkFinishedScrollRetryFrame" -- ); -+ const returnToTarget = () => { -+ var _a4, _b2; -+ state.silentInitialScrollReturn = void 0; -+ scrollToFallbackOffset(ctx, (_b2 = (_a4 = initialScrollWatchdog.get(state)) == null ? void 0 : _a4.targetOffset) != null ? _b2 : targetOffset); -+ }; -+ state.silentInitialScrollReturn = returnToTarget; -+ state.scheduledWork.frame(returnToTarget, "checkFinishedScrollRetryFrame"); - scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); - } else if (shouldRetryUnalignedEndScroll) { - scrollToFallbackOffset(ctx, completionState.clampedTargetOffset, !!isStillScrollingTo.animated); -@@ -2640,6 +2667,7 @@ - - // src/core/doScrollTo.native.ts - function doScrollTo(ctx, params) { -+ var _nativeScrollEpoch; - const state = ctx.state; - const { animated, horizontal, isInitialScroll, offset } = params; - const isAnimated = !!animated; -@@ -2650,6 +2678,7 @@ - } - if (!params.isAdjustment) { - state.userScrollContentSize = void 0; -+ state.nativeScrollEpoch = ((_nativeScrollEpoch = state.nativeScrollEpoch) != null ? _nativeScrollEpoch : 0) + 1; - } - scroller.scrollTo({ animated: isAnimated, ...getNativeScrollPoint(ctx, offset, horizontal) }); - if (isInitialScroll) { -diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs ---- a/node_modules/@legendapp/list/react-native.mjs -+++ b/node_modules/@legendapp/list/react-native.mjs -@@ -497,7 +497,9 @@ - var ENABLE_DEBUG_VIEW = IS_DEV && false; - - // src/core/cancelImperativeScroll.ts --function cancelScrollCompletionChecks({ scheduledWork }) { -+function cancelScrollCompletionChecks(state) { -+ const { scheduledWork } = state; -+ state.silentInitialScrollReturn = void 0; - scheduledWork.cancel("checkFinishedScrollFrame"); - scheduledWork.cancel("checkFinishedScrollRetryFrame"); - scheduledWork.cancel("checkFinishedScrollFallback"); -@@ -1030,6 +1032,20 @@ - } - - // src/utils/requestAdjust.ts -+function shiftNativeInitialScrollTarget(state, positionDiff) { -+ const scrollingTo = state.scrollingTo; -+ if (Platform.OS === "web" || !(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) || scrollingTo.animated) { -+ return; -+ } -+ scrollingTo.offset += positionDiff; -+ if (scrollingTo.targetOffset !== void 0) { -+ scrollingTo.targetOffset += positionDiff; -+ } -+ const watchdog = initialScrollWatchdog.get(state); -+ if (watchdog) { -+ watchdog.targetOffset += positionDiff; -+ } -+} - function requestAdjust(ctx, positionDiff, source) { - var _a3; - const state = ctx.state; -@@ -1049,6 +1065,7 @@ - state.userScrollContentSize += positionDiff; - } - state.scrollForNextCalculateItemsInView = void 0; -+ shiftNativeInitialScrollTarget(state, positionDiff); - const readyToRender = peek$(ctx, "readyToRender"); - if (readyToRender) { - doit(); -@@ -1080,8 +1097,11 @@ - } - } else { - state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1; -+ const nativeScrollEpoch = state.nativeScrollEpoch; - requestAnimationFrame(() => { -- doit(); -+ if (state.nativeScrollEpoch === nativeScrollEpoch) { -+ doit(); -+ } - state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 1) - 1; - if (!state.adjustingFromInitialMount) { - setInitialRenderState(ctx, {}); -@@ -2414,9 +2434,10 @@ - - // src/core/finishScrollTo.ts - function finishScrollTo(ctx) { -- var _a3, _b, _c; -+ var _a3, _b, _c, _d; - const state = ctx.state; - if (state == null ? void 0 : state.scrollingTo) { -+ (_d = state.silentInitialScrollReturn) == null ? void 0 : _d.call(state); - cancelScrollCompletionChecks(state); - const resolvePendingScroll = ((_a3 = ctx.scrollRequestTracker) == null ? void 0 : _a3.isWaitingToRun()) ? void 0 : state.pendingScrollResolve; - if (resolvePendingScroll) { -@@ -2537,7 +2558,10 @@ - } - } - function scrollToFallbackOffset(ctx, offset, animated = false) { -- var _a3; -+ var _a3, _b; -+ if (Platform.OS !== "web") { -+ ctx.state.nativeScrollEpoch = ((_b = ctx.state.nativeScrollEpoch) != null ? _b : 0) + 1; -+ } - (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({ animated, ...getNativeScrollPoint(ctx, offset) }); - } - function checkFinishedScrollFallback(ctx) { -@@ -2587,10 +2611,13 @@ - const jiggleOffset = targetOffset >= SILENT_INITIAL_SCROLL_TARGET_EPSILON ? targetOffset - SILENT_INITIAL_SCROLL_TARGET_EPSILON : targetOffset + SILENT_INITIAL_SCROLL_TARGET_EPSILON; - initialScrollCompletion.markSilentInitialScrollRetry(state); - scrollToFallbackOffset(ctx, jiggleOffset); -- state.scheduledWork.frame( -- () => scrollToFallbackOffset(ctx, targetOffset), -- "checkFinishedScrollRetryFrame" -- ); -+ const returnToTarget = () => { -+ var _a4, _b2; -+ state.silentInitialScrollReturn = void 0; -+ scrollToFallbackOffset(ctx, (_b2 = (_a4 = initialScrollWatchdog.get(state)) == null ? void 0 : _a4.targetOffset) != null ? _b2 : targetOffset); -+ }; -+ state.silentInitialScrollReturn = returnToTarget; -+ state.scheduledWork.frame(returnToTarget, "checkFinishedScrollRetryFrame"); - scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS); - } else if (shouldRetryUnalignedEndScroll) { - scrollToFallbackOffset(ctx, completionState.clampedTargetOffset, !!isStillScrollingTo.animated); -@@ -2619,6 +2646,7 @@ - - // src/core/doScrollTo.native.ts - function doScrollTo(ctx, params) { -+ var _nativeScrollEpoch; - const state = ctx.state; - const { animated, horizontal, isInitialScroll, offset } = params; - const isAnimated = !!animated; -@@ -2629,6 +2657,7 @@ - } - if (!params.isAdjustment) { - state.userScrollContentSize = void 0; -+ state.nativeScrollEpoch = ((_nativeScrollEpoch = state.nativeScrollEpoch) != null ? _nativeScrollEpoch : 0) + 1; - } - scroller.scrollTo({ animated: isAnimated, ...getNativeScrollPoint(ctx, offset, horizontal) }); - if (isInitialScroll) { diff --git a/patches/@legendapp/list/details.md b/patches/@legendapp/list/details.md index b3446ceaf904..551f199e10b3 100644 --- a/patches/@legendapp/list/details.md +++ b/patches/@legendapp/list/details.md @@ -1,55 +1,57 @@ # `@legendapp/list` patches +Each patch is the build output of its upstream PR, so it changes exactly what that PR changes. Remove a patch once a LegendList release contains its PR. + ### [@legendapp+list+3.7.2+001+apply-pending-anchor-correction.patch](@legendapp+list+3.7.2+001+apply-pending-anchor-correction.patch) - Reason: On web, LegendList holds a scroll anchor for a moment after a data change and defers the scroll correction for item size changes to the next frame. Any other pass that repositions items before that frame, such as the one a scroll event triggers, moves the visible messages without the correction. They jump for a frame and only partly return. With this patch, the first pass that repositions items applies the pending correction. -- Scope: Only the web bundles, and only while a deferred item-size correction is pending. +- Scope: All bundles. Only web defers the correction, so native lists are unaffected. - Upstream PR/issue: https://github.com/LegendApp/legend-list/pull/596 - E/App issue: https://github.com/Expensify/App/pull/100733#issuecomment-6411009930 - PR introducing patch: https://github.com/Expensify/App/pull/100733 - Reproduction: On web, open a chat with more history than the first page. Scroll to the top until older messages load. The message at the top of the screen should stay in place instead of jumping and snapping back. -### [@legendapp+list+3.7.2+002+anchor-end-aligned-lists-to-last-visible-item.patch](@legendapp+list+3.7.2+002+anchor-end-aligned-lists-to-last-visible-item.patch) +### [@legendapp+list+3.7.2+002+add-mvcp-end-anchor.patch](@legendapp+list+3.7.2+002+add-mvcp-end-anchor.patch) -- Reason: When items change size or data changes, LegendList keeps the first visible item in place. In a chat, the oldest loaded message shows the sender's avatar, and it loses that header once older messages from the same sender load. Its height shrinks, so every message below it moves up. An inverted list keeps the bottom in place instead. For lists with `alignItemsAtEnd`, this patch anchors the last visible item, so a size change above it only moves the content above. -- Scope: Only the web bundles, and only lists with `alignItemsAtEnd`. Other lists still anchor the first visible item. +- Reason: When items change size or data changes, LegendList keeps the first visible item in place. In a chat, the oldest loaded message shows the sender's avatar, and it loses that header once older messages from the same sender load. Its height shrinks, so every message below it moves up. An inverted list keeps the bottom in place instead. This patch adds `anchor: "end"` to `maintainVisibleContentPosition`, which keeps the last visible item in place, so a size change above it only moves the content above. +- Scope: All bundles and type definitions, and only lists that pass `anchor: "end"`. Other lists still keep the first visible item in place. - Upstream PR/issue: https://github.com/LegendApp/legend-list/pull/597 - E/App issue: 🛑 - PR introducing patch: https://github.com/Expensify/App/pull/100733 -- Reproduction: On web, clear the cache and open a chat whose oldest loaded message shows the sender's avatar. Scroll to that message and wait until older messages from the same sender load. The avatar line should disappear without moving the messages below it. +- Reproduction: Clear the cache and open a chat whose oldest loaded message shows the sender's avatar. Scroll to that message and wait until older messages from the same sender load. The avatar line should disappear without moving the messages below it. ### [@legendapp+list+3.7.2+003+anchor-data-changes-after-scroll-jumps.patch](@legendapp+list+3.7.2+003+anchor-data-changes-after-scroll-jumps.patch) -- Reason: After a large scroll jump, such as a fast scrollbar drag, LegendList repositions newly measured items without keeping anything in place, so the region the user jumped to can lay out. This lasts until it has measured every item it rendered since the jump. If older messages load in the meantime, their measurements push the visible messages down by a few hundred pixels for a frame. This patch ends that state when the data changes, so the new messages are anchored like any other data change. -- Scope: Only the web bundles, and only lists that keep their visible content in place on data changes. +- Reason: After a large scroll jump, such as a fast scrollbar drag or fling, LegendList repositions newly measured items without keeping anything in place, so the region the user jumped to can lay out. This lasts until it has measured every item it rendered since the jump. If older messages load in the meantime, their measurements push the visible messages down by a few hundred pixels for a frame. This patch ends that state when the data changes, so the new messages are anchored like any other data change. +- Scope: All bundles, and only lists that keep their visible content in place on data changes. - Upstream PR/issue: https://github.com/LegendApp/legend-list/pull/598 - E/App issue: 🛑 - PR introducing patch: https://github.com/Expensify/App/pull/100733 - Reproduction: On web, clear the cache and open a chat with a long history. Drag the scrollbar thumb quickly up and down a few times, then hold it near the top until older messages load. The messages on screen should not move. -### [@legendapp+list+3.7.2+004+reveal-list-after-initial-scroll-adjustments.patch](@legendapp+list+3.7.2+004+reveal-list-after-initial-scroll-adjustments.patch) +### [@legendapp+list+3.7.2+004+land-initial-mount-adjustments-once.patch](@legendapp+list+3.7.2+004+land-initial-mount-adjustments-once.patch) -- Reason: Before a list is ready, LegendList moves its items as soon as they are measured but applies the matching native scroll adjustment one frame later. It could mark the list ready, and show it, before that frame ran. On iOS, a chat that opens at a linked message then shows the message at the top of the list and moves it down by about 45 points. With this patch, the list becomes ready only after those adjustments have run. -- Scope: All bundles, and only during a list's initial layout while a scroll adjustment requested before the list was ready is still pending. +- Reason: Before a list is ready, LegendList moves its items as soon as they are measured but applies the matching native scroll adjustment one frame later. It could mark the list ready, and show it, before that frame ran. An initial scroll dispatched in between also already lands on the adjusted offset, so the late adjustment moved the list a second time, and LegendList's retries reissued the offset recorded when the scroll started. A chat that opens at a linked message then shows the message at the top of the list and moves it down. With this patch, the list becomes ready only after those adjustments have run, skips an adjustment that a later native scroll already includes, and moves the target of the initial scroll with each adjustment. +- Scope: All bundles, and only during a list's initial layout. Skipping adjustments and moving the target only apply on native. - Upstream PR/issue: https://github.com/LegendApp/legend-list/pull/600 - E/App issue: 🛑 - PR introducing patch: https://github.com/Expensify/App/pull/100733 -- Reproduction: On iOS, open a chat from an LHN row with a Pay or Approve action, so it opens at the linked expense. The highlighted expense should appear once, a little below the top of the list, and not move afterward. +- Reproduction: On iOS or Android, clear the cache and open a chat from an LHN row with a Pay, Approve, or Fix action, so it opens at the linked expense. The highlighted expense should appear once, a little below the top of the list, and not move afterward. ### [@legendapp+list+3.7.2+005+keep-scroll-adjust-anchor-precise.patch](@legendapp+list+3.7.2+005+keep-scroll-adjust-anchor-precise.patch) -- Reason: LegendList keeps the visible items in place with a hidden anchor view placed 10,000,000 points down. Native layout stores positions as 32-bit floats, which hold that value only to the nearest whole point, so each scroll adjustment was rounded while the items moved by the exact amount. On Android, a chat that opens at a linked message moved up by 1–2 px when the rows above it were measured. This patch moves the anchor to 1,000,000 points, where its position stays precise to 1/16 of a point. +- Reason: LegendList keeps the visible items in place with a hidden anchor view placed 10,000,000 points down. Native layout stores positions as 32-bit floats, which hold that value only to the nearest whole point, so each scroll adjustment was rounded while the items moved by the exact amount. On Android, a chat that opens at a linked message moved up by 1 or 2 px when the rows above it were measured. This patch moves the anchor to 1,000,000 points, where its position stays precise to 1/16 of a point. Adjustments move the anchor along with the content, so the list can still scroll through 1,000,000 points of content that loaded without one, such as the first page and new messages. - Scope: Only the native bundles. - Upstream PR/issue: https://github.com/LegendApp/legend-list/pull/602 - E/App issue: 🛑 - PR introducing patch: https://github.com/Expensify/App/pull/100733 - Reproduction: On Android, open a chat from an LHN row with a Fix action, so it opens at the linked expense. When the Latest messages button appears, the messages on screen should not move. -### [@legendapp+list+3.7.2+006+keep-native-initial-scroll-on-target.patch](@legendapp+list+3.7.2+006+keep-native-initial-scroll-on-target.patch) +### [@legendapp+list+3.7.2+006+finish-silent-initial-scroll-retries.patch](@legendapp+list+3.7.2+006+finish-silent-initial-scroll-retries.patch) -- Reason: Before a list is ready, LegendList moves its item positions right away but moves its native scroll anchor a frame later. An initial scroll dispatched in between already lands on the adjusted offset, so the late anchor move shifted the list again, and LegendList's retries then reissued the offset recorded when the scroll started. A retry for scrolls Android reports silently also left the list one point off when the scroll finished first. On Android, a chat that opens at a linked message appeared with the message at the top of the list and then moved down to its offset. This patch skips anchor moves that a later native scroll already includes, moves the pending initial-scroll target with each adjustment, and completes the retry before the scroll finishes. -- Scope: Only the native bundles, and only while an initial scroll is in flight or the list is not ready yet. -- Upstream PR/issue: https://github.com/LegendApp/legend-list/pull/604 +- Reason: When Android dispatches an initial scroll without reporting a scroll event, LegendList retries it by moving the list one point away and back on the next frame. If another check finished the scroll first, the list stayed one point off its target, and the return used the offset recorded before adjustments moved the target. With this patch, finishing the scroll completes a pending return, and the return goes to the current target. +- Scope: All bundles, and only initial scrolls that LegendList retries because they reported no scroll event. That only happens on native. +- Upstream PR/issue: https://github.com/LegendApp/legend-list/pull/604, which builds on https://github.com/LegendApp/legend-list/pull/600 - E/App issue: 🛑 - PR introducing patch: https://github.com/Expensify/App/pull/100733 -- Reproduction: On Android, clear the cache and open a chat from an LHN row with a Fix action, so it opens at the linked expense. The highlighted expense should appear once, a little below the top of the list, and not move afterward. +- Reproduction: On Android, clear the cache and open a chat from an LHN row with a Fix action, so it opens at the linked expense. The highlighted expense should come to rest a little below the top of the list and stay there. From 16a575a88b475eed274fdeb05bee0d1202d4e9e3 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 11 Nov 2026 13:20:06 +0100 Subject: [PATCH 2/3] docs: note LegendList's new anchor offset in the Android MVCP patch notes LegendList patch 005 places its scroll anchor at 1,000,000 points instead of 10,000,000. Co-Authored-By: Claude Opus 5.5 --- patches/react-native/details.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches/react-native/details.md b/patches/react-native/details.md index daec695c5aa5..29d5fa7d8346 100644 --- a/patches/react-native/details.md +++ b/patches/react-native/details.md @@ -12,7 +12,7 @@ - Reason: Fixes content jumping issues with `MaintainVisibleContentPosition` on Android, particularly in bidirectional pagination scenarios. The patch: 1. Changes when the first visible view is calculated - now happens on scroll events instead of during Fabric's willMountItems lifecycle, which was causing incorrect updates 2. Improves first visible view selection logic to handle Fabric's z-index-based view reordering by finding the view with the smallest position that's still greater than the scroll position - 3. Preserves a positioned, zero-sized first child as a scroll anchor. LegendList moves this anchor to compensate for item measurements. Selecting its surrounding container or rejecting its empty frame prevents native scroll compensation and makes the chat jump as estimated rows shrink. LegendList 3.7 places the anchor at its adjustment offset plus 10,000,000 pixels, beyond the viewport. The strict visibility check also prevents a fixed, zero-sized spacer at or above the scroll offset in another scroll view from taking over its visible-content anchor. + 3. Preserves a positioned, zero-sized first child as a scroll anchor. LegendList moves this anchor to compensate for item measurements. Selecting its surrounding container or rejecting its empty frame prevents native scroll compensation and makes the chat jump as estimated rows shrink. LegendList places the anchor at its adjustment offset plus 1,000,000 points, beyond the viewport (10,000,000 before `@legendapp/list` patch 005). The strict visibility check also prevents a fixed, zero-sized spacer at or above the scroll offset in another scroll view from taking over its visible-content anchor. - Scope: This changes Android `MaintainVisibleContentPosition` for every React Native scroll view that enables it, not only the report-action list. Keep this in mind when updating React Native or investigating anchoring in other scroll views. - Upstream PR/issue: https://github.com/react/react-native/pull/57959 merged on 2026-10-28 as [9bc1a28a9623419d62eb350acd0c172644e2c022](https://github.com/react/react-native/commit/9bc1a28a9623419d62eb350acd0c172644e2c022). It ports the original Android MVCP proposal and includes z-index anchor selection and support for LegendList's positioned, zero-sized anchor. - Expected release: React Native 0.89.0, [currently scheduled for 2026-12-07](https://reactnative.dev/releases/overview), unless cherry-picked into an earlier release. The fix landed after the 0.88 branch cut and before the scheduled 0.89 branch cut on 2026-11-03. From 92b6fbe97cbdafa888d0ca5a32b85c8466edd3f7 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Wed, 11 Nov 2026 13:21:14 +0100 Subject: [PATCH 3/3] fix: keep native list content in place when a LegendList header resizes LegendList only keeps the visible items in place for header size changes on web. The chat migration shows a loading placeholder as the list header while offline, and when it disappeared on reconnect, the messages on screen moved by its height on iOS and Android. This patch is the build output of LegendApp/legend-list#607, which applies the header adjustment on every platform and reuses LegendList's Android clamp handling for it. Co-Authored-By: Claude Opus 5.5 --- ...ve-content-in-place-on-header-resize.patch | 316 ++++++++++++++++++ patches/@legendapp/list/details.md | 9 + 2 files changed, 325 insertions(+) create mode 100644 patches/@legendapp/list/@legendapp+list+3.7.2+007+keep-native-content-in-place-on-header-resize.patch diff --git a/patches/@legendapp/list/@legendapp+list+3.7.2+007+keep-native-content-in-place-on-header-resize.patch b/patches/@legendapp/list/@legendapp+list+3.7.2+007+keep-native-content-in-place-on-header-resize.patch new file mode 100644 index 000000000000..db08e34de5ed --- /dev/null +++ b/patches/@legendapp/list/@legendapp+list+3.7.2+007+keep-native-content-in-place-on-header-resize.patch @@ -0,0 +1,316 @@ +diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js +index 3ccffa79110..56382a16e9d 100644 +--- a/node_modules/@legendapp/list/react-native.js ++++ b/node_modules/@legendapp/list/react-native.js +@@ -549,6 +549,7 @@ function cancelImperativeScroll(state) { + } + + // src/core/clampScrollOffset.ts ++var ANDROID_SHRINK_CLAMP_EPSILON = 0.1; + function getContentInsetTop(ctx) { + var _a3, _b, _c, _d; + const state = ctx.state; +@@ -561,6 +562,20 @@ function getContentInsetTop(ctx) { + function getScrollRangeEnd(ctx, contentSize = getContentSize(ctx)) { + return Math.max(0 - getContentInsetTop(ctx), contentSize - ctx.state.scrollLength); + } ++function getAdjustAfterAndroidShrinkClamp(ctx, positionDiff) { ++ const state = ctx.state; ++ if (Platform.OS !== "android") { ++ return positionDiff; ++ } ++ const maxScroll = getScrollRangeEnd(ctx); ++ if (state.scroll <= maxScroll + ANDROID_SHRINK_CLAMP_EPSILON) { ++ return positionDiff; ++ } ++ const targetScroll = Math.max(0, Math.min(state.scroll + positionDiff, maxScroll)); ++ state.scroll = maxScroll; ++ state.scrollPending = maxScroll; ++ return targetScroll - maxScroll; ++} + function clampScrollOffset(ctx, offset, scrollTarget) { + var _a3; + const state = ctx.state; +@@ -1999,14 +2014,8 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { + now, + positionDiff + }); +- if (Platform.OS === "android" && !dataChanged && scrollTarget === void 0) { +- const maxScroll = getScrollRangeEnd(ctx); +- if (state.scroll > maxScroll + MVCP_POSITION_EPSILON) { +- const targetScroll = Math.max(0, Math.min(state.scroll + positionDiff, maxScroll)); +- state.scroll = maxScroll; +- state.scrollPending = maxScroll; +- positionDiff = targetScroll - maxScroll; +- } ++ if (!dataChanged && scrollTarget === void 0) { ++ positionDiff = getAdjustAfterAndroidShrinkClamp(ctx, positionDiff); + } + if (shouldQueueNativeMVCPAdjust(dataChanged, state, positionDiff, prevTotalSize, prevScroll, scrollTarget)) { + state.pendingNativeMVCPAdjust = { +@@ -6715,7 +6724,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize + const sizeDiff = nextHeaderSize - previousHeaderSize; + const leadingPadding = props.horizontal ? props.stylePaddingLeft : props.stylePaddingTop; + const previousHeaderEnd = (leadingPadding || 0) + previousHeaderSize; +- return Platform.OS === "web" && props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; ++ return props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; + } + function setHeaderSize(ctx, size) { + const { state } = ctx; +@@ -6726,7 +6735,7 @@ function setHeaderSize(ctx, size) { + set$(ctx, "headerSize", size); + updateContentMetricsState(ctx); + if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { +- requestAdjust(ctx, size - previousHeaderSize); ++ requestAdjust(ctx, getAdjustAfterAndroidShrinkClamp(ctx, size - previousHeaderSize)); + } + } + state.didMeasureHeader = true; +diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs +index 676c117a45a..2c8dc675d44 100644 +--- a/node_modules/@legendapp/list/react-native.mjs ++++ b/node_modules/@legendapp/list/react-native.mjs +@@ -528,6 +528,7 @@ function cancelImperativeScroll(state) { + } + + // src/core/clampScrollOffset.ts ++var ANDROID_SHRINK_CLAMP_EPSILON = 0.1; + function getContentInsetTop(ctx) { + var _a3, _b, _c, _d; + const state = ctx.state; +@@ -540,6 +541,20 @@ function getContentInsetTop(ctx) { + function getScrollRangeEnd(ctx, contentSize = getContentSize(ctx)) { + return Math.max(0 - getContentInsetTop(ctx), contentSize - ctx.state.scrollLength); + } ++function getAdjustAfterAndroidShrinkClamp(ctx, positionDiff) { ++ const state = ctx.state; ++ if (Platform.OS !== "android") { ++ return positionDiff; ++ } ++ const maxScroll = getScrollRangeEnd(ctx); ++ if (state.scroll <= maxScroll + ANDROID_SHRINK_CLAMP_EPSILON) { ++ return positionDiff; ++ } ++ const targetScroll = Math.max(0, Math.min(state.scroll + positionDiff, maxScroll)); ++ state.scroll = maxScroll; ++ state.scrollPending = maxScroll; ++ return targetScroll - maxScroll; ++} + function clampScrollOffset(ctx, offset, scrollTarget) { + var _a3; + const state = ctx.state; +@@ -1978,14 +1993,8 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { + now, + positionDiff + }); +- if (Platform.OS === "android" && !dataChanged && scrollTarget === void 0) { +- const maxScroll = getScrollRangeEnd(ctx); +- if (state.scroll > maxScroll + MVCP_POSITION_EPSILON) { +- const targetScroll = Math.max(0, Math.min(state.scroll + positionDiff, maxScroll)); +- state.scroll = maxScroll; +- state.scrollPending = maxScroll; +- positionDiff = targetScroll - maxScroll; +- } ++ if (!dataChanged && scrollTarget === void 0) { ++ positionDiff = getAdjustAfterAndroidShrinkClamp(ctx, positionDiff); + } + if (shouldQueueNativeMVCPAdjust(dataChanged, state, positionDiff, prevTotalSize, prevScroll, scrollTarget)) { + state.pendingNativeMVCPAdjust = { +@@ -6694,7 +6703,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize + const sizeDiff = nextHeaderSize - previousHeaderSize; + const leadingPadding = props.horizontal ? props.stylePaddingLeft : props.stylePaddingTop; + const previousHeaderEnd = (leadingPadding || 0) + previousHeaderSize; +- return Platform.OS === "web" && props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; ++ return props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON; + } + function setHeaderSize(ctx, size) { + const { state } = ctx; +@@ -6705,7 +6714,7 @@ function setHeaderSize(ctx, size) { + set$(ctx, "headerSize", size); + updateContentMetricsState(ctx); + if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { +- requestAdjust(ctx, size - previousHeaderSize); ++ requestAdjust(ctx, getAdjustAfterAndroidShrinkClamp(ctx, size - previousHeaderSize)); + } + } + state.didMeasureHeader = true; +diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js +index b6f2da18e38..684b3017523 100644 +--- a/node_modules/@legendapp/list/react-native.web.js ++++ b/node_modules/@legendapp/list/react-native.web.js +@@ -488,8 +488,6 @@ function cancelImperativeScroll(state) { + state.scrollTargetPinnedRange = void 0; + settlePendingImperativeScroll(state); + } +- +-// src/core/clampScrollOffset.ts + function getContentInsetTop(ctx) { + ctx.state; + { +@@ -499,6 +497,12 @@ function getContentInsetTop(ctx) { + function getScrollRangeEnd(ctx, contentSize = getContentSize(ctx)) { + return Math.max(0 - getContentInsetTop(ctx), contentSize - ctx.state.scrollLength); + } ++function getAdjustAfterAndroidShrinkClamp(ctx, positionDiff) { ++ ctx.state; ++ { ++ return positionDiff; ++ } ++} + function clampScrollOffset(ctx, offset, scrollTarget) { + var _a3; + const state = ctx.state; +@@ -1887,6 +1891,9 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { + now, + positionDiff + }); ++ if (!dataChanged && scrollTarget === void 0) { ++ positionDiff = getAdjustAfterAndroidShrinkClamp(ctx, positionDiff); ++ } + if (shouldQueueNativeMVCPAdjust()) { + state.pendingNativeMVCPAdjust = { + amount: positionDiff, +@@ -7587,7 +7594,7 @@ function setHeaderSize(ctx, size) { + set$(ctx, "headerSize", size); + updateContentMetricsState(ctx); + if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { +- requestAdjust(ctx, size - previousHeaderSize); ++ requestAdjust(ctx, getAdjustAfterAndroidShrinkClamp(ctx, size - previousHeaderSize)); + } + } + state.didMeasureHeader = true; +diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs +index 7413734a88e..5c99c97c4d9 100644 +--- a/node_modules/@legendapp/list/react-native.web.mjs ++++ b/node_modules/@legendapp/list/react-native.web.mjs +@@ -467,8 +467,6 @@ function cancelImperativeScroll(state) { + state.scrollTargetPinnedRange = void 0; + settlePendingImperativeScroll(state); + } +- +-// src/core/clampScrollOffset.ts + function getContentInsetTop(ctx) { + ctx.state; + { +@@ -478,6 +476,12 @@ function getContentInsetTop(ctx) { + function getScrollRangeEnd(ctx, contentSize = getContentSize(ctx)) { + return Math.max(0 - getContentInsetTop(ctx), contentSize - ctx.state.scrollLength); + } ++function getAdjustAfterAndroidShrinkClamp(ctx, positionDiff) { ++ ctx.state; ++ { ++ return positionDiff; ++ } ++} + function clampScrollOffset(ctx, offset, scrollTarget) { + var _a3; + const state = ctx.state; +@@ -1866,6 +1870,9 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { + now, + positionDiff + }); ++ if (!dataChanged && scrollTarget === void 0) { ++ positionDiff = getAdjustAfterAndroidShrinkClamp(ctx, positionDiff); ++ } + if (shouldQueueNativeMVCPAdjust()) { + state.pendingNativeMVCPAdjust = { + amount: positionDiff, +@@ -7566,7 +7573,7 @@ function setHeaderSize(ctx, size) { + set$(ctx, "headerSize", size); + updateContentMetricsState(ctx); + if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { +- requestAdjust(ctx, size - previousHeaderSize); ++ requestAdjust(ctx, getAdjustAfterAndroidShrinkClamp(ctx, size - previousHeaderSize)); + } + } + state.didMeasureHeader = true; +diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js +index b6f2da18e38..684b3017523 100644 +--- a/node_modules/@legendapp/list/react.js ++++ b/node_modules/@legendapp/list/react.js +@@ -488,8 +488,6 @@ function cancelImperativeScroll(state) { + state.scrollTargetPinnedRange = void 0; + settlePendingImperativeScroll(state); + } +- +-// src/core/clampScrollOffset.ts + function getContentInsetTop(ctx) { + ctx.state; + { +@@ -499,6 +497,12 @@ function getContentInsetTop(ctx) { + function getScrollRangeEnd(ctx, contentSize = getContentSize(ctx)) { + return Math.max(0 - getContentInsetTop(ctx), contentSize - ctx.state.scrollLength); + } ++function getAdjustAfterAndroidShrinkClamp(ctx, positionDiff) { ++ ctx.state; ++ { ++ return positionDiff; ++ } ++} + function clampScrollOffset(ctx, offset, scrollTarget) { + var _a3; + const state = ctx.state; +@@ -1887,6 +1891,9 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { + now, + positionDiff + }); ++ if (!dataChanged && scrollTarget === void 0) { ++ positionDiff = getAdjustAfterAndroidShrinkClamp(ctx, positionDiff); ++ } + if (shouldQueueNativeMVCPAdjust()) { + state.pendingNativeMVCPAdjust = { + amount: positionDiff, +@@ -7587,7 +7594,7 @@ function setHeaderSize(ctx, size) { + set$(ctx, "headerSize", size); + updateContentMetricsState(ctx); + if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { +- requestAdjust(ctx, size - previousHeaderSize); ++ requestAdjust(ctx, getAdjustAfterAndroidShrinkClamp(ctx, size - previousHeaderSize)); + } + } + state.didMeasureHeader = true; +diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs +index 7413734a88e..5c99c97c4d9 100644 +--- a/node_modules/@legendapp/list/react.mjs ++++ b/node_modules/@legendapp/list/react.mjs +@@ -467,8 +467,6 @@ function cancelImperativeScroll(state) { + state.scrollTargetPinnedRange = void 0; + settlePendingImperativeScroll(state); + } +- +-// src/core/clampScrollOffset.ts + function getContentInsetTop(ctx) { + ctx.state; + { +@@ -478,6 +476,12 @@ function getContentInsetTop(ctx) { + function getScrollRangeEnd(ctx, contentSize = getContentSize(ctx)) { + return Math.max(0 - getContentInsetTop(ctx), contentSize - ctx.state.scrollLength); + } ++function getAdjustAfterAndroidShrinkClamp(ctx, positionDiff) { ++ ctx.state; ++ { ++ return positionDiff; ++ } ++} + function clampScrollOffset(ctx, offset, scrollTarget) { + var _a3; + const state = ctx.state; +@@ -1866,6 +1870,9 @@ function prepareMVCP(ctx, dataChanged, adjustmentSource) { + now, + positionDiff + }); ++ if (!dataChanged && scrollTarget === void 0) { ++ positionDiff = getAdjustAfterAndroidShrinkClamp(ctx, positionDiff); ++ } + if (shouldQueueNativeMVCPAdjust()) { + state.pendingNativeMVCPAdjust = { + amount: positionDiff, +@@ -7566,7 +7573,7 @@ function setHeaderSize(ctx, size) { + set$(ctx, "headerSize", size); + updateContentMetricsState(ctx); + if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) { +- requestAdjust(ctx, size - previousHeaderSize); ++ requestAdjust(ctx, getAdjustAfterAndroidShrinkClamp(ctx, size - previousHeaderSize)); + } + } + state.didMeasureHeader = true; diff --git a/patches/@legendapp/list/details.md b/patches/@legendapp/list/details.md index 551f199e10b3..85637bd6e55d 100644 --- a/patches/@legendapp/list/details.md +++ b/patches/@legendapp/list/details.md @@ -55,3 +55,12 @@ Each patch is the build output of its upstream PR, so it changes exactly what th - E/App issue: 🛑 - PR introducing patch: https://github.com/Expensify/App/pull/100733 - Reproduction: On Android, clear the cache and open a chat from an LHN row with a Fix action, so it opens at the linked expense. The highlighted expense should come to rest a little below the top of the list and stay there. + +### [@legendapp+list+3.7.2+007+keep-native-content-in-place-on-header-resize.patch](@legendapp+list+3.7.2+007+keep-native-content-in-place-on-header-resize.patch) + +- Reason: LegendList keeps the visible items in place when the list header resizes above the viewport, but only on web. While offline, a chat shows a loading placeholder as the list header, and when it disappears on reconnect, the messages on screen moved by its height on iOS and Android. This patch makes header size changes above the viewport keep the visible items in place on every platform. On Android, it first accounts for the scroll offset Android clamps when the content shrinks, as LegendList already does for item size changes. +- Scope: All bundles, and only header size changes above the viewport. Web behaves as before. +- Upstream PR/issue: https://github.com/LegendApp/legend-list/pull/607 +- E/App issue: 🛑 +- PR introducing patch: https://github.com/Expensify/App/pull/100733 +- Reproduction: On iOS or Android, open a chat with more history than the first page. Turn off the network connection. Turn the network connection back on. The messages on screen should not move when the placeholder above them disappears.