Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
be95b7d
Open the pressed expense from a multi-expense report preview
wildan-m Jul 29, 2026
7d052fa
Merge remote-tracking branch 'upstream/main' into wildan/91042-multi-…
wildan-m Aug 4, 2026
00c4429
Open the narrow-layout expense in the RHP and keep deleted rows out o…
wildan-m Aug 4, 2026
8bf0dce
Merge remote-tracking branch 'upstream/main' into wildan/91042-multi-…
wildan-m Aug 4, 2026
d7be269
Hydrate the report's actions when a thread resolves without them
wildan-m Aug 4, 2026
f0a606b
Make the report-preview action row layout testable
wildan-m Aug 4, 2026
95697ad
Read the action row style without an unsafe cast
wildan-m Aug 4, 2026
cd9a6da
Add UI regression tests for offline delete-pending rendering and onli…
wildan-m Aug 4, 2026
1bc259a
Stop a deferred expense press from hijacking a later one
wildan-m Aug 4, 2026
87ee423
Reword a comment so it passes spellcheck
wildan-m Aug 5, 2026
e00a24f
Cancel staged navigation when the user chooses something else
wildan-m Aug 5, 2026
b34fdbe
Cover the narrow fallback route and the RHP width mechanism
wildan-m Aug 5, 2026
501bafb
Merge remote-tracking branch 'upstream/main' into wildan/91042-multi-…
wildan-m Aug 5, 2026
b7d4a81
Open the report beneath the pressed expense on narrow layouts
wildan-m Aug 8, 2026
c28fc9c
Seed the expense view arrows in the order the cards are shown
wildan-m Aug 8, 2026
be5eb7e
Merge remote-tracking branch 'upstream/main' into wildan/91042-multi-…
wildan-m Aug 10, 2026
e402b55
Merge remote-tracking branch 'upstream/main' into wildan/91042-multi-…
wildan-m Aug 10, 2026
e65b116
Merge remote-tracking branch 'upstream/main' into wildan/91042-multi-…
wildan-m Aug 11, 2026
5942e24
Trim the report preview's comments to the non-obvious parts
wildan-m Aug 11, 2026
df54f87
Address review: release staged cascade state and fix the empty-refetc…
wildan-m Aug 11, 2026
7458fdd
Merge remote-tracking branch 'upstream/main' into wildan/91042-multi-…
wildan-m Aug 18, 2026
275de6a
Shorten the report preview's comments and follow upstream API changes
wildan-m Aug 18, 2026
bfbc745
Fix two carousel navigation bugs found in review
wildan-m Aug 19, 2026
3de5d94
Merge remote-tracking branch 'upstream/main' into wildan/91042-multi-…
wildan-m Aug 19, 2026
f49b296
Remove the report preview's explanatory comments
wildan-m Aug 20, 2026
8876f0c
Trim test comments and take the transaction seed off the navigation path
wildan-m Aug 20, 2026
c8dfe07
Merge remote-tracking branch 'upstream/main' into wildan/91042-multi-…
wildan-m Aug 21, 2026
1d8b6e0
Record why the pressed-expense cascade waits 180ms
wildan-m Aug 23, 2026
588ec9e
Wait for the parent action before minting a transaction thread from t…
wildan-m Aug 27, 2026
cf78629
Merge remote-tracking branch 'upstream/main' into wildan/91042-multi-…
wildan-m Aug 27, 2026
d12bdc3
Wait for the parent action before minting a transaction thread from t…
wildan-m Aug 27, 2026
44cf553
Merge remote-tracking branch 'upstream/main' into wildan/91042-multi-…
wildan-m Aug 27, 2026
e6eb279
Shorten the arrow guard comments
wildan-m Aug 31, 2026
0034a9e
Merge remote-tracking branch 'upstream/main' into wildan/91042-multi-…
wildan-m Aug 31, 2026
09cd833
Keep the newest seeded transaction IDs when the write is skipped
wildan-m Aug 31, 2026
40d90e1
Fetch the sibling's parent report instead of dropping an arrow press
wildan-m Aug 31, 2026
c3a3ef3
Open only one expense when two rows are pressed in quick succession
wildan-m Sep 1, 2026
8b04bff
Merge remote-tracking branch 'upstream/main' into wildan/91042-multi-…
wildan-m Sep 1, 2026
d555253
Merge remote-tracking branch 'upstream/main' into wildan/91042-multi-…
wildan-m Sep 1, 2026
cd75767
Stop re-rendering the preview on every IOU report action
wildan-m Sep 1, 2026
4a515ef
Keep the offline arrow working and release the report's super-wide hint
wildan-m Sep 1, 2026
841fec8
Merge remote-tracking branch 'upstream/main' into wildan/91042-multi-…
wildan-m Sep 1, 2026
4cd0066
Merge remote-tracking branch 'upstream/main' into wildan/91042-multi-…
wildan-m Sep 2, 2026
cbfe93c
Give the replay effect an explicit dependency list
wildan-m Sep 2, 2026
7588a5d
Drop two comments the field names already state
wildan-m Sep 3, 2026
145b823
Build preview navigation from the route captured at press time
wildan-m Sep 3, 2026
2924e97
Merge remote-tracking branch 'upstream/main' into wildan/91042-multi-…
wildan-m Sep 4, 2026
9a0130b
Merge remote-tracking branch 'upstream/main' into wildan/91042-multi-…
wildan-m Sep 4, 2026
5bba729
Merge remote-tracking branch 'upstream/main' into wildan/91042-multi-…
wildan-m Sep 4, 2026
0e29d3d
Keep the preview's carousel order for the expense arrows when the rep…
wildan-m Sep 5, 2026
be32e76
Merge remote-tracking branch 'upstream/main' into wildan/91042-multi-…
wildan-m Sep 7, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -602,9 +602,18 @@ function MoneyRequestReportTransactionList({
// "Recently added" flow) that belongs to the transaction thread sitting underneath this report.
// Overwriting and then clearing it would drop that carousel when the user navigates back. Row presses
// still seed the correct siblings lazily via useNavigateToTransactionThread.
if (getActiveTransactionIDs().descriptors) {
const {ids: activeIDs, descriptors: activeDescriptors} = getActiveTransactionIDs();
if (activeDescriptors) {
return;
}
// A report preview press seeds these arrows in the carousel's order, which can differ from this list's order.
// Keep that seed while it still covers exactly these rows, and re-seed only when the rows themselves change.
if (activeIDs && activeIDs.length === visualOrderTransactionIDs.length) {
const activeIDSet = new Set(activeIDs);
if (visualOrderTransactionIDs.every((transactionID) => activeIDSet.has(transactionID))) {
return;
}
}
setActiveTransactionIDs(visualOrderTransactionIDs);
return () => {
clearActiveTransactionIDs();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import PrevNextButtons from '@components/PrevNextButtons';
import {useWideRHPActions} from '@components/WideRHPContextProvider';

import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';

import {createTransactionThreadReport, setOptimisticTransactionThread} from '@libs/actions/Report';
import {createTransactionThreadReport, openReport, setOptimisticTransactionThread} from '@libs/actions/Report';
import {clearActiveTransactionIDs} from '@libs/actions/TransactionThreadNavigation';
import type {RightModalNavigatorParamList} from '@libs/Navigation/types';
import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
Expand All @@ -22,9 +23,9 @@ import getEmptyArray from '@src/types/utils/getEmptyArray';
import type {GestureResponderEvent} from 'react-native';
import type {OnyxCollection} from 'react-native-onyx';

import {findFocusedRoute} from '@react-navigation/native';
import {findFocusedRoute, useIsFocused} from '@react-navigation/native';
import {guidedSetupAndTourStatusSelector} from '@selectors/Onboarding';
import React, {startTransition, useCallback, useEffect, useMemo} from 'react';
import React, {startTransition, useCallback, useEffect, useMemo, useRef} from 'react';

type MoneyRequestReportRHPNavigationButtonsProps = {
currentTransactionID: string;
Expand All @@ -43,6 +44,10 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR

const {email: currentUserEmail, accountID: currentUserAccountID} = useCurrentUserPersonalDetails();
const {markReportRHPWidth} = useWideRHPActions();
const {isOffline} = useNetwork();
const isFocused = useIsFocused();

const pendingSiblingRef = useRef<{transactionID: string; originRoute: string} | null>(null);

const {prevTransactionID, nextTransactionID} = useMemo(() => {
if (!transactionIDsList || transactionIDsList.length < 2) {
Expand Down Expand Up @@ -125,9 +130,16 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR
};
}, []);

if (transactionIDsList.length < 2) {
return;
}
const stageSiblingPress = (transactionID: string | undefined, parentReportID: string | undefined) => {
// Offline there is no fetch to wait for, so the caller builds the thread optimistically instead.
if (!transactionID || !parentReportID || isOffline) {
return false;
}
pendingSiblingRef.current = {transactionID, originRoute: Navigation.getActiveRoute()};
// Always true here: we are fetching this report's actions, so it must not overwrite its cached name.
openReport({reportID: parentReportID, introSelected, conciergeChat, betas, currentUserAccountID, hasReportActions: true});
return true;
};

const onNext = (e: GestureResponderEvent | KeyboardEvent | undefined) => {
e?.preventDefault();
Expand Down Expand Up @@ -170,6 +182,11 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR
return;
}

// A thread created before the parent action loads would have no parent, so wait for the fetch.
if (!nextParentReportAction && stageSiblingPress(nextTransactionID, nextTransaction?.reportID)) {
return;
}

const nextThreadReportID = nextParentReportAction?.childReportID;
const navigationParams = {
reportID: nextThreadReportID,
Expand Down Expand Up @@ -245,6 +262,11 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR
return;
}

// A thread created before the parent action loads would have no parent, so wait for the fetch.
if (!prevParentReportAction && stageSiblingPress(prevTransactionID, prevTransaction?.reportID)) {
return;
}

const prevThreadReportID = prevParentReportAction?.childReportID;
const navigationParams = {
reportID: prevThreadReportID,
Expand Down Expand Up @@ -282,6 +304,33 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR
});
};

// Replays a staged press once its parent action arrives, but only if the user is still where they pressed —
// this screen stays mounted under a pushed RHP, and resuming from there would yank them out with a stale backTo.
useEffect(() => {
const pending = pendingSiblingRef.current;
if (!pending) {
return;
}
if (!isFocused || Navigation.getActiveRoute() !== pending.originRoute) {
pendingSiblingRef.current = null;
return;
}
if (pending.transactionID === nextTransactionID && nextParentReportAction) {
pendingSiblingRef.current = null;
onNext(undefined);
return;
}
if (pending.transactionID === prevTransactionID && prevParentReportAction) {
pendingSiblingRef.current = null;
onPrevious(undefined);
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- onNext/onPrevious are rebuilt every render, so listing them would defeat the dependency list
}, [isFocused, nextTransactionID, nextParentReportAction, prevTransactionID, prevParentReportAction]);

if (transactionIDsList.length < 2) {
return;
}

return (
<PrevNextButtons
isPrevButtonDisabled={!prevTransactionID}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ function MoneyRequestReportPreviewContent({
invoiceReceiverPersonalDetail,
lastTransactionViolations,
renderTransactionItem,
onOrderedTransactionsChange,
onCancelPendingPress,
onCarouselLayout,
onWrapperLayout,
currentWidth,
Expand All @@ -57,6 +59,8 @@ function MoneyRequestReportPreviewContent({
onPaymentOptionsShow={onPaymentOptionsShow}
onPaymentOptionsHide={onPaymentOptionsHide}
renderTransactionItem={renderTransactionItem}
onOrderedTransactionsChange={onOrderedTransactionsChange}
onCancelPendingPress={onCancelPendingPress}
currentWidth={currentWidth}
reportPreviewStyles={reportPreviewStyles}
newTransactionIDs={newTransactionIDs}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
ReportPreviewTransactionViolationsContext,
ReportPreviewUIStateContext,
} from './MoneyRequestReportPreviewContext';
import resolvePressOrigin from './resolvePressOrigin';
import usePreviewMessageAnimation from './usePreviewMessageAnimation';
import useReportPreviewActionDecision from './useReportPreviewActionDecision';
import useReportPreviewCarousel from './useReportPreviewCarousel';
Expand All @@ -68,6 +69,8 @@ type MoneyRequestReportPreviewProviderProps = ChildrenProps & {
onPaymentOptionsShow?: () => void;
onPaymentOptionsHide?: () => void;
renderTransactionItem: ListRenderItem<Transaction>;
onOrderedTransactionsChange?: (orderedTransactions: Transaction[]) => void;
onCancelPendingPress?: () => void;
currentWidth: number;
reportPreviewStyles: MoneyRequestReportPreviewStyleType;
newTransactionIDs?: Set<string>;
Expand Down Expand Up @@ -96,6 +99,8 @@ function MoneyRequestReportPreviewProvider({
onPaymentOptionsShow,
onPaymentOptionsHide,
renderTransactionItem,
onOrderedTransactionsChange,
onCancelPendingPress,
currentWidth,
reportPreviewStyles,
newTransactionIDs,
Expand Down Expand Up @@ -202,29 +207,35 @@ function MoneyRequestReportPreviewProvider({
currentWidth,
newTransactionIDs,
renderTransactionItem,
onOrderedTransactionsChange,
});

const openReportFromPreview = useCallback(() => {
if (!iouReportID) {
return;
}
const routeAtPress = Navigation.getActiveRoute();
onCancelPendingPress?.();

// "View" pressed inside the cascade window lands on a report a card press already opened, so there is
// nothing left to push; pushing it again would nest the report under itself.
const {wasPressedFromReport, backTo} = resolvePressOrigin(routeAtPress, isSmallScreenWidth ? `r/${iouReportID}` : `e/${iouReportID}`);
if (wasPressedFromReport) {
return;
}

startSpan(`${CONST.TELEMETRY.SPAN_OPEN_REPORT}_${iouReportID}`, {
name: 'MoneyRequestReportPreviewContent',
op: CONST.TELEMETRY.SPAN_OPEN_REPORT,
});
// Small screens navigate to full report view since super wide RHP
// is not available on narrow layouts and would break the navigation logic.
if (isSmallScreenWidth) {
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID, undefined, undefined, Navigation.getActiveRoute()));
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(iouReportID, undefined, undefined, backTo));
} else {
Navigation.navigate(
ROUTES.EXPENSE_REPORT_RHP.getRoute({
reportID: iouReportID,
backTo: Navigation.getActiveRoute(),
}),
);
Navigation.navigate(ROUTES.EXPENSE_REPORT_RHP.getRoute({reportID: iouReportID, backTo}));
}
}, [iouReportID, isSmallScreenWidth]);
}, [iouReportID, isSmallScreenWidth, onCancelPendingPress]);

const onHoldMenuOpen = useCallback((requestType: string, paymentType?: PaymentMethodType, canPay?: boolean, methodID?: number) => {
if (requestType !== CONST.IOU.REPORT_ACTION_TYPE.PAY && requestType !== CONST.IOU.REPORT_ACTION_TYPE.APPROVE) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import Button from '@components/ButtonComposed';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';

import variables from '@styles/variables';

import CONST from '@src/CONST';

import React from 'react';
Expand All @@ -24,7 +22,16 @@ function ReportPreviewActionButton() {
const {buttonMaxWidth} = useReportPreviewUIState();
const {openReportFromPreview} = useReportPreviewActions();

const renderButton = () => {
const viewButton = (
<Button
onPress={openReportFromPreview}
sentryLabel={CONST.SENTRY_LABEL.REPORT_PREVIEW.VIEW_BUTTON}
>
<Button.Text>{translate('common.view')}</Button.Text>
</Button>
);

const renderPrimaryButton = () => {
if (reportPreviewAction === CONST.REPORT.REPORT_PREVIEW_ACTIONS.SUBMIT) {
return <SubmitActionButton />;
}
Expand All @@ -45,17 +52,21 @@ function ReportPreviewActionButton() {
return <AddExpenseActionButton />;
}

return (
<Button
onPress={openReportFromPreview}
sentryLabel={CONST.SENTRY_LABEL.REPORT_PREVIEW.VIEW_BUTTON}
>
<Button.Text>{translate('common.view')}</Button.Text>
</Button>
);
return null;
};

return <View style={[buttonMaxWidth, styles.flex1, {height: variables.h40}]}>{renderButton()}</View>;
const primaryButton = renderPrimaryButton();

if (!primaryButton) {
return <View style={[buttonMaxWidth, styles.flex1, styles.reportPreviewActionRow]}>{viewButton}</View>;
}

return (
<View style={[buttonMaxWidth, styles.flex1, styles.flexRow, styles.gap2, styles.reportPreviewActionRow]}>
<View style={[styles.flex1]}>{primaryButton}</View>
{viewButton}
</View>
);
}

export default ReportPreviewActionButton;
Loading
Loading