From 8ebed901c18830907fad246eddb179fe124bcebd Mon Sep 17 00:00:00 2001 From: Vit Horacek <36083550+mountiny@users.noreply.github.com> Date: Wed, 3 Sep 2025 16:08:32 +0200 Subject: [PATCH] Revert "[63253] (1/2) Open transaction thread on top" --- src/hooks/useReportScrollManager/index.ts | 15 +-- src/hooks/useReportScrollManager/types.ts | 2 +- src/pages/home/report/ReportActionsList.tsx | 123 ++++++------------ .../useReportUnreadMessageScrollTracking.ts | 2 +- 4 files changed, 47 insertions(+), 95 deletions(-) diff --git a/src/hooks/useReportScrollManager/index.ts b/src/hooks/useReportScrollManager/index.ts index aeb1d0784b27..e5e95b77bbe8 100644 --- a/src/hooks/useReportScrollManager/index.ts +++ b/src/hooks/useReportScrollManager/index.ts @@ -34,16 +34,13 @@ function useReportScrollManager(): ReportScrollManagerData { /** * Scroll to the end of the FlatList. */ - const scrollToEnd = useCallback( - (animated = false) => { - if (!flatListRef?.current) { - return; - } + const scrollToEnd = useCallback(() => { + if (!flatListRef?.current) { + return; + } - flatListRef.current.scrollToEnd({animated}); - }, - [flatListRef], - ); + flatListRef.current.scrollToEnd({animated: false}); + }, [flatListRef]); const scrollToOffset = useCallback( (offset: number) => { diff --git a/src/hooks/useReportScrollManager/types.ts b/src/hooks/useReportScrollManager/types.ts index d3ed0af73eb4..6706f00e1744 100644 --- a/src/hooks/useReportScrollManager/types.ts +++ b/src/hooks/useReportScrollManager/types.ts @@ -4,7 +4,7 @@ type ReportScrollManagerData = { ref: FlatListRefType; scrollToIndex: (index: number, isEditing?: boolean) => void; scrollToBottom: () => void; - scrollToEnd: (animated?: boolean) => void; + scrollToEnd: () => void; scrollToOffset: (offset: number) => void; }; diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index ca83ad0b5b3b..a8c6c139ae43 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -1,11 +1,10 @@ import type {ListRenderItemInfo} from '@react-native/virtualized-lists/Lists/VirtualizedList'; import {useIsFocused, useRoute} from '@react-navigation/native'; import React, {memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; -import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; +import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent} from 'react-native'; import {DeviceEventEmitter, InteractionManager, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {renderScrollComponent} from '@components/ActionSheetAwareScrollView'; -import FlatList from '@components/FlatList'; import InvertedFlatList from '@components/InvertedFlatList'; import {AUTOSCROLL_TO_TOP_THRESHOLD} from '@components/InvertedFlatList/BaseInvertedFlatList'; import {PersonalDetailsContext, usePersonalDetails} from '@components/OnyxListItemProvider'; @@ -131,10 +130,6 @@ let prevReportID: string | null = null; * random enough to avoid collisions */ function keyExtractor(item: OnyxTypes.ReportAction): string { - if (item.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { - return CONST.REPORT.ACTIONS.TYPE.CREATED; - } - return item.reportActionID; } @@ -186,6 +181,7 @@ function ReportActionsList({ const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {canBeMissing: false}); const isTryNewDotNVPDismissed = !!tryNewDot?.classicRedirect?.dismissed; const [isScrollToBottomEnabled, setIsScrollToBottomEnabled] = useState(false); + const [shouldScrollToEndAfterLayout, setShouldScrollToEndAfterLayout] = useState(false); const [actionIdToHighlight, setActionIdToHighlight] = useState(''); useEffect(() => { @@ -201,15 +197,6 @@ function ReportActionsList({ const hasHeaderRendered = useRef(false); const linkedReportActionID = route?.params?.reportActionID; - // FlatList displays items from top to bottom, so we need the oldest actions first. - // Since sortedReportActions and sortedVisibleReportActions are ordered from newest to oldest, - // we use toReversed() to reverse the order when using FlatList, ensuring the oldest action appears at the top. - // InvertedFlatList automatically shows the newest action at the bottom, so no reversal is needed there. - const reportActions = useMemo(() => (isTransactionThread(parentReportAction) ? sortedReportActions.toReversed() : sortedReportActions), [parentReportAction, sortedReportActions]); - const visibleReportActions = useMemo( - () => (isTransactionThread(parentReportAction) ? sortedVisibleReportActions.toReversed() : sortedVisibleReportActions), - [parentReportAction, sortedVisibleReportActions], - ); const lastAction = sortedVisibleReportActions.at(0); const sortedVisibleReportActionsObjects: OnyxTypes.ReportActions = useMemo( () => @@ -349,31 +336,13 @@ function ReportActionsList({ currentVerticalScrollingOffsetRef: scrollingVerticalOffset, readActionSkippedRef: readActionSkipped, unreadMarkerReportActionIndex, - isInverted: !isTransactionThread(parentReportAction), + isInverted: true, onTrackScrolling: (event: NativeSyntheticEvent) => { - if (isTransactionThread(parentReportAction)) { - // For transaction threads, calculate distance from bottom like MoneyRequestReportActionsList - const {layoutMeasurement, contentSize, contentOffset} = event.nativeEvent; - scrollingVerticalOffset.current = contentSize.height - layoutMeasurement.height - contentOffset.y; - } else { - // For regular reports (InvertedFlatList), use raw contentOffset.y - scrollingVerticalOffset.current = event.nativeEvent.contentOffset.y; - } + scrollingVerticalOffset.current = event.nativeEvent.contentOffset.y; onScroll?.(event); }, }); - const scrollToBottom = useCallback(() => { - if (isTransactionThread(parentReportAction)) { - // In a transaction thread, we want to scroll to the end of the list - reportScrollManager.scrollToEnd(true); - return; - } - - // On remaining reports, we want to scroll to the bottom of the inverted FlatList which is the top of the list (offset: 0) - reportScrollManager.scrollToBottom(); - }, [parentReportAction, reportScrollManager]); - useEffect(() => { if ( scrollingVerticalOffset.current < AUTOSCROLL_TO_TOP_THRESHOLD && @@ -382,20 +351,11 @@ function ReportActionsList({ hasNewestReportAction ) { setIsFloatingMessageCounterVisible(false); - scrollToBottom(); + reportScrollManager.scrollToBottom(); } previousLastIndex.current = lastActionIndex; reportActionSize.current = sortedVisibleReportActions.length; - }, [ - lastActionIndex, - sortedVisibleReportActions, - reportScrollManager, - hasNewestReportAction, - linkedReportActionID, - setIsFloatingMessageCounterVisible, - parentReportAction, - scrollToBottom, - ]); + }, [lastActionIndex, sortedVisibleReportActions, reportScrollManager, hasNewestReportAction, linkedReportActionID, setIsFloatingMessageCounterVisible]); useEffect(() => { userActiveSince.current = DateUtils.getDBTime(); @@ -429,10 +389,18 @@ function ReportActionsList({ return; } + const shouldScrollToEnd = + (isExpenseReport(report) || isTransactionThread(parentReportAction)) && isSearchTopmostFullScreenRoute() && hasNewestReportAction && !unreadMarkerReportActionID; + + if (shouldScrollToEnd) { + setShouldScrollToEndAfterLayout(true); + } + InteractionManager.runAfterInteractions(() => { setIsFloatingMessageCounterVisible(false); - if (!isTransactionThread(parentReportAction)) { - scrollToBottom(); + + if (!shouldScrollToEnd) { + reportScrollManager.scrollToBottom(); } }); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps @@ -447,10 +415,10 @@ function ReportActionsList({ const prevSorted = lastAction?.reportActionID ? prevSortedVisibleReportActionsObjects[lastAction?.reportActionID] : null; if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_TRACK_EXPENSE_WHISPER && !prevSorted) { InteractionManager.runAfterInteractions(() => { - scrollToBottom(); + reportScrollManager.scrollToBottom(); }); } - }, [lastAction, parentReportAction, prevSortedVisibleReportActionsObjects, reportScrollManager, scrollToBottom]); + }, [lastAction, prevSortedVisibleReportActionsObjects, reportScrollManager]); const scrollToBottomForCurrentUserAction = useCallback( (isFromCurrentUser: boolean, action?: OnyxTypes.ReportAction) => { @@ -477,20 +445,20 @@ function ReportActionsList({ }, 100); } else { setIsFloatingMessageCounterVisible(false); - scrollToBottom(); + reportScrollManager.scrollToBottom(); } if (action?.reportActionID) { setActionIdToHighlight(action.reportActionID); } } else { setIsFloatingMessageCounterVisible(false); - scrollToBottom(); + reportScrollManager.scrollToBottom(); } setIsScrollToBottomEnabled(true); }); }, - [sortedVisibleReportActions, report.reportID, reportScrollManager, setIsFloatingMessageCounterVisible, scrollToBottom], + [report.reportID, reportScrollManager, setIsFloatingMessageCounterVisible, sortedVisibleReportActions], ); // Clear the highlighted report action after scrolling and highlighting @@ -545,10 +513,10 @@ function ReportActionsList({ return; } InteractionManager.runAfterInteractions(() => { - scrollToBottom(); + reportScrollManager.scrollToBottom(); }); // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [lastAction, scrollToBottom]); + }, [lastAction]); const scrollToBottomAndMarkReportAsRead = useCallback(() => { setIsFloatingMessageCounterVisible(false); @@ -564,13 +532,13 @@ function ReportActionsList({ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID)); } openReport(report.reportID); - scrollToBottom(); + reportScrollManager.scrollToBottom(); return; } - scrollToBottom(); + reportScrollManager.scrollToBottom(); readActionSkipped.current = false; readNewestAction(report.reportID); - }, [setIsFloatingMessageCounterVisible, hasNewestReportAction, scrollToBottom, report.reportID]); + }, [setIsFloatingMessageCounterVisible, hasNewestReportAction, reportScrollManager, report.reportID]); /** * Calculates the ideal number of report actions to render in the first render, based on the screen height and on @@ -657,7 +625,6 @@ function ReportActionsList({ const renderItem = useCallback( ({item: reportAction, index}: ListRenderItemInfo) => { - const actionIndex = isTransactionThread(parentReportAction) ? sortedVisibleReportActions.length - index - 1 : index; const originalReportID = getOriginalReportID(report.reportID, reportAction); const reportDraftMessages = draftMessage?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${originalReportID}`]; const matchingDraftMessage = reportDraftMessages?.[reportAction.reportActionID]; @@ -673,7 +640,7 @@ function ReportActionsList({ allReports={allReports} policies={policies} reportAction={reportAction} - reportActions={reportActions} + reportActions={sortedReportActions} parentReportAction={parentReportAction} parentReportActionForTransactionThread={parentReportActionForTransactionThread} index={index} @@ -681,8 +648,8 @@ function ReportActionsList({ transactionThreadReport={transactionThreadReport} linkedReportActionID={linkedReportActionID} displayAsGroup={ - !isConsecutiveChronosAutomaticTimerAction(sortedVisibleReportActions, actionIndex, chatIncludesChronosWithID(reportAction?.reportID)) && - isConsecutiveActionMadeByPreviousActor(sortedVisibleReportActions, actionIndex) + !isConsecutiveChronosAutomaticTimerAction(sortedVisibleReportActions, index, chatIncludesChronosWithID(reportAction?.reportID)) && + isConsecutiveActionMadeByPreviousActor(sortedVisibleReportActions, index) } mostRecentIOUReportActionID={mostRecentIOUReportActionID} shouldHideThreadDividerLine={shouldHideThreadDividerLine} @@ -706,11 +673,11 @@ function ReportActionsList({ ); }, [ - reportActions, draftMessage, emojiReactions, allReports, policies, + sortedReportActions, parentReportAction, parentReportActionForTransactionThread, report, @@ -746,11 +713,15 @@ function ReportActionsList({ (event: LayoutChangeEvent) => { onLayout(event); if (isScrollToBottomEnabled) { - scrollToBottom(); + reportScrollManager.scrollToBottom(); setIsScrollToBottomEnabled(false); } + if (shouldScrollToEndAfterLayout) { + reportScrollManager.scrollToEnd(); + setShouldScrollToEndAfterLayout(false); + } }, - [isScrollToBottomEnabled, onLayout, scrollToBottom], + [isScrollToBottomEnabled, onLayout, reportScrollManager, shouldScrollToEndAfterLayout], ); const retryLoadNewerChatsError = useCallback(() => { @@ -796,22 +767,6 @@ function ReportActionsList({ loadOlderChats(false); }, [loadOlderChats]); - // FlatList is used for transaction threads to keep the list scrolled at the top and display actions in chronological order. - // InvertedFlatList is used for regular reports, always scrolling to the bottom initially and showing the newest messages at the bottom. - const ListComponent = isTransactionThread(parentReportAction) ? FlatList : InvertedFlatList; - const contentContainerStyle = useMemo(() => { - const baseStyles: StyleProp = [styles.chatContentScrollView]; - - if (isTransactionThread(parentReportAction)) { - // InvertedFlatList applies a scale: -1 transform, so top padding becomes bottom padding and vice versa. - // When using FlatList for transaction threads, we need to manually add top padding (pt4) and remove bottom padding (pb0) - // to maintain consistent spacing and visual appearance at the top of the list. - baseStyles.push(styles.pb0, styles.pt4); - } - - return baseStyles; - }, [parentReportAction, styles.chatContentScrollView, styles.pb0, styles.pt4]); - return ( <> - = minIndex : unreadActionIndex < maxIndex; + const unreadActionVisible = isInverted ? unreadActionIndex >= minIndex : unreadActionIndex <= maxIndex; // display floating button if the unread report action is out of view if (!unreadActionVisible && hasUnreadMarkerReportAction) {