From 8ded0153a2f44e3d93da6a7dcd6f4e95f49e9bd3 Mon Sep 17 00:00:00 2001 From: Ben Limpich Date: Fri, 27 Feb 2026 11:18:43 -0800 Subject: [PATCH 1/2] Revert "Merge pull request #76104 from margelo/@chrispader/allow-multiple-loads-in-useLoadReportActions-hook" This reverts commit 3e2b068a138d956b23fe5e77f65d84540116e5e2, reversing changes made to 1b628d70cac107c150d37cd10e2aa15b81ec490a. --- src/hooks/useLoadReportActions.ts | 52 ++++++++++++------- src/libs/API/index.ts | 6 ++- src/libs/API/parameters/OpenReportParams.ts | 1 - src/libs/actions/Report/index.ts | 7 ++- src/pages/inbox/report/ReportActionsList.tsx | 24 +++++++-- src/pages/inbox/report/ReportActionsView.tsx | 1 + .../index.ts | 2 +- tests/ui/PaginationTest.tsx | 11 ++-- tests/unit/useLoadReportActionsTest.tsx | 1 + 9 files changed, 66 insertions(+), 39 deletions(-) diff --git a/src/hooks/useLoadReportActions.ts b/src/hooks/useLoadReportActions.ts index 6eb00bcec4af..14755d8655c2 100644 --- a/src/hooks/useLoadReportActions.ts +++ b/src/hooks/useLoadReportActions.ts @@ -1,5 +1,5 @@ import {useIsFocused} from '@react-navigation/native'; -import {useCallback, useMemo} from 'react'; +import {useCallback, useMemo, useRef} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {getNewerActions, getOlderActions} from '@userActions/Report'; import CONST from '@src/CONST'; @@ -11,6 +11,9 @@ type UseLoadReportActionsArguments = { /** The id of the current report */ reportID: string; + /** The id of the reportAction (if specific action was linked to */ + reportActionID?: string; + /** The list of reportActions linked to the current report */ reportActions: ReportAction[]; @@ -31,14 +34,16 @@ type UseLoadReportActionsArguments = { * Provides reusable logic to get the functions for loading older/newer reportActions. * Used in the report displaying components */ -function useLoadReportActions({reportID, reportActions, allReportActionIDs, transactionThreadReport, hasOlderActions, hasNewerActions}: UseLoadReportActionsArguments) { +function useLoadReportActions({reportID, reportActionID, reportActions, allReportActionIDs, transactionThreadReport, hasOlderActions, hasNewerActions}: UseLoadReportActionsArguments) { + const didLoadOlderChats = useRef(false); + const didLoadNewerChats = useRef(false); + const {isOffline} = useNetwork(); const isFocused = useIsFocused(); + const newestReportAction = useMemo(() => reportActions?.at(0), [reportActions]); const oldestReportAction = useMemo(() => reportActions?.at(-1), [reportActions]); - const isTransactionThreadReport = !isEmptyObject(transactionThreadReport); - // Track oldest/newest actions per report in a single pass const {currentReportOldest, currentReportNewest, transactionThreadOldest, transactionThreadNewest} = useMemo(() => { let currentReportNewestAction = null; @@ -61,7 +66,7 @@ function useLoadReportActions({reportID, reportActions, allReportActionIDs, tran } // Oldest = last matching action we encounter currentReportOldestAction = action; - } else if (isTransactionThreadReport && transactionThreadReport?.reportID === targetReportID) { + } else if (!isEmptyObject(transactionThreadReport) && transactionThreadReport?.reportID === targetReportID) { // Same logic for transaction thread if (!transactionThreadNewestAction) { transactionThreadNewestAction = action; @@ -76,7 +81,7 @@ function useLoadReportActions({reportID, reportActions, allReportActionIDs, tran transactionThreadOldest: transactionThreadOldestAction, transactionThreadNewest: transactionThreadNewestAction, }; - }, [allReportActionIDs, isTransactionThreadReport, reportActions, reportID, transactionThreadReport?.reportID]); + }, [reportActions, allReportActionIDs, reportID, transactionThreadReport]); /** * Retrieves the next set of reportActions for the chat once we are nearing the end of what we are currently @@ -94,40 +99,37 @@ function useLoadReportActions({reportID, reportActions, allReportActionIDs, tran return; } - if (isTransactionThreadReport) { + didLoadOlderChats.current = true; + + if (!isEmptyObject(transactionThreadReport)) { getOlderActions(reportID, currentReportOldest?.reportActionID); - getOlderActions(transactionThreadReport?.reportID, transactionThreadOldest?.reportActionID); + getOlderActions(transactionThreadReport.reportID, transactionThreadOldest?.reportActionID); } else { getOlderActions(reportID, currentReportOldest?.reportActionID); } }, - [ - currentReportOldest?.reportActionID, - hasOlderActions, - isOffline, - isTransactionThreadReport, - oldestReportAction, - reportID, - transactionThreadOldest?.reportActionID, - transactionThreadReport?.reportID, - ], + [isOffline, oldestReportAction, hasOlderActions, transactionThreadReport, reportID, currentReportOldest?.reportActionID, transactionThreadOldest?.reportActionID], ); const loadNewerChats = useCallback( (force = false) => { if ( !force && - (!isFocused || + (!reportActionID || + !isFocused || !newestReportAction || !hasNewerActions || isOffline || // If there was an error only try again once on initial mount. We should also still load // more in case we have cached messages. + didLoadNewerChats.current || newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) ) { return; } + didLoadNewerChats.current = true; + if (!isEmptyObject(transactionThreadReport)) { getNewerActions(reportID, currentReportNewest?.reportActionID); getNewerActions(transactionThreadReport.reportID, transactionThreadNewest?.reportActionID); @@ -135,7 +137,17 @@ function useLoadReportActions({reportID, reportActions, allReportActionIDs, tran getNewerActions(reportID, newestReportAction.reportActionID); } }, - [currentReportNewest?.reportActionID, hasNewerActions, isFocused, isOffline, newestReportAction, reportID, transactionThreadNewest?.reportActionID, transactionThreadReport], + [ + reportActionID, + isFocused, + newestReportAction, + hasNewerActions, + isOffline, + transactionThreadReport, + reportID, + currentReportNewest?.reportActionID, + transactionThreadNewest?.reportActionID, + ], ); return { diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts index 50c959552f08..e1b9419a0f62 100644 --- a/src/libs/API/index.ts +++ b/src/libs/API/index.ts @@ -311,11 +311,13 @@ function paginate processRequest(request, type)); + waitForWrites(command as ReadCommand).then(() => processRequest(request, type)); + return; default: throw new Error('Unknown API request type'); } diff --git a/src/libs/API/parameters/OpenReportParams.ts b/src/libs/API/parameters/OpenReportParams.ts index 7c3ab73bab06..aa58f4c9f677 100644 --- a/src/libs/API/parameters/OpenReportParams.ts +++ b/src/libs/API/parameters/OpenReportParams.ts @@ -26,7 +26,6 @@ type OpenReportParams = { */ moneyRequestPreviewReportActionID?: string; includePartiallySetupBankAccounts?: boolean; - useLastUnreadReportAction?: boolean; }; export default OpenReportParams; diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index ca315fcabdf4..d1ddf2840cbb 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -1287,7 +1287,6 @@ function openReport( parentReportActionID, transactionID: transaction?.transactionID, includePartiallySetupBankAccounts: true, - useLastUnreadReportAction: true, }; if (optimisticSelfDMReport) { @@ -2028,7 +2027,7 @@ function explain( */ function getOlderActions(reportID: string | undefined, reportActionID: string | undefined) { if (!reportID || !reportActionID) { - return Promise.resolve(); + return; } const optimisticData: Array> = [ @@ -2068,7 +2067,7 @@ function getOlderActions(reportID: string | undefined, reportActionID: string | reportActionID, }; - return API.paginate( + API.paginate( CONST.API_REQUEST_TYPE.READ, READ_COMMANDS.GET_OLDER_ACTIONS, parameters, @@ -2126,7 +2125,7 @@ function getNewerActions(reportID: string | undefined, reportActionID: string | reportActionID, }; - return API.paginate( + API.paginate( CONST.API_REQUEST_TYPE.READ, READ_COMMANDS.GET_NEWER_ACTIONS, parameters, diff --git a/src/pages/inbox/report/ReportActionsList.tsx b/src/pages/inbox/report/ReportActionsList.tsx index 7fc2925c5fdc..0aab7891d010 100644 --- a/src/pages/inbox/report/ReportActionsList.tsx +++ b/src/pages/inbox/report/ReportActionsList.tsx @@ -11,6 +11,7 @@ import InvertedFlatList from '@components/FlatList/InvertedFlatList'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import {AUTOSCROLL_TO_TOP_THRESHOLD} from '@hooks/useFlatListScrollKey'; import useIsAnonymousUser from '@hooks/useIsAnonymousUser'; import useLocalize from '@hooks/useLocalize'; import useNetworkWithOfflineStatus from '@hooks/useNetworkWithOfflineStatus'; @@ -344,10 +345,13 @@ function ReportActionsList({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [lastAction?.created]); + const lastActionIndex = lastAction?.reportActionID; + const reportActionSize = useRef(sortedVisibleReportActions.length); const lastVisibleActionCreated = getReportLastVisibleActionCreated(report, transactionThreadReport); const hasNewestReportAction = lastAction?.created === lastVisibleActionCreated || isReportPreviewAction(lastAction); const hasNewestReportActionRef = useRef(hasNewestReportAction); hasNewestReportActionRef.current = hasNewestReportAction; + const previousLastIndex = useRef(lastActionIndex); const sortedVisibleReportActionsRef = useRef(sortedVisibleReportActions); const {isFloatingMessageCounterVisible, setIsFloatingMessageCounterVisible, trackVerticalScrolling, onViewableItemsChanged} = useReportUnreadMessageScrollTracking({ @@ -366,6 +370,20 @@ function ReportActionsList({ hasOnceLoadedReportActions: !!reportMetadata?.hasOnceLoadedReportActions, }); + useEffect(() => { + if ( + scrollOffsetRef.current < AUTOSCROLL_TO_TOP_THRESHOLD && + previousLastIndex.current !== lastActionIndex && + reportActionSize.current !== sortedVisibleReportActions.length && + hasNewestReportAction + ) { + setIsFloatingMessageCounterVisible(false); + reportScrollManager.scrollToBottom(); + } + previousLastIndex.current = lastActionIndex; + reportActionSize.current = sortedVisibleReportActions.length; + }, [lastActionIndex, sortedVisibleReportActions.length, reportScrollManager, hasNewestReportAction, linkedReportActionID, setIsFloatingMessageCounterVisible, scrollOffsetRef]); + useEffect(() => { const shouldTriggerScroll = shouldFocusToTopOnMount && prevHasCreatedActionAdded && !hasCreatedActionAdded; if (!shouldTriggerScroll) { @@ -526,13 +544,9 @@ function ReportActionsList({ if (action?.reportActionID) { setActionIdToHighlight(action.reportActionID); } - } else if (Navigation.getReportRHPActiveRoute()) { + } else { setIsFloatingMessageCounterVisible(false); reportScrollManager.scrollToBottom(); - } else { - Navigation.setNavigationActionToMicrotaskQueue(() => { - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID)); - }); } setIsScrollToBottomEnabled(true); diff --git a/src/pages/inbox/report/ReportActionsView.tsx b/src/pages/inbox/report/ReportActionsView.tsx index da9bb5d5f1ff..8202baf851d8 100755 --- a/src/pages/inbox/report/ReportActionsView.tsx +++ b/src/pages/inbox/report/ReportActionsView.tsx @@ -265,6 +265,7 @@ function ReportActionsView({ const {loadOlderChats, loadNewerChats} = useLoadReportActions({ reportID, + reportActionID, reportActions, allReportActionIDs, transactionThreadReport, diff --git a/src/styles/utils/chatContentScrollViewPlatformStyles/index.ts b/src/styles/utils/chatContentScrollViewPlatformStyles/index.ts index 6ef204b0acd7..cef1d5e70e6e 100644 --- a/src/styles/utils/chatContentScrollViewPlatformStyles/index.ts +++ b/src/styles/utils/chatContentScrollViewPlatformStyles/index.ts @@ -1,7 +1,7 @@ import type ChatContentScrollViewPlatformStyles from './types'; const chatContentScrollViewPlatformStyles: ChatContentScrollViewPlatformStyles = { - overflow: 'clip', + overflow: 'hidden', }; export default chatContentScrollViewPlatformStyles; diff --git a/tests/ui/PaginationTest.tsx b/tests/ui/PaginationTest.tsx index 07474755fdc1..d2bebf6fca7a 100644 --- a/tests/ui/PaginationTest.tsx +++ b/tests/ui/PaginationTest.tsx @@ -376,9 +376,6 @@ describe('Pagination', () => { // Here we have 5 messages from the initial OpenReport and 5 from the initial GetNewerActions. expect(getReportActions()).toHaveLength(10); - // Simulate the backend returning no new messages to simulate reaching the start of the chat. - mockGetNewerActions(0); - // There is 1 extra call here because of the comment linking report. TestHelper.expectAPICommandToHaveBeenCalled('OpenReport', 3); TestHelper.expectAPICommandToHaveBeenCalledWith('OpenReport', 1, {reportID: REPORT_ID, reportActionID: '5'}); @@ -393,20 +390,22 @@ describe('Pagination', () => { TestHelper.expectAPICommandToHaveBeenCalled('OpenReport', 3); TestHelper.expectAPICommandToHaveBeenCalled('GetOlderActions', 0); - TestHelper.expectAPICommandToHaveBeenCalled('GetNewerActions', 2); + TestHelper.expectAPICommandToHaveBeenCalled('GetNewerActions', 1); // We now have 10 messages. 5 from the initial OpenReport and 5 from the GetNewerActions call. expect(getReportActions()).toHaveLength(10); + // Simulate the backend returning no new messages to simulate reaching the start of the chat. + mockGetNewerActions(0); + scrollToOffset(500); await waitForBatchedUpdatesWithAct(); scrollToOffset(0); await waitForBatchedUpdatesWithAct(); - // When there are no newer actions, we don't want to trigger GetNewerActions again. TestHelper.expectAPICommandToHaveBeenCalled('OpenReport', 3); TestHelper.expectAPICommandToHaveBeenCalled('GetOlderActions', 0); - TestHelper.expectAPICommandToHaveBeenCalled('GetNewerActions', 2); + TestHelper.expectAPICommandToHaveBeenCalled('GetNewerActions', 1); // We still have 15 messages. 5 from the initial OpenReport and 5 from the GetNewerActions call. expect(getReportActions()).toHaveLength(10); diff --git a/tests/unit/useLoadReportActionsTest.tsx b/tests/unit/useLoadReportActionsTest.tsx index 871d57c808a1..2f743e78d0cd 100644 --- a/tests/unit/useLoadReportActionsTest.tsx +++ b/tests/unit/useLoadReportActionsTest.tsx @@ -124,6 +124,7 @@ describe('useLoadReportActions', () => { const props = { ...baseProps, hasNewerActions: true, + reportActionID: 'EXISTING_ACTION_ID', }; const {result} = renderHook(() => useLoadReportActions(props)); From a48f764cd6b38322cb0aeb24613315fdb7db9365 Mon Sep 17 00:00:00 2001 From: Ben Limpich Date: Fri, 27 Feb 2026 13:23:51 -0800 Subject: [PATCH 2/2] fix: update useFlatListScrollKey import path after PR #70378 moved the file Co-Authored-By: Claude Opus 4.6 --- src/pages/inbox/report/ReportActionsList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/inbox/report/ReportActionsList.tsx b/src/pages/inbox/report/ReportActionsList.tsx index 0aab7891d010..dd7af9f0f806 100644 --- a/src/pages/inbox/report/ReportActionsList.tsx +++ b/src/pages/inbox/report/ReportActionsList.tsx @@ -7,11 +7,11 @@ import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent} from 'r import {DeviceEventEmitter, InteractionManager, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {renderScrollComponent as renderActionSheetAwareScrollView} from '@components/ActionSheetAwareScrollView'; +import {AUTOSCROLL_TO_TOP_THRESHOLD} from '@components/FlatList/hooks/useFlatListScrollKey'; import InvertedFlatList from '@components/FlatList/InvertedFlatList'; import {usePersonalDetails} from '@components/OnyxListItemProvider'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import {AUTOSCROLL_TO_TOP_THRESHOLD} from '@hooks/useFlatListScrollKey'; import useIsAnonymousUser from '@hooks/useIsAnonymousUser'; import useLocalize from '@hooks/useLocalize'; import useNetworkWithOfflineStatus from '@hooks/useNetworkWithOfflineStatus';