From 6b2ebd6cc7c0989d44d56b37ed7d51e570354850 Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Sun, 15 Mar 2026 18:41:47 +0700 Subject: [PATCH 1/9] fix content alignment & improvemance --- src/components/ActionSheetAwareScrollView/index.tsx | 3 --- src/components/FlatList/InvertedFlatList/index.tsx | 3 +++ src/pages/inbox/report/ReportActionsList.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/ActionSheetAwareScrollView/index.tsx b/src/components/ActionSheetAwareScrollView/index.tsx index 799f4742a94b5..408149163968d 100644 --- a/src/components/ActionSheetAwareScrollView/index.tsx +++ b/src/components/ActionSheetAwareScrollView/index.tsx @@ -2,21 +2,18 @@ // On all other platforms, the action sheet is implemented using the Animated.ScrollView import React from 'react'; import Reanimated from 'react-native-reanimated'; -import useThemeStyles from '@hooks/useThemeStyles'; import {Actions, ActionSheetAwareScrollViewProvider, useActionSheetAwareScrollViewActions, useActionSheetAwareScrollViewState} from './ActionSheetAwareScrollViewContext'; import type {ActionSheetAwareScrollViewProps, RenderActionSheetAwareScrollViewComponent} from './types'; import useActionSheetAwareScrollViewRef from './useActionSheetAwareScrollViewRef'; function ActionSheetAwareScrollView({children, ref, ...restProps}: ActionSheetAwareScrollViewProps) { const {onRef} = useActionSheetAwareScrollViewRef(ref); - const styles = useThemeStyles(); return ( {children} diff --git a/src/components/FlatList/InvertedFlatList/index.tsx b/src/components/FlatList/InvertedFlatList/index.tsx index 87af6b05d9576..8abd97fafde93 100644 --- a/src/components/FlatList/InvertedFlatList/index.tsx +++ b/src/components/FlatList/InvertedFlatList/index.tsx @@ -4,6 +4,7 @@ import useFlatListScrollKey from '@components/FlatList/hooks/useFlatListScrollKe import CellRendererComponent from './CellRendererComponent'; import shouldRemoveClippedSubviews from './shouldRemoveClippedSubviews'; import type {InvertedFlatListProps} from './types'; +import useThemeStyles from '@hooks/useThemeStyles'; // Adapted from https://github.com/facebook/react-native/blob/29a0d7c3b201318a873db0d1b62923f4ce720049/packages/virtualized-lists/Lists/VirtualizeUtils.js#L237 function defaultKeyExtractor(item: T | {key: string} | {id: string}, index: number): string { @@ -38,6 +39,7 @@ function InvertedFlatList({ renderItem, ref, }); + const styles = useThemeStyles(); return ( @@ -52,6 +54,7 @@ function InvertedFlatList({ onStartReached={handleStartReached} CellRendererComponent={CellRendererComponent} removeClippedSubviews={shouldRemoveClippedSubviews} + contentContainerStyle={[restProps.contentContainerStyle, restProps.horizontal ? styles.flexRowReverse : styles.flexColumnReverse]} /> ); } diff --git a/src/pages/inbox/report/ReportActionsList.tsx b/src/pages/inbox/report/ReportActionsList.tsx index a7aba0f5e5c77..5c96c87beba70 100644 --- a/src/pages/inbox/report/ReportActionsList.tsx +++ b/src/pages/inbox/report/ReportActionsList.tsx @@ -918,7 +918,7 @@ function ReportActionsList({ data={sortedVisibleReportActions} renderItem={renderItem} renderScrollComponent={renderActionSheetAwareScrollView} - contentContainerStyle={[styles.chatContentScrollView, shouldFocusToTopOnMount ? styles.justifyContentEnd : undefined]} + contentContainerStyle={[styles.chatContentScrollView, !shouldFocusToTopOnMount ? styles.justifyContentEnd : undefined]} shouldHideContent={shouldScrollToEndAfterLayout} shouldDisableVisibleContentPosition={shouldScrollToEndAfterLayout} showsVerticalScrollIndicator={!shouldScrollToEndAfterLayout} From 881bd22dc973e4701a323537230cb144a36e59df Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Sun, 15 Mar 2026 18:49:32 +0700 Subject: [PATCH 2/9] fix check error --- src/components/FlatList/InvertedFlatList/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/FlatList/InvertedFlatList/index.tsx b/src/components/FlatList/InvertedFlatList/index.tsx index 8abd97fafde93..74b97011edbf8 100644 --- a/src/components/FlatList/InvertedFlatList/index.tsx +++ b/src/components/FlatList/InvertedFlatList/index.tsx @@ -1,10 +1,10 @@ import React from 'react'; import FlatList from '@components/FlatList/FlatList'; import useFlatListScrollKey from '@components/FlatList/hooks/useFlatListScrollKey'; +import useThemeStyles from '@hooks/useThemeStyles'; import CellRendererComponent from './CellRendererComponent'; import shouldRemoveClippedSubviews from './shouldRemoveClippedSubviews'; import type {InvertedFlatListProps} from './types'; -import useThemeStyles from '@hooks/useThemeStyles'; // Adapted from https://github.com/facebook/react-native/blob/29a0d7c3b201318a873db0d1b62923f4ce720049/packages/virtualized-lists/Lists/VirtualizeUtils.js#L237 function defaultKeyExtractor(item: T | {key: string} | {id: string}, index: number): string { From 495d176dad1877cc096ad9d97eeeefa4296a49ae Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Mon, 16 Mar 2026 04:53:51 +0700 Subject: [PATCH 3/9] fix: native bug & improvemance --- src/components/FlatList/InvertedFlatList/index.tsx | 6 +++++- src/pages/inbox/report/PureReportActionItem.tsx | 4 +++- src/pages/inbox/report/ReportActionsList.tsx | 11 +++++++++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/components/FlatList/InvertedFlatList/index.tsx b/src/components/FlatList/InvertedFlatList/index.tsx index 74b97011edbf8..d09ef797cb624 100644 --- a/src/components/FlatList/InvertedFlatList/index.tsx +++ b/src/components/FlatList/InvertedFlatList/index.tsx @@ -2,6 +2,8 @@ import React from 'react'; import FlatList from '@components/FlatList/FlatList'; import useFlatListScrollKey from '@components/FlatList/hooks/useFlatListScrollKey'; import useThemeStyles from '@hooks/useThemeStyles'; +import getPlatform from '@libs/getPlatform'; +import CONST from '@src/CONST'; import CellRendererComponent from './CellRendererComponent'; import shouldRemoveClippedSubviews from './shouldRemoveClippedSubviews'; import type {InvertedFlatListProps} from './types'; @@ -40,6 +42,8 @@ function InvertedFlatList({ ref, }); const styles = useThemeStyles(); + const platform = getPlatform(); + const isWeb = platform === CONST.PLATFORM.WEB; return ( @@ -54,7 +58,7 @@ function InvertedFlatList({ onStartReached={handleStartReached} CellRendererComponent={CellRendererComponent} removeClippedSubviews={shouldRemoveClippedSubviews} - contentContainerStyle={[restProps.contentContainerStyle, restProps.horizontal ? styles.flexRowReverse : styles.flexColumnReverse]} + contentContainerStyle={[restProps.contentContainerStyle, !isWeb ? undefined : restProps.horizontal ? styles.flexRowReverse : styles.flexColumnReverse]} /> ); } diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index e6a64ecb1aa46..2b7cd6b203e04 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -67,6 +67,7 @@ import type {OnyxDataWithErrors} from '@libs/ErrorUtils'; import {getLatestErrorMessageField, isReceiptError} from '@libs/ErrorUtils'; import focusComposerWithDelay from '@libs/focusComposerWithDelay'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; +import getPlatform from '@libs/getPlatform'; import {isReportMessageAttachment} from '@libs/isReportMessageAttachment'; import Navigation from '@libs/Navigation/Navigation'; import {getBankAccountLastFourDigits} from '@libs/PaymentUtils'; @@ -546,6 +547,7 @@ function PureReportActionItem({ const {transitionActionSheetState} = ActionSheetAwareScrollView.useActionSheetAwareScrollViewActions(); const {translate, formatPhoneNumber, localeCompare, formatTravelDate, getLocalDateFromDatetime, datetimeToCalendarTime} = useLocalize(); const {showConfirmModal} = useConfirmModal(); + const platform = getPlatform(); const personalDetail = useCurrentUserPersonalDetails(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const reportID = report?.reportID ?? action?.reportID; @@ -2056,7 +2058,7 @@ function PureReportActionItem({ withoutFocusOnSecondaryInteraction accessibilityLabel={accessibilityLabel} accessibilityHint={translate('accessibilityHints.chatMessage')} - accessibilityRole={Platform.OS !== CONST.PLATFORM.WEB ? CONST.ROLE.BUTTON : undefined} + accessibilityRole={platform !== CONST.PLATFORM.WEB ? CONST.ROLE.BUTTON : undefined} sentryLabel={CONST.SENTRY_LABEL.REPORT.PURE_REPORT_ACTION_ITEM} > { @@ -918,7 +925,7 @@ function ReportActionsList({ data={sortedVisibleReportActions} renderItem={renderItem} renderScrollComponent={renderActionSheetAwareScrollView} - contentContainerStyle={[styles.chatContentScrollView, !shouldFocusToTopOnMount ? styles.justifyContentEnd : undefined]} + contentContainerStyle={chatContentContainerStyle} shouldHideContent={shouldScrollToEndAfterLayout} shouldDisableVisibleContentPosition={shouldScrollToEndAfterLayout} showsVerticalScrollIndicator={!shouldScrollToEndAfterLayout} From bb06be62125a0874fbea4d1bc4760a65078d960f Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Mon, 16 Mar 2026 16:43:29 +0700 Subject: [PATCH 4/9] fix --- src/components/FlatList/InvertedFlatList/index.tsx | 4 +++- src/pages/inbox/report/PureReportActionItem.tsx | 5 +++-- src/pages/inbox/report/ReportActionsList.tsx | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/FlatList/InvertedFlatList/index.tsx b/src/components/FlatList/InvertedFlatList/index.tsx index d09ef797cb624..bf4d9804e5ef8 100644 --- a/src/components/FlatList/InvertedFlatList/index.tsx +++ b/src/components/FlatList/InvertedFlatList/index.tsx @@ -45,6 +45,8 @@ function InvertedFlatList({ const platform = getPlatform(); const isWeb = platform === CONST.PLATFORM.WEB; + const contentContainerDirectionStyle = [restProps.horizontal ? styles.flexRowReverse : styles.flexColumnReverse] + return ( // eslint-disable-next-line react/jsx-props-no-spreading @@ -58,7 +60,7 @@ function InvertedFlatList({ onStartReached={handleStartReached} CellRendererComponent={CellRendererComponent} removeClippedSubviews={shouldRemoveClippedSubviews} - contentContainerStyle={[restProps.contentContainerStyle, !isWeb ? undefined : restProps.horizontal ? styles.flexRowReverse : styles.flexColumnReverse]} + contentContainerStyle={[restProps.contentContainerStyle, isWeb ? contentContainerDirectionStyle : undefined]} /> ); } diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index 2b7cd6b203e04..e95805dd4744f 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -3,7 +3,7 @@ import {deepEqual} from 'fast-equals'; import mapValues from 'lodash/mapValues'; import React, {memo, use, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import type {GestureResponderEvent, TextInput} from 'react-native'; -import {InteractionManager, Keyboard, Platform, View} from 'react-native'; +import {InteractionManager, Keyboard, View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {Emoji} from '@assets/emojis/types'; @@ -548,6 +548,7 @@ function PureReportActionItem({ const {translate, formatPhoneNumber, localeCompare, formatTravelDate, getLocalDateFromDatetime, datetimeToCalendarTime} = useLocalize(); const {showConfirmModal} = useConfirmModal(); const platform = getPlatform(); + const isWeb = platform === CONST.PLATFORM.WEB; const personalDetail = useCurrentUserPersonalDetails(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const reportID = report?.reportID ?? action?.reportID; @@ -2058,7 +2059,7 @@ function PureReportActionItem({ withoutFocusOnSecondaryInteraction accessibilityLabel={accessibilityLabel} accessibilityHint={translate('accessibilityHints.chatMessage')} - accessibilityRole={platform !== CONST.PLATFORM.WEB ? CONST.ROLE.BUTTON : undefined} + accessibilityRole={!isWeb ? CONST.ROLE.BUTTON : undefined} sentryLabel={CONST.SENTRY_LABEL.REPORT.PURE_REPORT_ACTION_ITEM} > Date: Mon, 16 Mar 2026 16:47:02 +0700 Subject: [PATCH 5/9] fix: prettier --- src/components/FlatList/InvertedFlatList/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/FlatList/InvertedFlatList/index.tsx b/src/components/FlatList/InvertedFlatList/index.tsx index bf4d9804e5ef8..f2b5fe20e3efd 100644 --- a/src/components/FlatList/InvertedFlatList/index.tsx +++ b/src/components/FlatList/InvertedFlatList/index.tsx @@ -45,7 +45,7 @@ function InvertedFlatList({ const platform = getPlatform(); const isWeb = platform === CONST.PLATFORM.WEB; - const contentContainerDirectionStyle = [restProps.horizontal ? styles.flexRowReverse : styles.flexColumnReverse] + const contentContainerDirectionStyle = [restProps.horizontal ? styles.flexRowReverse : styles.flexColumnReverse]; return ( From 062868428074dd79c7ac1c33da2cf9226570c1b0 Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Tue, 17 Mar 2026 01:19:17 +0700 Subject: [PATCH 6/9] fix: platform consistency --- .../InvertedFlatList/index.native.tsx | 63 +++++++++++++++++++ .../FlatList/InvertedFlatList/index.tsx | 9 +-- .../FlatList/InvertedFlatList/types.ts | 1 + src/pages/inbox/report/ReportActionsList.tsx | 9 +-- 4 files changed, 68 insertions(+), 14 deletions(-) create mode 100644 src/components/FlatList/InvertedFlatList/index.native.tsx diff --git a/src/components/FlatList/InvertedFlatList/index.native.tsx b/src/components/FlatList/InvertedFlatList/index.native.tsx new file mode 100644 index 0000000000000..40c886cb196d2 --- /dev/null +++ b/src/components/FlatList/InvertedFlatList/index.native.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import FlatList from '@components/FlatList/FlatList'; +import useFlatListScrollKey from '@components/FlatList/hooks/useFlatListScrollKey'; +import CellRendererComponent from './CellRendererComponent'; +import shouldRemoveClippedSubviews from './shouldRemoveClippedSubviews'; +import type {InvertedFlatListProps} from './types'; +import useThemeStyles from '@hooks/useThemeStyles'; + +// Adapted from https://github.com/facebook/react-native/blob/29a0d7c3b201318a873db0d1b62923f4ce720049/packages/virtualized-lists/Lists/VirtualizeUtils.js#L237 +function defaultKeyExtractor(item: T | {key: string} | {id: string}, index: number): string { + if (item != null) { + if (typeof item === 'object' && 'key' in item) { + return item.key; + } + if (typeof item === 'object' && 'id' in item) { + return item.id; + } + } + return String(index); +} + +function InvertedFlatList({ + ref, + shouldEnableAutoScrollToTopThreshold, + shouldFocusToTopOnMount = false, + initialScrollKey, + data, + onStartReached, + renderItem, + keyExtractor = defaultKeyExtractor, + ...restProps +}: InvertedFlatListProps) { + const {displayedData, maintainVisibleContentPosition, handleStartReached, handleRenderItem, listRef} = useFlatListScrollKey({ + data, + keyExtractor, + initialScrollKey, + inverted: true, + onStartReached, + shouldEnableAutoScrollToTopThreshold, + renderItem, + ref, + }); + const styles = useThemeStyles(); + + return ( + + // eslint-disable-next-line react/jsx-props-no-spreading + {...restProps} + ref={listRef} + maintainVisibleContentPosition={maintainVisibleContentPosition} + inverted + data={displayedData} + renderItem={handleRenderItem} + keyExtractor={keyExtractor} + onStartReached={handleStartReached} + CellRendererComponent={CellRendererComponent} + removeClippedSubviews={shouldRemoveClippedSubviews} + contentContainerStyle={[restProps.contentContainerStyle, shouldFocusToTopOnMount ? styles.justifyContentEnd : undefined]} + /> + ); +} + +export default InvertedFlatList; diff --git a/src/components/FlatList/InvertedFlatList/index.tsx b/src/components/FlatList/InvertedFlatList/index.tsx index f2b5fe20e3efd..a904e0a20f5a3 100644 --- a/src/components/FlatList/InvertedFlatList/index.tsx +++ b/src/components/FlatList/InvertedFlatList/index.tsx @@ -2,8 +2,6 @@ import React from 'react'; import FlatList from '@components/FlatList/FlatList'; import useFlatListScrollKey from '@components/FlatList/hooks/useFlatListScrollKey'; import useThemeStyles from '@hooks/useThemeStyles'; -import getPlatform from '@libs/getPlatform'; -import CONST from '@src/CONST'; import CellRendererComponent from './CellRendererComponent'; import shouldRemoveClippedSubviews from './shouldRemoveClippedSubviews'; import type {InvertedFlatListProps} from './types'; @@ -24,6 +22,7 @@ function defaultKeyExtractor(item: T | {key: string} | {id: string}, index: n function InvertedFlatList({ ref, shouldEnableAutoScrollToTopThreshold, + shouldFocusToTopOnMount = false, initialScrollKey, data, onStartReached, @@ -42,10 +41,6 @@ function InvertedFlatList({ ref, }); const styles = useThemeStyles(); - const platform = getPlatform(); - const isWeb = platform === CONST.PLATFORM.WEB; - - const contentContainerDirectionStyle = [restProps.horizontal ? styles.flexRowReverse : styles.flexColumnReverse]; return ( @@ -60,7 +55,7 @@ function InvertedFlatList({ onStartReached={handleStartReached} CellRendererComponent={CellRendererComponent} removeClippedSubviews={shouldRemoveClippedSubviews} - contentContainerStyle={[restProps.contentContainerStyle, isWeb ? contentContainerDirectionStyle : undefined]} + contentContainerStyle={[restProps.contentContainerStyle, restProps.horizontal ? styles.flexRowReverse : styles.flexColumnReverse, !shouldFocusToTopOnMount ? styles.justifyContentEnd : undefined]} /> ); } diff --git a/src/components/FlatList/InvertedFlatList/types.ts b/src/components/FlatList/InvertedFlatList/types.ts index a779bc247bd92..eb17bdeb533d2 100644 --- a/src/components/FlatList/InvertedFlatList/types.ts +++ b/src/components/FlatList/InvertedFlatList/types.ts @@ -4,6 +4,7 @@ import type {CustomFlatListProps} from '@components/FlatList/FlatList/types'; type InvertedFlatListProps = Omit, 'data' | 'renderItem' | 'initialScrollIndex'> & { shouldEnableAutoScrollToTopThreshold?: boolean; + shouldFocusToTopOnMount?: boolean; data: T[]; renderItem: ListRenderItem; initialScrollKey?: string | null; diff --git a/src/pages/inbox/report/ReportActionsList.tsx b/src/pages/inbox/report/ReportActionsList.tsx index 6a9aa6ebfe042..b870b2e0f68e6 100644 --- a/src/pages/inbox/report/ReportActionsList.tsx +++ b/src/pages/inbox/report/ReportActionsList.tsx @@ -816,12 +816,6 @@ function ReportActionsList({ const hideComposer = !canUserPerformWriteAction(report, isReportArchived); const shouldShowReportRecipientLocalTime = canShowReportRecipientLocalTime(personalDetailsList, report, currentUserAccountID) && !isComposerFullSize; const canShowHeader = isOffline || hasHeaderRendered.current; - const platform = getPlatform(); - const isWeb = platform === CONST.PLATFORM.WEB; - - // Web: reversed via `unshift`, `justifyContentEnd` only needed for normal chat (pushes content down), not when `shouldFocusToTopOnMount`. - // Native: flipped via scaleY(-1), `justifyContentEnd` always needed since "end" aligns to visual top. - const chatContentContainerStyle = [styles.chatContentScrollView, (isWeb ? !shouldFocusToTopOnMount : shouldFocusToTopOnMount) ? styles.justifyContentEnd : undefined]; const onLayoutInner = useCallback( (event: LayoutChangeEvent) => { @@ -925,8 +919,9 @@ function ReportActionsList({ data={sortedVisibleReportActions} renderItem={renderItem} renderScrollComponent={renderActionSheetAwareScrollView} - contentContainerStyle={chatContentContainerStyle} + contentContainerStyle={styles.chatContentScrollView} shouldHideContent={shouldScrollToEndAfterLayout} + shouldFocusToTopOnMount={shouldFocusToTopOnMount} shouldDisableVisibleContentPosition={shouldScrollToEndAfterLayout} showsVerticalScrollIndicator={!shouldScrollToEndAfterLayout} keyExtractor={keyExtractor} From c1acd81c766b7454b26c0db93f7c27c78e768a81 Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Tue, 17 Mar 2026 01:23:12 +0700 Subject: [PATCH 7/9] fix prettier --- src/components/FlatList/InvertedFlatList/index.native.tsx | 2 +- src/components/FlatList/InvertedFlatList/index.tsx | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/FlatList/InvertedFlatList/index.native.tsx b/src/components/FlatList/InvertedFlatList/index.native.tsx index 40c886cb196d2..28a7040859570 100644 --- a/src/components/FlatList/InvertedFlatList/index.native.tsx +++ b/src/components/FlatList/InvertedFlatList/index.native.tsx @@ -1,10 +1,10 @@ import React from 'react'; import FlatList from '@components/FlatList/FlatList'; import useFlatListScrollKey from '@components/FlatList/hooks/useFlatListScrollKey'; +import useThemeStyles from '@hooks/useThemeStyles'; import CellRendererComponent from './CellRendererComponent'; import shouldRemoveClippedSubviews from './shouldRemoveClippedSubviews'; import type {InvertedFlatListProps} from './types'; -import useThemeStyles from '@hooks/useThemeStyles'; // Adapted from https://github.com/facebook/react-native/blob/29a0d7c3b201318a873db0d1b62923f4ce720049/packages/virtualized-lists/Lists/VirtualizeUtils.js#L237 function defaultKeyExtractor(item: T | {key: string} | {id: string}, index: number): string { diff --git a/src/components/FlatList/InvertedFlatList/index.tsx b/src/components/FlatList/InvertedFlatList/index.tsx index a904e0a20f5a3..335cc526c41f9 100644 --- a/src/components/FlatList/InvertedFlatList/index.tsx +++ b/src/components/FlatList/InvertedFlatList/index.tsx @@ -55,7 +55,11 @@ function InvertedFlatList({ onStartReached={handleStartReached} CellRendererComponent={CellRendererComponent} removeClippedSubviews={shouldRemoveClippedSubviews} - contentContainerStyle={[restProps.contentContainerStyle, restProps.horizontal ? styles.flexRowReverse : styles.flexColumnReverse, !shouldFocusToTopOnMount ? styles.justifyContentEnd : undefined]} + contentContainerStyle={[ + restProps.contentContainerStyle, + restProps.horizontal ? styles.flexRowReverse : styles.flexColumnReverse, + !shouldFocusToTopOnMount ? styles.justifyContentEnd : undefined, + ]} /> ); } From 573213a6cd2ef832709e3b5cc158c97938b4d071 Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Tue, 17 Mar 2026 02:58:41 +0700 Subject: [PATCH 8/9] fix: eslint --- src/pages/inbox/report/ReportActionsList.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/inbox/report/ReportActionsList.tsx b/src/pages/inbox/report/ReportActionsList.tsx index b870b2e0f68e6..cbef88bc60db7 100644 --- a/src/pages/inbox/report/ReportActionsList.tsx +++ b/src/pages/inbox/report/ReportActionsList.tsx @@ -28,7 +28,6 @@ import {isSafari} from '@libs/Browser'; import type {ReasoningEntry} from '@libs/ConciergeReasoningStore'; import DateUtils from '@libs/DateUtils'; import FS from '@libs/Fullstory'; -import getPlatform from '@libs/getPlatform'; import durationHighlightItem from '@libs/Navigation/helpers/getDurationHighlightItem'; import isReportTopmostSplitNavigator from '@libs/Navigation/helpers/isReportTopmostSplitNavigator'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; From b7316a83c6d8669126bfdc13512ad37cee96f2f7 Mon Sep 17 00:00:00 2001 From: NJ-2020 Date: Thu, 19 Mar 2026 01:43:10 +0700 Subject: [PATCH 9/9] re-implement #82507 --- patches/react-native-web/details.md | 9 + ...ive-web+0.21.2+013+fix-selection-bug.patch | 194 ++++++++++++++++++ src/components/FlatList/FlatList/index.tsx | 4 +- 3 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 patches/react-native-web/react-native-web+0.21.2+013+fix-selection-bug.patch diff --git a/patches/react-native-web/details.md b/patches/react-native-web/details.md index df0cad52449ef..6450a4ae92a97 100644 --- a/patches/react-native-web/details.md +++ b/patches/react-native-web/details.md @@ -134,3 +134,12 @@ - Upstream PR/issue: https://github.com/necolas/react-native-web/issues/2817 - E/App issue: https://github.com/Expensify/App/issues/73782 - PR introducing patch: https://github.com/Expensify/App/pull/76332 + +### [react-native-web+0.21.2+013+fix-selection-bug.patch](react-native-web+0.21.2+013+fix-selection-bug.patch) +- Reason: + ``` + Fix selection bug for InvertedFlatlist by reversing the DOM tree elements using `pushOrUnshift` method + ``` +- Upstream PR/issue: https://github.com/necolas/react-native-web/issues/1807, it has been closed because of [this](https://github.com/necolas/react-native-web/issues/1807#issuecomment-725689704) +- E/App issue: https://github.com/Expensify/App/issues/37447 +- PR introducing patch: https://github.com/Expensify/App/pull/82507 \ No newline at end of file diff --git a/patches/react-native-web/react-native-web+0.21.2+013+fix-selection-bug.patch b/patches/react-native-web/react-native-web+0.21.2+013+fix-selection-bug.patch new file mode 100644 index 0000000000000..2a296cff0dba3 --- /dev/null +++ b/patches/react-native-web/react-native-web+0.21.2+013+fix-selection-bug.patch @@ -0,0 +1,194 @@ +diff --git a/node_modules/react-native-web/dist/exports/ScrollView/index.js b/node_modules/react-native-web/dist/exports/ScrollView/index.js +index c4f9b5b..fa32056 100644 +--- a/node_modules/react-native-web/dist/exports/ScrollView/index.js ++++ b/node_modules/react-native-web/dist/exports/ScrollView/index.js +@@ -558,8 +558,9 @@ class ScrollView extends React.Component { + var children = hasStickyHeaderIndices || pagingEnabled ? React.Children.map(this.props.children, (child, i) => { + var isSticky = hasStickyHeaderIndices && stickyHeaderIndices.indexOf(i) > -1; + if (child != null && (isSticky || pagingEnabled)) { ++ var stickyItemIndex = (this.props.children.length - 1) - i + 10; + return /*#__PURE__*/React.createElement(View, { +- style: [isSticky && styles.stickyHeader, pagingEnabled && styles.pagingEnabledChild] ++ style: [isSticky && styles.stickyHeader, pagingEnabled && styles.pagingEnabledChild, isSticky && {zIndex: stickyItemIndex}] + }, child); + } else { + return child; +@@ -636,7 +637,6 @@ var styles = StyleSheet.create({ + stickyHeader: { + position: 'sticky', + top: 0, +- zIndex: 10 + }, + pagingEnabledHorizontal: { + scrollSnapType: 'x mandatory' +diff --git a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +index 42c4984..caf22bb 100644 +--- a/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js ++++ b/node_modules/react-native-web/dist/vendor/react-native/VirtualizedList/index.js +@@ -108,6 +108,15 @@ function windowSizeOrDefault(windowSize) { + * + */ + class VirtualizedList extends StateSafePureComponent { ++ // reverse push order logic when props.inverted = true ++ pushOrUnshift(input, item) { ++ if (this.props.inverted) { ++ input.unshift(item); ++ } else { ++ input.push(item); ++ } ++ } ++ + // scrollToEnd may be janky without getItemLayout prop + scrollToEnd(params) { + var animated = params ? params.animated : true; +@@ -343,6 +352,7 @@ class VirtualizedList extends StateSafePureComponent { + }; + this._defaultRenderScrollComponent = props => { + var onRefresh = props.onRefresh; ++ var inversionStyle = this.props.inverted ? this.props.horizontal ? styles.rowReverse : styles.columnReverse : null; + if (this._isNestedWithSameOrientation()) { + // $FlowFixMe[prop-missing] - Typing ReactNativeComponent revealed errors + return /*#__PURE__*/React.createElement(View, props); +@@ -354,6 +364,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[prop-missing] Invalid prop usage + // $FlowFixMe[incompatible-use] + React.createElement(ScrollView, _extends({}, props, { ++ contentContainerStyle: StyleSheet.compose(inversionStyle, this.props.contentContainerStyle), + refreshControl: props.refreshControl == null ? /*#__PURE__*/React.createElement(RefreshControl + // $FlowFixMe[incompatible-type] + , { +@@ -366,7 +377,9 @@ class VirtualizedList extends StateSafePureComponent { + } else { + // $FlowFixMe[prop-missing] Invalid prop usage + // $FlowFixMe[incompatible-use] +- return /*#__PURE__*/React.createElement(ScrollView, props); ++ return /*#__PURE__*/React.createElement(ScrollView, _extends({}, props, { ++ contentContainerStyle: StyleSheet.compose(inversionStyle, this.props.contentContainerStyle) ++ })); + } + }; + this._onCellLayout = (e, cellKey, index) => { +@@ -568,6 +581,14 @@ class VirtualizedList extends StateSafePureComponent { + this._updateViewableItems(this.props, this.state.cellsAroundViewport); + this.setState((state, props) => { + var cellsAroundViewport = this._adjustCellsAroundViewport(props, state.cellsAroundViewport, state.pendingScrollUpdateCount); ++ ++ // revert the state if calculations are off ++ // this would only happen on the inverted flatlist (probably a bug with overscroll-behavior) ++ // when scrolled from bottom all the way up until onEndReached is triggered ++ if (cellsAroundViewport.first === cellsAroundViewport.last) { ++ cellsAroundViewport = state.cellsAroundViewport; ++ } ++ + var renderMask = VirtualizedList._createRenderMask(props, cellsAroundViewport, this._getNonViewportRenderRegions(props)); + if (cellsAroundViewport.first === state.cellsAroundViewport.first && cellsAroundViewport.last === state.cellsAroundViewport.last && renderMask.equals(state.renderMask)) { + return null; +@@ -679,7 +700,7 @@ class VirtualizedList extends StateSafePureComponent { + onViewableItemsChanged = _this$props3.onViewableItemsChanged, + viewabilityConfig = _this$props3.viewabilityConfig; + if (onViewableItemsChanged) { +- this._viewabilityTuples.push({ ++ this.pushOrUnshift(this._viewabilityTuples, { + viewabilityHelper: new ViewabilityHelper(viewabilityConfig), + onViewableItemsChanged: onViewableItemsChanged + }); +@@ -991,15 +1012,15 @@ class VirtualizedList extends StateSafePureComponent { + var end = getItemCount(data) - 1; + var prevCellKey; + last = Math.min(end, last); +- var _loop = function _loop() { ++ var _loop = () => { + var item = getItem(data, ii); + var key = VirtualizedList._keyExtractor(item, ii, _this.props); + _this._indicesToKeys.set(ii, key); + if (stickyIndicesFromProps.has(ii + stickyOffset)) { +- stickyHeaderIndices.push(cells.length); ++ this.pushOrUnshift(stickyHeaderIndices, cells.length); + } + var shouldListenForLayout = getItemLayout == null || debug || _this._fillRateHelper.enabled(); +- cells.push(/*#__PURE__*/React.createElement(CellRenderer, _extends({ ++ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(CellRenderer, _extends({ + CellRendererComponent: CellRendererComponent, + ItemSeparatorComponent: ii < end ? ItemSeparatorComponent : undefined, + ListItemComponent: ListItemComponent, +@@ -1074,14 +1095,14 @@ class VirtualizedList extends StateSafePureComponent { + // 1. Add cell for ListHeaderComponent + if (ListHeaderComponent) { + if (stickyIndicesFromProps.has(0)) { +- stickyHeaderIndices.push(0); ++ this.pushOrUnshift(stickyHeaderIndices, 0); + } + var _element = /*#__PURE__*/React.isValidElement(ListHeaderComponent) ? ListHeaderComponent : + /*#__PURE__*/ + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + React.createElement(ListHeaderComponent, null); +- cells.push(/*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { ++ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { + cellKey: this._getCellKey() + '-header', + key: "$header" + }, /*#__PURE__*/React.createElement(View +@@ -1105,7 +1126,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + React.createElement(ListEmptyComponent, null); +- cells.push(/*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { ++ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { + cellKey: this._getCellKey() + '-empty', + key: "$empty" + }, /*#__PURE__*/React.cloneElement(_element2, { +@@ -1145,7 +1166,7 @@ class VirtualizedList extends StateSafePureComponent { + var firstMetrics = this.__getFrameMetricsApprox(section.first, this.props); + var lastMetrics = this.__getFrameMetricsApprox(last, this.props); + var spacerSize = lastMetrics.offset + lastMetrics.length - firstMetrics.offset; +- cells.push(/*#__PURE__*/React.createElement(View, { ++ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(View, { + key: "$spacer-" + section.first, + style: { + [spacerKey]: spacerSize +@@ -1168,7 +1189,7 @@ class VirtualizedList extends StateSafePureComponent { + // $FlowFixMe[not-a-component] + // $FlowFixMe[incompatible-type-arg] + React.createElement(ListFooterComponent, null); +- cells.push(/*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { ++ this.pushOrUnshift(cells, /*#__PURE__*/React.createElement(VirtualizedListCellContextProvider, { + cellKey: this._getFooterCellKey(), + key: "$footer" + }, /*#__PURE__*/React.createElement(View, { +@@ -1179,6 +1200,14 @@ class VirtualizedList extends StateSafePureComponent { + _element3))); + } + ++ if (this.props.inverted && stickyHeaderIndices.length > 0) { ++ var totalCells = cells.length; ++ stickyHeaderIndices = stickyHeaderIndices.map(function(recordedIndex) { ++ return totalCells - 1 - recordedIndex; ++ }); ++ } ++ ++ + // 4. Render the ScrollView + var scrollProps = _objectSpread(_objectSpread({}, this.props), {}, { + onContentSizeChange: this._onContentSizeChange, +@@ -1353,7 +1382,7 @@ class VirtualizedList extends StateSafePureComponent { + * suppresses an error found when Flow v0.68 was deployed. To see the + * error delete this comment and run Flow. */ + if (frame.inLayout) { +- framesInLayout.push(frame); ++ this.pushOrUnshift(framesInLayout, frame); + } + } + var windowTop = this.__getFrameMetricsApprox(this.state.cellsAroundViewport.first, this.props).offset; +@@ -1526,6 +1555,12 @@ var styles = StyleSheet.create({ + horizontallyInverted: { + transform: 'scaleX(-1)' + }, ++ rowReverse: { ++ flexDirection: 'row-reverse', ++ }, ++ columnReverse: { ++ flexDirection: 'column-reverse', ++ }, + debug: { + flex: 1 + }, \ No newline at end of file diff --git a/src/components/FlatList/FlatList/index.tsx b/src/components/FlatList/FlatList/index.tsx index 022bc2047acc5..e30c50b128c1d 100644 --- a/src/components/FlatList/FlatList/index.tsx +++ b/src/components/FlatList/FlatList/index.tsx @@ -112,7 +112,7 @@ function MVCPFlatList({ const contentViewLength = contentView.childNodes.length; for (let i = mvcpMinIndexForVisible; i < contentViewLength; i++) { - const subview = contentView.childNodes[i] as HTMLElement; + const subview = contentView.childNodes[restProps.inverted ? contentViewLength - i - 1 : i] as HTMLElement; const subviewOffset = horizontal ? subview.offsetLeft : subview.offsetTop; if (subviewOffset > scrollOffset) { prevFirstVisibleOffsetRef.current = subviewOffset; @@ -120,7 +120,7 @@ function MVCPFlatList({ break; } } - }, [getContentView, getScrollOffset, mvcpMinIndexForVisible, horizontal]); + }, [getContentView, getScrollOffset, mvcpMinIndexForVisible, horizontal, restProps.inverted]); const adjustForMaintainVisibleContentPosition = useCallback( (animated = true) => {