Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,10 @@ const ONYXKEYS = {
/** List of transaction IDs used when navigating to prev/next transaction when viewing it in RHP */
TRANSACTION_THREAD_NAVIGATION_TRANSACTION_IDS: 'transactionThreadNavigationTransactionIDs',

/** Hash of the search snapshot that holds the transactions referenced by TRANSACTION_THREAD_NAVIGATION_TRANSACTION_IDS.
* Used to fall back to snapshot data when the live transaction collection hasn't loaded those transactions yet (e.g. opening an expense from the Spend page as an approver). */
TRANSACTION_THREAD_NAVIGATION_SNAPSHOT_HASH: 'transactionThreadNavigationSnapshotHash',

/** Optional map of transactionID -> sibling descriptor for prev/next navigation in snapshot-backed flows (e.g. Home "Recently added"), where siblings may be absent from the main Onyx collections. When set, navigation resolves (and lazily creates) each sibling's thread on demand from its descriptor. */
TRANSACTION_THREAD_NAVIGATION_THREAD_REPORT_IDS: 'transactionThreadNavigationThreadReportIDs',

Expand Down Expand Up @@ -1821,6 +1825,7 @@ type OnyxValuesMapping = {
[ONYXKEYS.REPORT_NAVIGATION_LAST_SEARCH_QUERY]: OnyxTypes.LastSearchParams;
[ONYXKEYS.NVP_LAST_ANDROID_LOGIN]: string;
[ONYXKEYS.TRANSACTION_THREAD_NAVIGATION_TRANSACTION_IDS]: string[];
[ONYXKEYS.TRANSACTION_THREAD_NAVIGATION_SNAPSHOT_HASH]: number;
[ONYXKEYS.TRANSACTION_THREAD_NAVIGATION_THREAD_REPORT_IDS]: Record<string, TransactionThreadNavigationDescriptor>;
[ONYXKEYS.NVP_INTEGRATION_SERVER_EXPORT_TEMPLATES]: OnyxTypes.ExportTemplate[];
[ONYXKEYS.ONBOARDING_USER_REPORTED_INTEGRATION]: OnboardingAccounting;
Expand Down
74 changes: 63 additions & 11 deletions src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import useLocalize from '@hooks/useLocalize';
import useMobileSelectionMode from '@hooks/useMobileSelectionMode';
import useMoneyReportHeaderMoreContentVisibility from '@hooks/useMoneyReportHeaderMoreContentVisibility';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import useReportPrimaryAction from '@hooks/useReportPrimaryAction';
Expand All @@ -9,16 +10,21 @@ import useThemeStyles from '@hooks/useThemeStyles';
import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport';

import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList} from '@libs/Navigation/types';
import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';

import type {OnyxEntry} from 'react-native-onyx';

import {useRoute} from '@react-navigation/native';
import React, {useEffect} from 'react';
import React, {useCallback, useEffect} from 'react';
import {View} from 'react-native';

import HeaderLoadingBar from './HeaderLoadingBar';
Expand All @@ -27,6 +33,8 @@ import MoneyReportHeaderActions from './MoneyReportHeaderActions';
import {ExportDownloadStatusProvider} from './MoneyReportHeaderActions/ExportDownloadStatusProvider';
import MoneyReportHeaderModals from './MoneyReportHeaderModals';
import MoneyReportHeaderMoreContent from './MoneyReportHeaderMoreContent';
import MoneyRequestReportNavigation from './MoneyRequestReportView/MoneyRequestReportNavigation';
import MoneyRequestReportTransactionsNavigation from './MoneyRequestReportView/MoneyRequestReportTransactionsNavigation';
import {PaymentAnimationsProvider} from './PaymentAnimationsContext';
import {useSearchSelectionActions} from './Search/SearchContext';

Expand Down Expand Up @@ -79,15 +87,38 @@ function MoneyReportHeaderContent({reportID: reportIDProp, shouldDisplayBackButt

const transactions = Object.values(reportTransactions);

const [activeTransactionIDs] = useOnyx(ONYXKEYS.TRANSACTION_THREAD_NAVIGATION_TRANSACTION_IDS);

const singleTransactionID = transactions.length === 1 ? transactions.at(0)?.transactionID : undefined;

const threadParentReportActionID = moneyRequestReport?.parentReportActionID;
const threadTransactionIDSelector = useCallback(
(parentReportActions: OnyxEntry<OnyxTypes.ReportActions>) => {
const parentReportAction = threadParentReportActionID ? parentReportActions?.[threadParentReportActionID] : undefined;
return isMoneyRequestAction(parentReportAction) ? getOriginalMessage(parentReportAction)?.IOUTransactionID : undefined;
},
[threadParentReportActionID],
);
const [threadTransactionID] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(moneyRequestReport?.parentReportID)}`, {selector: threadTransactionIDSelector});

const anchorTransactionIDFromRoute = route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT ? route.params.anchorTransactionID : undefined;
const multiTxAnchorTransactionID = anchorTransactionIDFromRoute && activeTransactionIDs?.includes(anchorTransactionIDFromRoute) ? anchorTransactionIDFromRoute : undefined;
const carouselAnchorTransactionID = singleTransactionID ?? threadTransactionID ?? multiTxAnchorTransactionID;
const shouldShowTransactionNavigation = !!carouselAnchorTransactionID && !!activeTransactionIDs?.includes(carouselAnchorTransactionID);

const styles = useThemeStyles();

const {isWideRHPDisplayedOnWideLayout, isSuperWideRHPDisplayedOnWideLayout} = useResponsiveLayoutOnWideRHP();

const shouldShowHeaderButtonsInHeaderRow = isInLandscapeMode || !shouldDisplayNarrowVersion || isWideRHPDisplayedOnWideLayout || isSuperWideRHPDisplayedOnWideLayout;

const isReportInRHP = route.name !== SCREENS.REPORT;
const shouldDisplaySearchRouter = !isReportInRHP || isSmallScreenWidth;
const isReportInSearch = route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT || route.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT;

const {statusBarType, shouldShowNextStep, hasStatusOrNextStep} = useMoneyReportHeaderMoreContentVisibility(reportIDProp);
const shouldRenderActionsInHeaderRow = shouldShowHeaderButtonsInHeaderRow && !hasStatusOrNextStep;
const shouldDisplaySearchRouter = !isReportInRHP || (isSmallScreenWidth && !isReportInSearch);

const backTo = (route.params as {backTo?: Route} | undefined)?.backTo;

const primaryAction = useReportPrimaryAction(reportIDProp);
Expand Down Expand Up @@ -135,14 +166,28 @@ function MoneyReportHeaderContent({reportID: reportIDProp, shouldDisplayBackButt
shouldEnableDetailPageNavigation
openParentReportInCurrentTab
>
{shouldShowHeaderButtonsInHeaderRow && (
<MoneyReportHeaderActions
reportID={reportIDProp}
primaryAction={primaryAction}
isReportInSearch={isReportInSearch}
backTo={backTo}
/>
)}
<View style={[styles.flexRow, styles.alignItemsCenter, styles.gap3]}>
{shouldRenderActionsInHeaderRow && (
<MoneyReportHeaderActions
reportID={reportIDProp}
primaryAction={primaryAction}
isReportInSearch={isReportInSearch}
backTo={backTo}
/>
)}
{isReportInSearch &&
(shouldShowTransactionNavigation && carouselAnchorTransactionID ? (
<MoneyRequestReportTransactionsNavigation
currentTransactionID={carouselAnchorTransactionID}
shouldDisplayNarrowVersion={!shouldShowHeaderButtonsInHeaderRow}
/>
) : (
<MoneyRequestReportNavigation
reportID={reportIDProp}
shouldDisplayNarrowVersion={!shouldShowHeaderButtonsInHeaderRow}
/>
))}
</View>
</HeaderWithBackButton>
{!shouldShowHeaderButtonsInHeaderRow && (
<MoneyReportHeaderActions
Expand All @@ -152,7 +197,14 @@ function MoneyReportHeaderContent({reportID: reportIDProp, shouldDisplayBackButt
backTo={backTo}
/>
)}
<MoneyReportHeaderMoreContent reportID={reportIDProp} />
<MoneyReportHeaderMoreContent
reportID={reportIDProp}
primaryAction={primaryAction}
backTo={backTo}
statusBarType={statusBarType}
shouldShowNextStep={shouldShowNextStep}
shouldRenderActionsInRow={shouldShowHeaderButtonsInHeaderRow && !shouldRenderActionsInHeaderRow}
/>
<HeaderLoadingBar />
</View>
);
Expand Down
70 changes: 45 additions & 25 deletions src/components/MoneyReportHeaderMoreContent.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import useMoneyReportHeaderStatusBar from '@hooks/useMoneyReportHeaderStatusBar';
import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP';
import useThemeStyles from '@hooks/useThemeStyles';

import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
import type {ReportsSplitNavigatorParamList, RightModalNavigatorParamList} from '@libs/Navigation/types';
import {isGroupPolicy} from '@libs/PolicyUtils';
import {isInvoiceReport as isInvoiceReportUtil} from '@libs/ReportUtils';

import type CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';

Expand All @@ -22,20 +17,37 @@ import {useRoute} from '@react-navigation/native';
import React from 'react';
import {View} from 'react-native';

import type {MoneyReportHeaderActionsProps} from './MoneyReportHeaderActions/types';

import MoneyReportHeaderActions from './MoneyReportHeaderActions';
import MoneyReportHeaderNextStep from './MoneyReportHeaderNextStep';
import MoneyReportHeaderStatusBarSection from './MoneyReportHeaderStatusBarSection';
import {useMoneyReportTransactionThread} from './MoneyReportTransactionThreadContext';
import MoneyRequestReportNavigation from './MoneyRequestReportView/MoneyRequestReportNavigation';

type MoneyReportHeaderMoreContentProps = {
reportID: string | undefined;

/** The report's primary action, forwarded to the actions row */
primaryAction: MoneyReportHeaderActionsProps['primaryAction'];

/** Route to navigate back to */
backTo: Route | undefined;

/** Which status bar to render, resolved by the header via useMoneyReportHeaderMoreContentVisibility */
statusBarType: ValueOf<typeof CONST.REPORT.STATUS_BAR_TYPE> | undefined;

/** Whether the next step bar should be rendered, resolved alongside `statusBarType` */
shouldShowNextStep: boolean;

/** Whether the report actions belong at the end of this row. The header renders them itself when this row is empty. */
shouldRenderActionsInRow: boolean;
};

/**
* Cheap visibility gate — fetches minimal data to decide whether the more-content section
* should render at all, avoiding expensive hooks in the body when nothing is shown.
* Cheap visibility gate — decides whether the more-content section should render at all,
Comment thread
thelullabyy marked this conversation as resolved.
* avoiding expensive hooks in the body when nothing is shown.
*/
function MoneyReportHeaderMoreContent({reportID}: MoneyReportHeaderMoreContentProps) {
function MoneyReportHeaderMoreContent({reportID, primaryAction, backTo, statusBarType, shouldShowNextStep, shouldRenderActionsInRow}: MoneyReportHeaderMoreContentProps) {
const route = useRoute<
| PlatformStackRouteProp<ReportsSplitNavigatorParamList, typeof SCREENS.REPORT>
| PlatformStackRouteProp<RightModalNavigatorParamList, typeof SCREENS.RIGHT_MODAL.EXPENSE_REPORT>
Expand All @@ -45,13 +57,9 @@ function MoneyReportHeaderMoreContent({reportID}: MoneyReportHeaderMoreContentPr
const isReportInSearch = route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT || route.name === SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT;

const [moneyRequestReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(moneyRequestReport?.policyID)}`);
const {shouldShowStatusBar, statusBarType} = useMoneyReportHeaderStatusBar(reportID, moneyRequestReport?.chatReportID);

const isInvoiceReport = isInvoiceReportUtil(moneyRequestReport);
const shouldShowNextStep = isGroupPolicy(policy) && !isInvoiceReport && !shouldShowStatusBar;

const shouldShowMoreContent = shouldShowNextStep || !!statusBarType || isReportInSearch;
const hasStatusOrNextStep = shouldShowNextStep || !!statusBarType;
const shouldShowMoreContent = hasStatusOrNextStep || shouldRenderActionsInRow;

if (!shouldShowMoreContent) {
return null;
Expand All @@ -63,6 +71,9 @@ function MoneyReportHeaderMoreContent({reportID}: MoneyReportHeaderMoreContentPr
statusBarType={statusBarType}
isReportInSearch={isReportInSearch}
shouldShowNextStep={shouldShowNextStep}
primaryAction={primaryAction}
backTo={backTo}
shouldRenderActionsInRow={shouldRenderActionsInRow}
/>
);
}
Expand All @@ -72,20 +83,27 @@ type MoneyReportHeaderMoreContentBodyProps = {
statusBarType: ValueOf<typeof CONST.REPORT.STATUS_BAR_TYPE> | undefined;
isReportInSearch: boolean;
shouldShowNextStep: boolean;
primaryAction: MoneyReportHeaderActionsProps['primaryAction'];
backTo: Route | undefined;
shouldRenderActionsInRow: boolean;
};

function MoneyReportHeaderMoreContentBody({moneyRequestReport, statusBarType, isReportInSearch, shouldShowNextStep}: MoneyReportHeaderMoreContentBodyProps) {
function MoneyReportHeaderMoreContentBody({
moneyRequestReport,
statusBarType,
isReportInSearch,
shouldShowNextStep,
primaryAction,
backTo,
shouldRenderActionsInRow,
}: MoneyReportHeaderMoreContentBodyProps) {
const styles = useThemeStyles();
const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout();
const shouldDisplayNarrowVersion = shouldUseNarrowLayout || isMediumScreenWidth;
const {isWideRHPDisplayedOnWideLayout, isSuperWideRHPDisplayedOnWideLayout} = useResponsiveLayoutOnWideRHP();
const shouldDisplayNarrowMoreButton = !shouldDisplayNarrowVersion || isWideRHPDisplayedOnWideLayout || isSuperWideRHPDisplayedOnWideLayout;

const reportID = moneyRequestReport?.reportID;
const {iouTransactionID} = useMoneyReportTransactionThread();

return (
<View style={[styles.flexRow, styles.gap2, styles.justifyContentStart, styles.flexNoWrap, styles.ph5, styles.pb3]}>
<View style={[styles.flexRow, styles.gap2, styles.justifyContentStart, styles.flexNoWrap, styles.ph5, styles.pb3, shouldShowNextStep && styles.pt0]}>
<View style={[styles.flexShrink1, styles.flexGrow1, styles.mnw0, styles.flexWrap, styles.justifyContentCenter]}>
{shouldShowNextStep && <MoneyReportHeaderNextStep reportID={reportID} />}
<MoneyReportHeaderStatusBarSection
Expand All @@ -94,10 +112,12 @@ function MoneyReportHeaderMoreContentBody({moneyRequestReport, statusBarType, is
iouTransactionID={iouTransactionID}
/>
</View>
{isReportInSearch && (
<MoneyRequestReportNavigation
{shouldRenderActionsInRow && (
<MoneyReportHeaderActions
reportID={reportID}
shouldDisplayNarrowVersion={!shouldDisplayNarrowMoreButton}
primaryAction={primaryAction}
isReportInSearch={isReportInSearch}
backTo={backTo}
/>
)}
</View>
Expand Down
22 changes: 16 additions & 6 deletions src/components/MoneyRequestHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ function MoneyRequestHeader({reportID: reportIDProp, onBackButtonPress}: MoneyRe
const shouldDisplayTransactionNavigation = !!(reportID && isReportInRHP);
const shouldOpenParentReportInCurrentTab = !isSelfDM(parentReport);
const shouldDisplayButtonsInSeparateLine = useShouldDisplayButtonsInSeparateLine() && (wideRHPRouteKeys.length === 0 || isSmallScreenWidth);
const shouldDisplayNarrowVersion = shouldDisplayButtonsInSeparateLine;

const getStatusIcon: (src: IconAsset) => ReactNode = (src) => (
<Icon
Expand Down Expand Up @@ -182,7 +183,7 @@ function MoneyRequestHeader({reportID: reportIDProp, onBackButtonPress}: MoneyRe
shouldEnableDetailPageNavigation
openParentReportInCurrentTab={shouldOpenParentReportInCurrentTab}
>
{!shouldDisplayButtonsInSeparateLine && (
{!shouldDisplayButtonsInSeparateLine && !statusBarProps && (
<MoneyRequestHeaderActions
reportID={reportID}
onBackButtonPress={onBackButtonPress}
Expand All @@ -192,6 +193,7 @@ function MoneyRequestHeader({reportID: reportIDProp, onBackButtonPress}: MoneyRe
<MoneyRequestReportTransactionsNavigation
currentTransactionID={transaction.transactionID}
isFromReviewDuplicates={isFromReviewDuplicates}
shouldDisplayNarrowVersion={shouldDisplayNarrowVersion}
/>
)}
</HeaderWithBackButton>
Expand All @@ -202,11 +204,19 @@ function MoneyRequestHeader({reportID: reportIDProp, onBackButtonPress}: MoneyRe
/>
)}
{!!statusBarProps && (
<View style={[styles.ph5, styles.pb3]}>
<MoneyRequestHeaderStatusBar
icon={statusBarProps.icon}
description={statusBarProps.description}
/>
<View style={[styles.flexRow, styles.gap2, styles.justifyContentStart, styles.flexNoWrap, styles.ph5, styles.pb3]}>
<View style={[styles.flexShrink1, styles.flexGrow1, styles.mnw0, styles.flexWrap, styles.justifyContentCenter]}>
<MoneyRequestHeaderStatusBar
icon={statusBarProps.icon}
description={statusBarProps.description}
/>
</View>
{!shouldDisplayButtonsInSeparateLine && (
<MoneyRequestHeaderActions
reportID={reportID}
onBackButtonPress={onBackButtonPress}
/>
)}
</View>
)}
<HeaderLoadingBar />
Expand Down
Loading
Loading