Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
diff --git a/node_modules/@shopify/flash-list/dist/FlashListProps.d.ts b/node_modules/@shopify/flash-list/dist/FlashListProps.d.ts
index fa786bf..586014c 100644
--- a/node_modules/@shopify/flash-list/dist/FlashListProps.d.ts
+++ b/node_modules/@shopify/flash-list/dist/FlashListProps.d.ts
@@ -127,10 +127,12 @@ export interface FlashListProps<TItem> extends Omit<ScrollViewProps, "maintainVi
/**
* Additional configuration for initialScrollIndex.
* Use viewOffset to apply an offset to the initial scroll position as defined by initialScrollIndex.
+ * Use viewPosition to position the item within the viewport (0 = start, 0.5 = center, 1 = end), mirroring scrollToIndex.
* Ignored if initialScrollIndex is not set.
*/
initialScrollIndexParams?: {
viewOffset?: number;
+ viewPosition?: number;
} | null | undefined;
/**
* Used to extract a unique key for a given item at the specified index.
diff --git a/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerViewManager.js b/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerViewManager.js
index 3b69234..41bfb84 100644
--- a/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerViewManager.js
+++ b/node_modules/@shopify/flash-list/dist/recyclerview/RecyclerViewManager.js
@@ -294,11 +294,26 @@ export class RecyclerViewManager {
// re-estimate unmeasured items with an updated average height, changing
// the target item's position. Reading before recompute would capture a
// stale offset, causing the wrong items to be rendered.
- this.layoutManager.recomputeLayouts(0, initialScrollIndex);
+ this.layoutManager.recomputeLayouts(0, this.getDataLength() - 1);
const initialItemLayout = this.layoutManager.getLayout(initialScrollIndex);
- const initialItemOffset = this.propsRef.horizontal
+ let initialItemOffset = this.propsRef.horizontal
? initialItemLayout.x
: initialItemLayout.y;
+ // Anchor the initial render window according to initialScrollIndexParams.viewPosition so the
+ // first painted frame already shows the target item at the requested position in the viewport.
+ const initialScrollIndexParams = this.propsRef.initialScrollIndexParams;
+ const viewPosition = initialScrollIndexParams === null || initialScrollIndexParams === void 0 ? void 0 : initialScrollIndexParams.viewPosition;
+ if (viewPosition !== undefined) {
+ const windowSize = this.propsRef.horizontal
+ ? this.getWindowSize().width
+ : this.getWindowSize().height;
+ const itemSize = this.propsRef.horizontal
+ ? initialItemLayout.width
+ : initialItemLayout.height;
+ if (windowSize > 0) {
+ initialItemOffset = Math.max(0, initialItemOffset - (windowSize - itemSize) * viewPosition);
+ }
+ }
this.engagedIndicesTracker.scrollOffset = initialItemOffset;
}
else {
diff --git a/node_modules/@shopify/flash-list/dist/recyclerview/hooks/useRecyclerViewController.js b/node_modules/@shopify/flash-list/dist/recyclerview/hooks/useRecyclerViewController.js
index 18e59ce..17c8063 100644
--- a/node_modules/@shopify/flash-list/dist/recyclerview/hooks/useRecyclerViewController.js
+++ b/node_modules/@shopify/flash-list/dist/recyclerview/hooks/useRecyclerViewController.js
@@ -566,10 +566,45 @@ export function useRecyclerViewController(recyclerViewManager, ref, scrollViewRe
}, 500);
pauseOffsetCorrection.current = true;
const additionalOffset = (_c = initialScrollIndexParams === null || initialScrollIndexParams === void 0 ? void 0 : initialScrollIndexParams.viewOffset) !== null && _c !== void 0 ? _c : 0;
- const offset = horizontal
- ? recyclerViewManager.getLayout(initialScrollIndex).x + additionalOffset
- : recyclerViewManager.getLayout(initialScrollIndex).y +
- additionalOffset;
+ const initialItemLayout = recyclerViewManager.getLayout(initialScrollIndex);
+ let offset = (horizontal ? initialItemLayout.x : initialItemLayout.y) +
+ additionalOffset;
+ // Position the target item within the viewport (0 = start, 0.5 = center, 1 = end), mirroring scrollToIndex.
+ const viewPosition = initialScrollIndexParams === null || initialScrollIndexParams === void 0 ? void 0 : initialScrollIndexParams.viewPosition;
+ if (viewPosition !== undefined) {
+ const containerSize = horizontal
+ ? recyclerViewManager.getWindowSize().width
+ : recyclerViewManager.getWindowSize().height;
+ const itemSize = horizontal
+ ? initialItemLayout.width
+ : initialItemLayout.height;
+ if (containerSize > 0) {
+ offset = Math.max(0, offset - (containerSize - itemSize) * viewPosition);
+ }
+ }
+ // Make it clear there are more items to scroll to underneath the bottom edge.
+ // If the bottom item is (essentially) fully visible against the bottom edge AND there
+ // is an item underneath it, nudge the bottom edge up so CROP_OFFSET px of the current
+ // bottom item gets cropped, signalling that more content can be scrolled into view.
+ if (viewPosition !== undefined && !horizontal && recyclerViewManager.props.inverted && offset > 0) {
+ const CROP_OFFSET = 10;
+ let bottomIndex = -1;
+ for (let i = initialScrollIndex; i >= 0; i--) {
+ if (recyclerViewManager.getLayout(i).y <= offset) {
+ bottomIndex = i;
+ break;
+ }
+ }
+ if (bottomIndex > 0) {
+ const bottomItemLayout = recyclerViewManager.getLayout(bottomIndex);
+ const hiddenPortion = offset - bottomItemLayout.y;
+ // 8px is bottom padding of every item
+ if (hiddenPortion <= 8) {
+ // Crop the current bottom item rather than letting it sit flush against the edge.
+ offset = bottomItemLayout.y + CROP_OFFSET;
+ }
+ }
+ }
handlerMethods.scrollToOffset({
offset,
animated: false,
12 changes: 12 additions & 0 deletions patches/@shopify/flash-list/details.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,15 @@
- Upstream PR/issue: https://github.com/Shopify/flash-list/issues/2334
- E/App issue: https://github.com/Expensify/App/issues/91584, https://github.com/Expensify/App/issues/92263
- PR introducing patch: https://github.com/Expensify/App/pull/92520

### [@shopify+flash-list+2.3.0+013+improve-scroll-key-handling.patch](@shopify+flash-list+2.3.0+013+improve-scroll-key-handling.patch)

- Reason: Adds `viewPosition` support to `initialScrollIndexParams` (0 = start, 0.5 = center, 1 = end — same semantics as `scrollToIndex`'s `viewPosition`). Four changes:
1. **`applyInitialScrollIndex`** in `useRecyclerViewController.js`: the corrective scroll for `initialScrollIndex` now shifts the target offset by `(containerSize - itemSize) * viewPosition` (clamped to ≥ 0, and skipped while the container is unmeasured), mirroring `scrollToIndex`'s math.
2. **`applyInitialScrollAdjustment`** in `RecyclerViewManager.js`: the initial render window is anchored with the same `viewPosition` adjustment, so the very first painted frame already renders the items around the centered position — without this, the first frame renders items from the target's raw offset (target at the viewport edge) and visibly jumps once the first corrective scroll lands.
3. **Bottom crop** in `applyInitialScrollIndex` (`useRecyclerViewController.js`): for inverted vertical lists positioned via `viewPosition`, when the bottom-most visible item is flush against the bottom edge and another item exists underneath it, the offset is nudged up so the current bottom item is cropped by a few pixels — signaling there is more content below.
4. **`recomputeLayouts` range** in `applyInitialScrollAdjustment` (`RecyclerViewManager.js`): the recompute that precedes reading the target offset is widened from `recomputeLayouts(0, initialScrollIndex)` to `recomputeLayouts(0, this.getDataLength() - 1)`, so every item gets a measured/re-estimated layout before the positioning.
- Files changed: `dist/FlashListProps.d.ts`, `dist/recyclerview/hooks/useRecyclerViewController.js`, `dist/recyclerview/RecyclerViewManager.js`.
- Upstream PR/issue: https://github.com/Shopify/flash-list/pull/2318 (for point 4)
- E/App issue: https://github.com/Expensify/App/issues/92152
- PR introducing patch: https://github.com/Expensify/App/pull/93403
1 change: 1 addition & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1751,6 +1751,7 @@ const CONST = {
THREAD_DISABLED: ['CREATED'],
LATEST_MESSAGES_PILL_SCROLL_OFFSET_THRESHOLD: 2000,
ACTION_VISIBLE_THRESHOLD: 250,
LINKED_MESSAGE_OFFSET: 40,
MAX_GROUPING_TIME: 300000,
},
CANCEL_PAYMENT_REASONS: {
Expand Down
42 changes: 2 additions & 40 deletions src/components/FlashList/InvertedFlashList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import type {FlashListProps} from '@shopify/flash-list';
import React from 'react';
import useFlashListScrollKey from '@components/FlashList/useFlashListScrollKey';
import type {FlatListRefType} from '@pages/inbox/ReportScreenContext';
import FlashList from '..';
import CellRendererComponent from './CellRendererComponent';

type InvertedFlashListProps<T> = FlashListProps<T> & {
/** Key of the item to initially scroll to when the list first renders. */
initialScrollKey?: string | null;

/** The array of items to render in the list. */
data: T[];

Expand All @@ -17,48 +13,14 @@ type InvertedFlashListProps<T> = FlashListProps<T> & {

/** Ref to the underlying list instance. */
ref: FlatListRefType;

/** Whether the list should handle `maintainVisibleContentPosition` */
shouldMaintainVisibleContentPosition?: boolean;
};

function InvertedFlashList<T>({
data,
keyExtractor,
initialScrollKey,
onStartReached: onStartReachedProp,
maintainVisibleContentPosition: maintainVisibleContentPositionProp,
shouldMaintainVisibleContentPosition,
...restProps
}: InvertedFlashListProps<T>) {
const {
displayedData,
onStartReached,
maintainVisibleContentPosition: maintainVisibleContentPositionForScrollKey,
} = useFlashListScrollKey<T>({
data,
keyExtractor,
initialScrollKey,
onStartReached: onStartReachedProp,
shouldMaintainVisibleContentPosition,
});

const maintainVisibleContentPosition = maintainVisibleContentPositionProp
? {
...maintainVisibleContentPositionForScrollKey,
...maintainVisibleContentPositionProp,
}
: maintainVisibleContentPositionForScrollKey;

function InvertedFlashList<T>(props: InvertedFlashListProps<T>) {
return (
<FlashList<T>
{...restProps}
{...props}
inverted
onStartReached={onStartReached}
data={displayedData}
keyExtractor={keyExtractor}
CellRendererComponent={CellRendererComponent}
maintainVisibleContentPosition={maintainVisibleContentPosition}
/>
);
}
Expand Down
64 changes: 0 additions & 64 deletions src/components/FlashList/useFlashListScrollKey.ts

This file was deleted.

Loading
Loading