From a6c869dc7e3e1bd9943a220db3056d57caaa68a0 Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Tue, 17 Mar 2026 16:09:21 +0100 Subject: [PATCH 1/4] Make header/footer components self-subscribing via reportID (#84895) Convert 5 header/footer components from receiving full report/policy objects as props to self-subscribing via reportID: - HeaderView: subscribe to report via useOnyx, get parentReportAction via useParentReportAction, get shouldUseNarrowLayout from useResponsiveLayout - MoneyReportHeader: subscribe to report/policy/metadata via useOnyx, get reportActions via usePaginatedReportActions, compute transactionThreadReportID internally - MoneyRequestHeader: subscribe to report/policy via useOnyx, get parentReportAction via useParentReportAction - ArchivedReportFooter: subscribe to report via useOnyx, get currentUserAccountID from useCurrentUserPersonalDetails - AnonymousReportFooter: subscribe to report via useOnyx, compute isSmallSizeLayout internally using useResponsiveLayout and useWindowDimensions Update all call sites (ReportScreen, MoneyRequestReportView, ReportFooter) to pass reportID instead of full objects. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/AnonymousReportFooter.tsx | 22 ++++++--- src/components/ArchivedReportFooter.tsx | 15 +++--- src/components/MoneyReportHeader.tsx | 49 +++++++++---------- src/components/MoneyRequestHeader.tsx | 21 ++++---- .../MoneyRequestReportView.tsx | 12 ++--- src/pages/inbox/HeaderView.tsx | 19 +++---- src/pages/inbox/ReportScreen.tsx | 27 ++-------- src/pages/inbox/report/ReportFooter.tsx | 19 +------ tests/ui/components/HeaderViewTest.tsx | 12 ++--- 9 files changed, 72 insertions(+), 124 deletions(-) diff --git a/src/components/AnonymousReportFooter.tsx b/src/components/AnonymousReportFooter.tsx index 0f2baad10aa50..16707cccb1ff8 100644 --- a/src/components/AnonymousReportFooter.tsx +++ b/src/components/AnonymousReportFooter.tsx @@ -1,26 +1,32 @@ import React from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; +import useIsInSidePanel from '@hooks/useIsInSidePanel'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import variables from '@styles/variables'; import {signOutAndRedirectToSignIn} from '@userActions/Session'; -import type {Report} from '@src/types/onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; import AvatarWithDisplayName from './AvatarWithDisplayName'; import Button from './Button'; import ExpensifyWordmark from './ExpensifyWordmark'; import Text from './Text'; type AnonymousReportFooterProps = { - /** The report currently being looked at */ - report: OnyxEntry; - - /** Whether the small screen size layout should be used */ - isSmallSizeLayout?: boolean; + /** The reportID of the report currently being looked at */ + reportID: string | undefined; }; -function AnonymousReportFooter({isSmallSizeLayout = false, report}: AnonymousReportFooterProps) { +function AnonymousReportFooter({reportID}: AnonymousReportFooterProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const isInSidePanel = useIsInSidePanel(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {windowWidth} = useWindowDimensions(); + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); + const isSmallSizeLayout = windowWidth - (shouldUseNarrowLayout ? 0 : variables.sideBarWithLHBWidth) < variables.anonymousReportFooterBreakpoint || isInSidePanel; return ( diff --git a/src/components/ArchivedReportFooter.tsx b/src/components/ArchivedReportFooter.tsx index 66abbf25d0410..6f7456ec004e2 100644 --- a/src/components/ArchivedReportFooter.tsx +++ b/src/components/ArchivedReportFooter.tsx @@ -1,6 +1,7 @@ import {getLastClosedReportAction} from '@selectors/ReportAction'; import lodashEscape from 'lodash/escape'; import React from 'react'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -9,23 +10,23 @@ import {getOriginalMessage, isClosedAction} from '@libs/ReportActionsUtils'; import {getPolicyName} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetailsList, Report} from '@src/types/onyx'; +import type {PersonalDetailsList} from '@src/types/onyx'; import {getEmptyObject} from '@src/types/utils/EmptyObject'; import Banner from './Banner'; type ArchivedReportFooterProps = { - /** The archived report */ - report: Report; - /** Current user's account id */ - currentUserAccountID: number; + /** The reportID of the archived report */ + reportID: string; }; -function ArchivedReportFooter({report, currentUserAccountID}: ArchivedReportFooterProps) { +function ArchivedReportFooter({reportID}: ArchivedReportFooterProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); const [personalDetails = getEmptyObject()] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); - const [reportClosedAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, {canEvict: false, selector: getLastClosedReportAction}); + const [reportClosedAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {canEvict: false, selector: getLastClosedReportAction}); const originalMessage = isClosedAction(reportClosedAction) ? getOriginalMessage(reportClosedAction) : null; const archiveReason = originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT; const actorPersonalDetails = personalDetails?.[reportClosedAction?.actorAccountID ?? CONST.DEFAULT_NUMBER_ID]; diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 868ed3de8168e..a2b8954e0c6a9 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -8,7 +8,6 @@ import truncate from 'lodash/truncate'; import React, {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {InteractionManager, View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import useActiveAdminPolicies from '@hooks/useActiveAdminPolicies'; import useConfirmModal from '@hooks/useConfirmModal'; @@ -23,6 +22,7 @@ import useLocalize from '@hooks/useLocalize'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; +import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import useParticipantsInvoiceReport from '@hooks/useParticipantsInvoiceReport'; import usePaymentAnimations from '@hooks/usePaymentAnimations'; import usePaymentOptions from '@hooks/usePaymentOptions'; @@ -30,6 +30,7 @@ import usePermissions from '@hooks/usePermissions'; import usePersonalPolicy from '@hooks/usePersonalPolicy'; import usePolicy from '@hooks/usePolicy'; import useReportIsArchived from '@hooks/useReportIsArchived'; +import useReportTransactionsCollection from '@hooks/useReportTransactionsCollection'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP'; import useSearchShouldCalculateTotals from '@hooks/useSearchShouldCalculateTotals'; @@ -53,7 +54,7 @@ import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import getPlatform from '@libs/getPlatform'; import {getExistingTransactionID} from '@libs/IOUUtils'; import Log from '@libs/Log'; -import {getThreadReportIDsForTransactions, getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils'; +import {getAllNonDeletedTransactions, getThreadReportIDsForTransactions, getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList} from '@libs/Navigation/types'; @@ -68,8 +69,10 @@ import type {KYCFlowEvent, TriggerKYCFlow} from '@libs/PaymentUtils'; import {handleUnvalidatedAccount, selectPaymentType} from '@libs/PaymentUtils'; import {getConnectedIntegration, getValidConnectedIntegration, hasDynamicExternalWorkflow, sortPoliciesByName} from '@libs/PolicyUtils'; import { + getFilteredReportActionsForReportView, getIOUActionForReportID, getIOUActionForTransactionID, + getOneTransactionThreadReportID, getOriginalMessage, getReportAction, hasPendingDEWApprove, @@ -195,21 +198,8 @@ import type {PaymentActionParams} from './SettlementButton/types'; import Text from './Text'; type MoneyReportHeaderProps = { - /** The report currently being looked at */ - report: OnyxEntry; - - /** The policy tied to the expense report */ - policy: OnyxEntry; - - /** Array of report actions for the report */ - reportActions: OnyxTypes.ReportAction[]; - - /** The reportID of the transaction thread report associated with this current report, if any */ - // eslint-disable-next-line react/no-unused-prop-types - transactionThreadReportID: string | undefined; - - /** whether we are loading report data in openReport command */ - isLoadingInitialReportActions?: boolean; + /** The reportID of the report currently being looked at */ + reportID: string | undefined; /** Whether back button should be displayed in header */ shouldDisplayBackButton?: boolean; @@ -218,15 +208,15 @@ type MoneyReportHeaderProps = { onBackButtonPress: () => void; }; -function MoneyReportHeader({ - policy, - report: moneyRequestReport, - transactionThreadReportID, - reportActions, - isLoadingInitialReportActions, - shouldDisplayBackButton = false, - onBackButtonPress, -}: MoneyReportHeaderProps) { +function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = false, onBackButtonPress}: MoneyReportHeaderProps) { + // Self-subscribe to report, policy, metadata + const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDProp}`); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(moneyRequestReport?.policyID)}`); + const [reportMetadataInternal] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportIDProp}`); + const isLoadingInitialReportActions = reportMetadataInternal?.isLoadingInitialReportActions; + const {reportActions: unfilteredReportActions} = usePaginatedReportActions(moneyRequestReport?.reportID); + const reportActions = getFilteredReportActionsForReportView(unfilteredReportActions); + // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use a correct layout for the hold expense modal https://github.com/Expensify/App/pull/47990#issuecomment-2362382026 // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout(); @@ -244,6 +234,12 @@ function MoneyReportHeader({ const [ownerBillingGraceEndPeriod] = useOnyx(ONYXKEYS.NVP_PRIVATE_OWNER_BILLING_GRACE_PERIOD_END); const [userBillingGraceEndPeriods] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END); const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${moneyRequestReport?.chatReportID}`); + const {isOffline} = useNetwork(); + const allReportTransactions = useReportTransactionsCollection(reportIDProp); + const nonDeletedTransactions = getAllNonDeletedTransactions(allReportTransactions, reportActions, isOffline, true); + const visibleTransactionsForThreadID = nonDeletedTransactions?.filter((t) => isOffline || t.pendingAction !== 'delete'); + const reportTransactionIDs = visibleTransactionsForThreadID?.map((t) => t.transactionID); + const transactionThreadReportID = getOneTransactionThreadReportID(moneyRequestReport, chatReport, reportActions ?? [], isOffline, reportTransactionIDs); const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport?.reportID}`); const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, { selector: isUserValidatedSelector, @@ -406,7 +402,6 @@ function MoneyReportHeader({ usePaymentAnimations(); const styles = useThemeStyles(); const theme = useTheme(); - const {isOffline} = useNetwork(); const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(transaction, originalTransaction); const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 873479b5ea7a7..5a0b1987d5bd7 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -5,7 +5,6 @@ import {validTransactionDraftsSelector} from '@selectors/TransactionDraft'; import type {ReactNode} from 'react'; import React, {useCallback, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import useConfirmModal from '@hooks/useConfirmModal'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -16,6 +15,7 @@ import useGetIOUReportFromReportAction from '@hooks/useGetIOUReportFromReportAct import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import useParentReportAction from '@hooks/useParentReportAction'; import usePermissions from '@hooks/usePermissions'; import usePolicyForMovingExpenses from '@hooks/usePolicyForMovingExpenses'; import useReportIsArchived from '@hooks/useReportIsArchived'; @@ -74,7 +74,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -import type {Policy, Report, ReportAction, Transaction} from '@src/types/onyx'; +import type {Transaction} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; import BrokenConnectionDescription from './BrokenConnectionDescription'; import Button from './Button'; @@ -96,20 +96,19 @@ import {useSearchActionsContext, useSearchStateContext} from './Search/SearchCon import {useWideRHPState} from './WideRHPContextProvider'; type MoneyRequestHeaderProps = { - /** The report currently being looked at */ - report: OnyxEntry; - - /** The policy which the report is tied to */ - policy: OnyxEntry; - - /** The report action the transaction is tied to from the parent report */ - parentReportAction: OnyxEntry; + /** The reportID of the report currently being looked at */ + reportID: string | undefined; /** Method to trigger when pressing close button of the header */ onBackButtonPress: (prioritizeBackTo?: boolean) => void; }; -function MoneyRequestHeader({report, parentReportAction, policy, onBackButtonPress}: MoneyRequestHeaderProps) { +function MoneyRequestHeader({reportID: reportIDProp, onBackButtonPress}: MoneyRequestHeaderProps) { + // Self-subscribe to report, policy, parentReportAction + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDProp}`); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(report?.policyID)}`); + const parentReportAction = useParentReportAction(report); + // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use a correct layout for the hold expense modal https://github.com/Expensify/App/pull/47990#issuecomment-2362382026 // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth, isInNarrowPaneModal} = useResponsiveLayout(); diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx index db5e3f5d913c5..74610e88c8ea1 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx @@ -180,9 +180,7 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe () => isTransactionThreadView ? ( { if (!backToRoute) { goBackFromSearchMoneyRequest(); @@ -193,11 +191,7 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe /> ) : ( { if (!backToRoute) { @@ -208,7 +202,7 @@ function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayRe }} /> ), - [backToRoute, isLoadingInitialReportActions, isTransactionThreadView, parentReportAction, policy, report, reportActions, transactionThreadReportID], + [backToRoute, isTransactionThreadView, report?.reportID], ); // We need to cancel telemetry span when user leaves the screen before full report data is loaded diff --git a/src/pages/inbox/HeaderView.tsx b/src/pages/inbox/HeaderView.tsx index dea0f76317c33..8a4a15bf730aa 100644 --- a/src/pages/inbox/HeaderView.tsx +++ b/src/pages/inbox/HeaderView.tsx @@ -4,7 +4,6 @@ import {pendingChatMembersSelector} from '@selectors/ReportMetaData'; import {isPast} from 'date-fns'; import React, {useMemo} from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; import Button from '@components/Button'; import CaretWrapper from '@components/CaretWrapper'; import DisplayNames from '@components/DisplayNames'; @@ -83,30 +82,24 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; import reportsSelector from '@src/selectors/Attributes'; -import type {Report, ReportAction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; type HeaderViewProps = { /** Toggles the navigationMenu open and closed */ onNavigationMenuButtonClicked: () => void; - /** The report currently being looked at */ - report: OnyxEntry; - - /** The report action the transaction is tied to from the parent report */ - parentReportAction: OnyxEntry | null; - /** The reportID of the current report */ reportID: string | undefined; - - /** Whether we should display the header as in narrow layout */ - shouldUseNarrowLayout?: boolean; }; -function HeaderView({report, parentReportAction, onNavigationMenuButtonClicked, shouldUseNarrowLayout = false}: HeaderViewProps) { +function HeaderView({onNavigationMenuButtonClicked, reportID}: HeaderViewProps) { + // Self-subscribe to report, parentReportAction + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); + const parentReportAction = useParentReportAction(report); + const icons = useMemoizedLazyExpensifyIcons(['BackArrow', 'Close', 'DotIndicator'] as const); // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth - const {isSmallScreenWidth} = useResponsiveLayout(); + const {isSmallScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); const isInSidePanel = useIsInSidePanel(); const route = useRoute(); const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(report?.parentReportID) ?? getNonEmptyStringOnyxID(report?.reportID)}`); diff --git a/src/pages/inbox/ReportScreen.tsx b/src/pages/inbox/ReportScreen.tsx index cce3a62827b17..fddce398d2cc2 100644 --- a/src/pages/inbox/ReportScreen.tsx +++ b/src/pages/inbox/ReportScreen.tsx @@ -408,9 +408,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { if (isTransactionThreadView) { return ( ); @@ -419,11 +417,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { if (isMoneyRequestOrInvoiceReport) { return ( ); @@ -433,24 +427,9 @@ function ReportScreen({route, navigation}: ReportScreenProps) { ); - }, [ - isTransactionThreadView, - isMoneyRequestOrInvoiceReport, - report, - policy, - parentReportAction, - onBackButtonPress, - transactionThreadReportID, - reportMetadata.isLoadingInitialReportActions, - reportActions, - reportIDFromRoute, - shouldUseNarrowLayout, - ]); + }, [isTransactionThreadView, isMoneyRequestOrInvoiceReport, onBackButtonPress, reportIDFromRoute]); useEffect(() => { if (!transactionThreadReportID || !route?.params?.reportActionID || !isOneTransactionThread(childReport, report, linkedAction)) { diff --git a/src/pages/inbox/report/ReportFooter.tsx b/src/pages/inbox/report/ReportFooter.tsx index bab9c20404680..ee338f26f616e 100644 --- a/src/pages/inbox/report/ReportFooter.tsx +++ b/src/pages/inbox/report/ReportFooter.tsx @@ -22,7 +22,6 @@ import useReportIsArchived from '@hooks/useReportIsArchived'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useShortMentionsList from '@hooks/useShortMentionsList'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import {addComment} from '@libs/actions/Report'; import {createTaskAndNavigate, setNewOptimisticAssignee} from '@libs/actions/Task'; import {isEmailPublicDomain} from '@libs/LoginUtils'; @@ -37,7 +36,6 @@ import { isSystemChat as isSystemChatUtil, } from '@libs/ReportUtils'; import {generateAccountID} from '@libs/UserUtils'; -import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; @@ -85,7 +83,6 @@ function ReportFooter({ const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {translate} = useLocalize(); - const {windowWidth} = useWindowDimensions(); const isInSidePanel = useIsInSidePanel(); // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth, shouldUseNarrowLayout} = useResponsiveLayout(); @@ -113,8 +110,6 @@ function ReportFooter({ const ancestors = useAncestors(report); const isArchivedRoom = isArchivedNonExpenseReport(report, isReportArchived); - const isSmallSizeLayout = windowWidth - (shouldUseNarrowLayout ? 0 : variables.sideBarWithLHBWidth) < variables.anonymousReportFooterBreakpoint; - // If a user just signed in and is viewing a public report, optimistically show the composer while loading the report, since they will have write access when the response comes back. const shouldShowComposerOptimistically = !isAnonymousUser && isPublicRoom(report) && !!isLoadingInitialReportActions; const canPerformWriteAction = canUserPerformWriteAction(report, isReportArchived) ?? shouldShowComposerOptimistically; @@ -222,18 +217,8 @@ function ReportFooter({ shouldUseNarrowLayout ? styles.mb5 : null, ]} > - {isAnonymousUser && !isArchivedRoom && ( - - )} - {isArchivedRoom && ( - - )} + {isAnonymousUser && !isArchivedRoom && } + {isArchivedRoom && } {!isArchivedRoom && !!isBlockedFromChat && } {!isAnonymousUser && !canWriteInReport && isSystemChat && } {isAdminsOnlyPostingRoom && !isUserPolicyAdmin && !isArchivedRoom && !isAnonymousUser && !isBlockedFromChat && ( diff --git a/tests/ui/components/HeaderViewTest.tsx b/tests/ui/components/HeaderViewTest.tsx index ff2de6b5ef5e5..0c6fada15d3a2 100644 --- a/tests/ui/components/HeaderViewTest.tsx +++ b/tests/ui/components/HeaderViewTest.tsx @@ -50,7 +50,7 @@ describe('HeaderView', () => { }); beforeAll(() => { - Onyx.init({keys: ONYXKEYS}); + Onyx.init({keys: ONYXKEYS, evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS]}); initOnyxDerivedValues(); return waitForBatchedUpdates(); }); @@ -82,9 +82,7 @@ describe('HeaderView', () => { {}} - parentReportAction={null} reportID={report.reportID} /> @@ -117,14 +115,14 @@ describe('HeaderView', () => { notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, }; + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); + await waitForBatchedUpdates(); + render( {}} - parentReportAction={null} reportID={report.reportID} - shouldUseNarrowLayout /> , ); @@ -178,9 +176,7 @@ describe('HeaderView', () => { {}} - parentReportAction={parentReportAction} reportID={threadReport.reportID} /> From e0e72d6f0953799a5cdc25e06e6dff0a5efe7da2 Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Wed, 18 Mar 2026 14:01:03 +0100 Subject: [PATCH 2/4] Remove noisy self-subscribe comments from header components Co-Authored-By: Claude Sonnet 4.6 (1M context) --- src/components/MoneyReportHeader.tsx | 1 - src/components/MoneyRequestHeader.tsx | 1 - src/pages/inbox/HeaderView.tsx | 1 - 3 files changed, 3 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index a2b8954e0c6a9..48d33ed519700 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -209,7 +209,6 @@ type MoneyReportHeaderProps = { }; function MoneyReportHeader({reportID: reportIDProp, shouldDisplayBackButton = false, onBackButtonPress}: MoneyReportHeaderProps) { - // Self-subscribe to report, policy, metadata const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDProp}`); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(moneyRequestReport?.policyID)}`); const [reportMetadataInternal] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportIDProp}`); diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 5a0b1987d5bd7..7096eb2e57175 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -104,7 +104,6 @@ type MoneyRequestHeaderProps = { }; function MoneyRequestHeader({reportID: reportIDProp, onBackButtonPress}: MoneyRequestHeaderProps) { - // Self-subscribe to report, policy, parentReportAction const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDProp}`); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(report?.policyID)}`); const parentReportAction = useParentReportAction(report); diff --git a/src/pages/inbox/HeaderView.tsx b/src/pages/inbox/HeaderView.tsx index 8a4a15bf730aa..1062f6851dd99 100644 --- a/src/pages/inbox/HeaderView.tsx +++ b/src/pages/inbox/HeaderView.tsx @@ -93,7 +93,6 @@ type HeaderViewProps = { }; function HeaderView({onNavigationMenuButtonClicked, reportID}: HeaderViewProps) { - // Self-subscribe to report, parentReportAction const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); const parentReportAction = useParentReportAction(report); From 1bbd8d94141465ab451c41cf8af944e459fd2690 Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Wed, 18 Mar 2026 16:38:13 +0100 Subject: [PATCH 3/4] Hydrate report currency from snapshot via action instead of local memo Replace the local reportToUse memoization with a dedicated hydrateReportCurrencyFromSnapshot action so self-subscribing child components (e.g. MoneyReportHeader) can read the currency directly from Onyx. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/libs/actions/Report/index.ts | 5 +++++ .../Search/SearchMoneyRequestReportPage.tsx | 17 +++++++---------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 31d47f7dd603e..6743779dab34b 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -1915,6 +1915,10 @@ function getOptimisticChatReport(accountID: number, currentUserAccountID: number }); } +function hydrateReportCurrencyFromSnapshot(reportID: string, currency: string) { + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {currency}); +} + function createTransactionThreadReport( introSelected: OnyxEntry, currentUserLogin: string, @@ -7290,4 +7294,5 @@ export { setOptimisticTransactionThread, prepareOnyxDataForCleanUpOptimisticParticipants, getGuidedSetupDataForOpenReport, + hydrateReportCurrencyFromSnapshot, }; diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx index 690565e2ffb76..59a57d882eac5 100644 --- a/src/pages/Search/SearchMoneyRequestReportPage.tsx +++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx @@ -43,7 +43,7 @@ import {isDefaultAvatar, isLetterAvatar, isPresetAvatar} from '@libs/UserAvatarU import Navigation from '@navigation/Navigation'; import ReactionListWrapper from '@pages/inbox/ReactionListWrapper'; import {ActionListContext} from '@pages/inbox/ReportScreenContext'; -import {clearDeleteTransactionNavigateBackUrl, createTransactionThreadReport, openReport, updateLastVisitTime} from '@userActions/Report'; +import {clearDeleteTransactionNavigateBackUrl, createTransactionThreadReport, hydrateReportCurrencyFromSnapshot, openReport, updateLastVisitTime} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; @@ -138,14 +138,6 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`] ?? {}) as typeof report; }, [snapshot, reportIDFromRoute]); - // Use snapshot report currency if main collection doesn't have it (for offline mode) - const reportToUse = useMemo(() => { - if (!report) { - return report; - } - return {...report, currency: report.currency ?? snapshotReport?.currency}; - }, [report, snapshotReport?.currency]); - const [reportMetadata = defaultReportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportIDFromRoute}`); const [policies = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; @@ -237,6 +229,11 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { isInitialMountRef.current = true; } + // Hydrate currency from snapshot if Onyx lacks it (offline) + if (report?.reportID && !report.currency && snapshotReport?.currency) { + hydrateReportCurrencyFromSnapshot(report.reportID, snapshotReport.currency); + } + // Guard prevents calling openReport for multi-transaction reports if (visibleTransactions.length > 2 && !isInitialMountRef.current) { return; @@ -428,7 +425,7 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { > Date: Fri, 20 Mar 2026 15:35:10 +0100 Subject: [PATCH 4/4] =?UTF-8?q?Remove=20snapshot=20currency=20fallback=20?= =?UTF-8?q?=E2=80=94=20Search=20API=20already=20populates=20Onyx=20collect?= =?UTF-8?q?ions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Search API returns main Onyx collections alongside snapshot data, so by the time the user opens a search result, useOnyx already has the full report with currency. The snapshot fallback is unnecessary. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/libs/actions/Report/index.ts | 5 ----- src/pages/Search/SearchMoneyRequestReportPage.tsx | 12 +----------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index eafd7165d0513..f520810ed5fc8 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -1920,10 +1920,6 @@ function getOptimisticChatReport(accountID: number, currentUserAccountID: number }); } -function hydrateReportCurrencyFromSnapshot(reportID: string, currency: string) { - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {currency}); -} - function createTransactionThreadReport( introSelected: OnyxEntry, currentUserLogin: string, @@ -7299,5 +7295,4 @@ export { setOptimisticTransactionThread, prepareOnyxDataForCleanUpOptimisticParticipants, getGuidedSetupDataForOpenReport, - hydrateReportCurrencyFromSnapshot, }; diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx index 59a57d882eac5..3fdccb5a62344 100644 --- a/src/pages/Search/SearchMoneyRequestReportPage.tsx +++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx @@ -43,7 +43,7 @@ import {isDefaultAvatar, isLetterAvatar, isPresetAvatar} from '@libs/UserAvatarU import Navigation from '@navigation/Navigation'; import ReactionListWrapper from '@pages/inbox/ReactionListWrapper'; import {ActionListContext} from '@pages/inbox/ReportScreenContext'; -import {clearDeleteTransactionNavigateBackUrl, createTransactionThreadReport, hydrateReportCurrencyFromSnapshot, openReport, updateLastVisitTime} from '@userActions/Report'; +import {clearDeleteTransactionNavigateBackUrl, createTransactionThreadReport, openReport, updateLastVisitTime} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; @@ -133,11 +133,6 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { }); }, [isFocused, deleteTransactionNavigateBackUrl]); - const snapshotReport = useMemo(() => { - // eslint-disable-next-line @typescript-eslint/no-deprecated - return (snapshot?.data?.[`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`] ?? {}) as typeof report; - }, [snapshot, reportIDFromRoute]); - const [reportMetadata = defaultReportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportIDFromRoute}`); const [policies = getEmptyObject>>()] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; @@ -229,11 +224,6 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) { isInitialMountRef.current = true; } - // Hydrate currency from snapshot if Onyx lacks it (offline) - if (report?.reportID && !report.currency && snapshotReport?.currency) { - hydrateReportCurrencyFromSnapshot(report.reportID, snapshotReport.currency); - } - // Guard prevents calling openReport for multi-transaction reports if (visibleTransactions.length > 2 && !isInitialMountRef.current) { return;