Skip to content
Open
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
3 changes: 2 additions & 1 deletion patches/react-native/details.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@

### [react-native+0.86.0+002+fixMVCPAndroid.patch](react-native+0.86.0+002+fixMVCPAndroid.patch)

- Reason: Fixes content jumping issues with `MaintainVisibleContentPosition` on Android, particularly in bidirectional pagination scenarios. The patch makes two key improvements:
- 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.
- Upstream PR/issue: https://github.com/facebook/react-native/pull/46247
- E/App issue: 🛑
- PR Introducing Patch: https://github.com/Expensify/App/pull/46315 (introduced), https://github.com/Expensify/App/pull/45289 (refactored)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,16 @@ index 2bee605..ba26e7b 100644
for (i in config.minIndexForVisible until contentView.childCount) {
val child = contentView.getChildAt(i)

@@ -128,27 +135,49 @@ internal class MaintainVisibleScrollPositionHelper<ScrollViewT>(
@@ -128,27 +135,57 @@ internal class MaintainVisibleScrollPositionHelper<ScrollViewT>(
val position = if (horizontal) child.x + child.width else child.y + child.height

+ // Virtualized lists can use a zero-sized, positioned first child as their scroll anchor.
+ // Preserve it instead of choosing the container that holds all rendered items.
+ if (i == config.minIndexForVisible && child.width == 0 && child.height == 0 && position > currentScroll) {
+ firstVisibleView = child
+ break
+ }
+
// If the child is partially visible or this is the last child, select it as the anchor.
- if (position > currentScroll || i == contentView.childCount - 1) {
- firstVisibleViewRef = WeakReference(child)
Expand Down Expand Up @@ -70,7 +77,8 @@ index 2bee605..ba26e7b 100644
}
+ val frame = Rect()
+ firstVisibleView.getHitRect(frame)
+ if (frame.width() > 0 || frame.height() > 0) {
+ // Zero-sized anchors have a meaningful position even though they have no area.
+ if (frame.width() > 0 || frame.height() > 0 || frame.left != 0 || frame.top != 0) {
+ prevFirstVisibleFrame = frame
+ } else {
+ prevFirstVisibleFrame = null
Expand Down

This file was deleted.

28 changes: 0 additions & 28 deletions src/components/FlashList/InvertedFlashList/index.tsx

This file was deleted.

2 changes: 1 addition & 1 deletion src/components/FlashList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {FlashList as ShopifyFlashList} from '@shopify/flash-list';
import React from 'react';

function FlashList<T>({onScroll: onScrollProp, inverted, ...restProps}: FlashListProps<T>) {
const emitComposerScrollEvents = useEmitComposerScrollEvents({enabled: true, inverted});
const emitComposerScrollEvents = useEmitComposerScrollEvents({enabled: !!inverted});

const handleScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
onScrollProp?.(e);
Expand Down
8 changes: 2 additions & 6 deletions src/components/FlashList/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import type {RefObject} from 'react';
import type {FlatList} from 'react-native';
import type ActionListRefType from '@pages/inbox/ActionListTypes';

/** Ref to the underlying list instance attached via `ref={}`. */
type FlatListRefType = RefObject<FlatList<unknown> | null> | null;

export default FlatListRefType;
export default ActionListRefType;
37 changes: 14 additions & 23 deletions src/components/FlatList/FlatList/index.ios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import useThemeStyles from '@hooks/useThemeStyles';

import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native';

import React, {useCallback, useRef, useState} from 'react';
import {useRef, useState} from 'react';
import {FlatList} from 'react-native';

import type {CustomFlatListProps} from './types';
Expand All @@ -26,30 +26,21 @@ function CustomFlatList<T>({
}: CustomFlatListProps<T>) {
const [isScrolling, setIsScrolling] = useState(false);
const styles = useThemeStyles();
const handleScrollBegin = useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
onMomentumScrollBegin?.(event);
setIsScrolling(true);
},
[onMomentumScrollBegin],
);
const handleScrollBegin = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
onMomentumScrollBegin?.(event);
setIsScrolling(true);
};

const handleScrollEnd = useCallback(
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
onMomentumScrollEnd?.(event);
setIsScrolling(false);
},
[onMomentumScrollEnd],
);
const handleScrollEnd = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
onMomentumScrollEnd?.(event);
setIsScrolling(false);
};

const emitComposerScrollEvents = useEmitComposerScrollEvents({enabled: !enableAnimatedKeyboardDismissal, inverted: restProps.inverted});
const handleScroll = useCallback(
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
onScrollProp?.(e);
emitComposerScrollEvents();
},
[emitComposerScrollEvents, onScrollProp],
);
const emitComposerScrollEvents = useEmitComposerScrollEvents({enabled: !enableAnimatedKeyboardDismissal && !!restProps.inverted});
const handleScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
onScrollProp?.(e);
emitComposerScrollEvents();
};

const listRef = useRef<FlatListInnerRefType<T> | null>(null);
useFlatListHandle<T>({
Expand Down
2 changes: 1 addition & 1 deletion src/components/FlatList/FlatList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ function MVCPFlatList<T>({
};
}, []);

const emitComposerScrollEvents = useEmitComposerScrollEvents({enabled: true, inverted: restProps.inverted});
const emitComposerScrollEvents = useEmitComposerScrollEvents({enabled: !!restProps.inverted});
const handleScroll = useCallback(
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
onScrollProp?.(e);
Expand Down
2 changes: 1 addition & 1 deletion src/components/KeyboardDismissibleFlatList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {useKeyboardDismissibleFlatListActions} from './KeyboardDismissibleFlatLi
function KeyboardDismissibleFlatList<T>({onScroll: onScrollProp, inverted, ref, ...restProps}: AnimatedFlatListWithCellRendererProps<T>) {
const {onScroll: onScrollHandleKeyboard} = useKeyboardDismissibleFlatListActions();

const emitComposerScrollEvents = useEmitComposerScrollEvents({enabled: true, inverted});
const emitComposerScrollEvents = useEmitComposerScrollEvents({enabled: !!inverted});

const additionalOnScroll = useAnimatedScrollHandler({
onScroll: emitComposerScrollEvents,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import {useActionListContext, useActionListRef} from '@pages/inbox/ActionListCon
import {useAgentZeroStatus} from '@pages/inbox/AgentZeroStatusContext';
import {useConciergeDraft} from '@pages/inbox/ConciergeDraftContext';
import FloatingMessageCounter from '@pages/inbox/report/FloatingMessageCounter';
import ReportActionIndexContext from '@pages/inbox/report/ReportActionIndexContext';
import ReportActionIndexContext, {ReportActionScrollToNewestContext} from '@pages/inbox/report/ReportActionIndexContext';
import ReportActionsListItemRenderer from '@pages/inbox/report/ReportActionsListItemRenderer';
import {getUnreadMarkerReportAction} from '@pages/inbox/report/shouldDisplayNewMarkerOnReportAction';
import useReportUnreadMessageScrollTracking from '@pages/inbox/report/useReportUnreadMessageScrollTracking';
Expand Down Expand Up @@ -637,25 +637,34 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
hasNextActionMadeBySameActor(visibleReportActions, indexWithinReportActions, isOffline);
const shouldDisableContextMenuForConciergeDraft = isDraftPendingCompletion && draftReportActionID === reportAction.reportActionID;

// This value cannot be memoized, because it is based on the indexWithinReportActions which changes on every render.
// eslint-disable-next-line react/jsx-no-constructed-context-values
const reportActionIndexContextValue = {
index: indexWithinReportActions,
isNewest: indexWithinReportActions === visibleReportActions.length - 1,
};

return (
<ReportActionIndexContext.Provider value={indexWithinReportActions}>
<ReportActionsListItemRenderer
reportAction={reportAction}
parentReportAction={parentReportAction}
parentReportActionForTransactionThread={EmptyParentReportActionForTransactionThread}
report={reportStable}
transactionThreadReport={transactionThreadReport}
chatReport={chatReport}
displayAsGroup={displayAsGroup}
shouldDisplayNewMarker={reportAction.reportActionID === unreadMarkerReportActionID}
shouldDisplayReplyDivider={visibleReportActions.length > 1}
isFirstVisibleReportAction={firstVisibleReportActionID === reportAction.reportActionID}
shouldHideThreadDividerLine
linkedReportActionID={linkedReportActionID}
isHarvestCreatedExpenseReport={shouldShowHarvestCreatedAction}
shouldDisableContextMenuForConciergeDraft={shouldDisableContextMenuForConciergeDraft}
/>
</ReportActionIndexContext.Provider>
<ReportActionScrollToNewestContext.Provider value={scrollToBottom}>
<ReportActionIndexContext.Provider value={reportActionIndexContextValue}>
<ReportActionsListItemRenderer
reportAction={reportAction}
parentReportAction={parentReportAction}
parentReportActionForTransactionThread={EmptyParentReportActionForTransactionThread}
report={reportStable}
transactionThreadReport={transactionThreadReport}
chatReport={chatReport}
displayAsGroup={displayAsGroup}
shouldDisplayNewMarker={reportAction.reportActionID === unreadMarkerReportActionID}
shouldDisplayReplyDivider={visibleReportActions.length > 1}
isFirstVisibleReportAction={firstVisibleReportActionID === reportAction.reportActionID}
shouldHideThreadDividerLine
linkedReportActionID={linkedReportActionID}
isHarvestCreatedExpenseReport={shouldShowHarvestCreatedAction}
shouldDisableContextMenuForConciergeDraft={shouldDisableContextMenuForConciergeDraft}
/>
</ReportActionIndexContext.Provider>
</ReportActionScrollToNewestContext.Provider>
);
},
[
Expand All @@ -671,6 +680,7 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
shouldShowHarvestCreatedAction,
draftReportActionID,
isDraftPendingCompletion,
scrollToBottom,
],
);

Expand Down
Loading
Loading