From 0f1990cb6c89b9c4d35d447fe88860df8c768eda Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Tue, 19 May 2026 10:35:40 +0200 Subject: [PATCH 1/2] extract useReportActionsVisibility hook Co-Authored-By: Claude Opus 4.7 (1M context) --- src/hooks/useReportActionsVisibility.ts | 111 ++++++++++++++++++ .../useTransactionsAndViolationsForReport.ts | 2 +- src/pages/inbox/report/ReportActionsView.tsx | 100 +++------------- 3 files changed, 130 insertions(+), 83 deletions(-) create mode 100644 src/hooks/useReportActionsVisibility.ts diff --git a/src/hooks/useReportActionsVisibility.ts b/src/hooks/useReportActionsVisibility.ts new file mode 100644 index 000000000000..a0d5d6f7f4f7 --- /dev/null +++ b/src/hooks/useReportActionsVisibility.ts @@ -0,0 +1,111 @@ +import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils'; +import {isCreatedAction, isDeletedParentAction, isIOUActionMatchingTransactionList, isReportActionVisible} from '@libs/ReportActionsUtils'; +import {isConciergeChatReport} from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReportAction} from '@src/types/onyx'; +import useConciergeSidePanelReportActions from './useConciergeSidePanelReportActions'; +import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; +import useIsInSidePanel from './useIsInSidePanel'; +import useLocalize from './useLocalize'; +import useNetwork from './useNetwork'; +import useOnyx from './useOnyx'; +import useSidePanelState from './useSidePanelState'; +import useTransactionsAndViolationsForReport from './useTransactionsAndViolationsForReport'; + +type UseReportActionsVisibilityParams = { + reportID: string | undefined; + reportActions: ReportAction[]; + allReportActions: ReportAction[]; + canPerformWriteAction: boolean; + hasOlderActions: boolean; + loadOlderChats: (force?: boolean) => void; +}; + +type UseReportActionsVisibilityResult = { + sortedReportActions: ReportAction[]; + sortedVisibleReportActions: ReportAction[]; + isConciergeSidePanel: boolean; + showConciergeSidePanelWelcome: boolean; + showFullHistory: boolean; + hasPreviousMessages: boolean; + handleShowPreviousMessages: () => void; +}; + +function useReportActionsVisibility({ + reportID, + reportActions, + allReportActions, + canPerformWriteAction, + hasOlderActions, + loadOlderChats, +}: UseReportActionsVisibilityParams): UseReportActionsVisibilityResult { + const {isOffline} = useNetwork(); + const {translate} = useLocalize(); + const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); + + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS); + + const isInSidePanel = useIsInSidePanel(); + const isConciergeSidePanel = isInSidePanel && isConciergeChatReport(report, conciergeReportID); + + const {sessionStartTime} = useSidePanelState(); + + const hasUserSentMessage = + isConciergeSidePanel && sessionStartTime + ? allReportActions.some((action) => !isCreatedAction(action) && action.actorAccountID === currentUserAccountID && action.created >= sessionStartTime) + : false; + + const {transactions: reportTransactions, isLoaded: areTransactionsLoaded} = useTransactionsAndViolationsForReport(reportID); + // When transactions haven't loaded yet, pass undefined to skip IOU filtering entirely + // (undefined = "don't filter" in isIOUActionMatchingTransactionList). + // Once loaded, filter normally — even if transactions is empty (genuinely no transactions). + const reportTransactionIDs = areTransactionsLoaded ? getAllNonDeletedTransactions(reportTransactions, allReportActions ?? []).map((transaction) => transaction.transactionID) : undefined; + + const visibleReportActions = reportActions.filter((reportAction) => { + const passesOfflineCheck = isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors; + + if (!passesOfflineCheck) { + return false; + } + + const actionReportID = reportAction.reportID ?? reportID; + if (!isReportActionVisible(reportAction, actionReportID, canPerformWriteAction, visibleReportActionsData)) { + return false; + } + + if (!isIOUActionMatchingTransactionList(reportAction, reportTransactionIDs)) { + return false; + } + + return true; + }); + + const {filteredVisibleActions, filteredReportActions, showConciergeSidePanelWelcome, showFullHistory, hasPreviousMessages, handleShowPreviousMessages} = + useConciergeSidePanelReportActions({ + report, + reportActions, + visibleReportActions, + isConciergeSidePanel, + hasUserSentMessage, + hasOlderActions, + sessionStartTime, + currentUserAccountID, + greetingText: translate('common.concierge.sidePanelGreeting'), + loadOlderChats, + }); + + return { + sortedReportActions: filteredReportActions, + sortedVisibleReportActions: filteredVisibleActions, + isConciergeSidePanel, + showConciergeSidePanelWelcome, + showFullHistory, + hasPreviousMessages, + handleShowPreviousMessages, + }; +} + +export default useReportActionsVisibility; diff --git a/src/hooks/useTransactionsAndViolationsForReport.ts b/src/hooks/useTransactionsAndViolationsForReport.ts index ae2a4397cb05..7e5e9fdbb8cd 100644 --- a/src/hooks/useTransactionsAndViolationsForReport.ts +++ b/src/hooks/useTransactionsAndViolationsForReport.ts @@ -23,7 +23,7 @@ function useTransactionsAndViolationsForReport(reportID?: string) { filteredViolations[transactionViolationKey] = getTransactionViolations(transaction, violations, currentUserDetails.email ?? '', currentUserDetails.accountID, report, policy) ?? []; } - return {transactions, violations: filteredViolations}; + return {transactions, violations: filteredViolations, isLoaded: allReportsTransactionsAndViolations !== undefined}; } export default useTransactionsAndViolationsForReport; diff --git a/src/pages/inbox/report/ReportActionsView.tsx b/src/pages/inbox/report/ReportActionsView.tsx index b4ef1d4f1c14..d787e2e97922 100755 --- a/src/pages/inbox/report/ReportActionsView.tsx +++ b/src/pages/inbox/report/ReportActionsView.tsx @@ -2,28 +2,20 @@ import {useRoute} from '@react-navigation/native'; import React, {useEffect, useMemo, useRef} from 'react'; import type {LayoutChangeEvent} from 'react-native'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; -import useConciergeSidePanelReportActions from '@hooks/useConciergeSidePanelReportActions'; import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; -import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; -import useIsInSidePanel from '@hooks/useIsInSidePanel'; import useLoadReportActions from '@hooks/useLoadReportActions'; -import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useParentReportAction from '@hooks/useParentReportAction'; import usePendingConciergeResponse from '@hooks/usePendingConciergeResponse'; import useReportActionsPagination from '@hooks/useReportActionsPagination'; +import useReportActionsVisibility from '@hooks/useReportActionsVisibility'; import useReportIsArchived from '@hooks/useReportIsArchived'; -import useSidePanelState from '@hooks/useSidePanelState'; -import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport'; import {getReportPreviewAction} from '@libs/actions/IOU/MoneyRequestBuilder'; import {updateLoadingInitialReportAction} from '@libs/actions/Report'; -import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils'; -import {isCreatedAction, isDeletedParentAction, isIOUActionMatchingTransactionList, isReportActionVisible} from '@libs/ReportActionsUtils'; -import {canUserPerformWriteAction, isConciergeChatReport, isReportTransactionThread as isReportTransactionThreadUtil, isUnread} from '@libs/ReportUtils'; +import {canUserPerformWriteAction, isReportTransactionThread as isReportTransactionThreadUtil, isUnread} from '@libs/ReportUtils'; import markOpenReportEnd from '@libs/telemetry/markOpenReportEnd'; import type ReportScreenNavigationProps from '@pages/inbox/types'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import ReportActionsList from './ReportActionsList'; @@ -42,9 +34,7 @@ function ReportActionsView({reportID, onLayout}: ReportActionsViewProps) { const reportActionIDFromRoute = route?.params?.reportActionID; useCopySelectionHelper(); - const {translate} = useLocalize(); usePendingConciergeResponse(reportID); - const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const {isOffline} = useNetwork(); const [report, reportResult] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); @@ -72,26 +62,12 @@ function ReportActionsView({reportID, onLayout}: ReportActionsViewProps) { const isLoadingInitialReportActions = reportLoadingState?.isLoadingInitialReportActions; const hasOnceLoadedReportActions = reportLoadingState?.hasOnceLoadedReportActions; - const isInSidePanel = useIsInSidePanel(); - const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); - const isConciergeSidePanel = isInSidePanel && isConciergeChatReport(report, conciergeReportID); - - const {sessionStartTime} = useSidePanelState(); - - const hasUserSentMessage = useMemo(() => { - if (!isConciergeSidePanel || !sessionStartTime) { - return false; - } - return allReportActions.some((action) => !isCreatedAction(action) && action.actorAccountID === currentUserAccountID && action.created >= sessionStartTime); - }, [isConciergeSidePanel, allReportActions, currentUserAccountID, sessionStartTime]); - const isReportTransactionThread = isReportTransactionThreadUtil(report); const isReportArchived = useReportIsArchived(reportID); const canPerformWriteAction = !!canUserPerformWriteAction(report, isReportArchived); const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP); - const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS); const reportPreviewAction = useMemo(() => getReportPreviewAction(report?.chatReportID, report?.reportID), [report?.chatReportID, report?.reportID]); const didLayout = useRef(false); @@ -99,12 +75,6 @@ function ReportActionsView({reportID, onLayout}: ReportActionsViewProps) { didLayout.current = false; }, [reportID]); - const {transactions: reportTransactions} = useTransactionsAndViolationsForReport(reportID); - const reportTransactionIDs = useMemo( - () => getAllNonDeletedTransactions(reportTransactions, allReportActions ?? []).map((transaction) => transaction.transactionID), - [reportTransactions, allReportActions], - ); - useEffect(() => { // When we linked to message - we do not need to wait for initial actions - they already exists if (!reportActionIDFromRoute || !isOffline) { @@ -116,35 +86,6 @@ function ReportActionsView({reportID, onLayout}: ReportActionsViewProps) { // Remount the list when the deep-linked message or unread anchor changes (scroll positioning), or when the report changes. const listID = [reportID, reportActionIDFromRoute, hasOnceLoadedReportActions ? undefined : oldestUnreadReportAction?.reportActionID].join(':'); - const visibleReportActions = useMemo( - () => - reportActions.filter((reportAction) => { - const passesOfflineCheck = - isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors; - - if (!passesOfflineCheck) { - return false; - } - - const actionReportID = reportAction.reportID ?? reportID; - if (!isReportActionVisible(reportAction, actionReportID, canPerformWriteAction, visibleReportActionsData)) { - return false; - } - - if (!isIOUActionMatchingTransactionList(reportAction, reportTransactionIDs)) { - return false; - } - - return true; - }), - [canPerformWriteAction, isOffline, reportActions, reportID, reportTransactionIDs, visibleReportActionsData], - ); - - const isSingleExpenseReport = reportPreviewAction?.childMoneyRequestCount === 1; - const isMissingTransactionThreadReportID = !transactionThreadReport?.reportID; - const isReportDataIncomplete = isSingleExpenseReport && isMissingTransactionThreadReportID; - const isMissingReportActions = visibleReportActions.length === 0; - const {loadOlderChats, loadNewerChats} = useLoadReportActions({ reportID, reportActions, @@ -154,25 +95,20 @@ function ReportActionsView({reportID, onLayout}: ReportActionsViewProps) { hasNewerActions, }); - const { - filteredVisibleActions: conciergeSidePanelFilteredVisibleActions, - filteredReportActions: conciergeSidePanelFilteredReportActions, - showConciergeSidePanelWelcome, - showFullHistory, - hasPreviousMessages, - handleShowPreviousMessages, - } = useConciergeSidePanelReportActions({ - report, - reportActions, - visibleReportActions, - isConciergeSidePanel, - hasUserSentMessage, - hasOlderActions, - sessionStartTime, - currentUserAccountID, - greetingText: translate('common.concierge.sidePanelGreeting'), - loadOlderChats, - }); + const {sortedReportActions, sortedVisibleReportActions, isConciergeSidePanel, showConciergeSidePanelWelcome, showFullHistory, hasPreviousMessages, handleShowPreviousMessages} = + useReportActionsVisibility({ + reportID, + reportActions, + allReportActions, + canPerformWriteAction, + hasOlderActions, + loadOlderChats, + }); + + const isSingleExpenseReport = reportPreviewAction?.childMoneyRequestCount === 1; + const isMissingTransactionThreadReportID = !transactionThreadReport?.reportID; + const isReportDataIncomplete = isSingleExpenseReport && isMissingTransactionThreadReportID; + const isMissingReportActions = sortedVisibleReportActions.length === 0; /** * Runs when the FlatList finishes laying out @@ -244,8 +180,8 @@ function ReportActionsView({reportID, onLayout}: ReportActionsViewProps) { parentReportAction={parentReportAction} parentReportActionForTransactionThread={parentReportActionForTransactionThread} onLayout={recordTimeToMeasureItemLayout} - sortedReportActions={conciergeSidePanelFilteredReportActions} - sortedVisibleReportActions={conciergeSidePanelFilteredVisibleActions} + sortedReportActions={sortedReportActions} + sortedVisibleReportActions={sortedVisibleReportActions} loadOlderChats={loadOlderChats} loadNewerChats={loadNewerChats} hasNewerActions={hasNewerActions} From 71c2e57539c9d11b48c1ac21526054a8540ad8e2 Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Tue, 19 May 2026 15:58:10 +0200 Subject: [PATCH 2/2] add isLoaded to ReportActionsViewTest mock Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/ui/ReportActionsViewTest.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/ui/ReportActionsViewTest.tsx b/tests/ui/ReportActionsViewTest.tsx index c4980df1f1a2..4c378db22605 100644 --- a/tests/ui/ReportActionsViewTest.tsx +++ b/tests/ui/ReportActionsViewTest.tsx @@ -182,6 +182,7 @@ describe('ReportActionsView', () => { mockUseTransactionsAndViolationsForReport.mockReturnValue({ transactions: {}, violations: {}, + isLoaded: true, }); mockUsePaginatedReportActions.mockReturnValue({