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
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ function MoneyRequestReportNavigationContent({reportID, shouldDisplayNarrowVersi
});
Navigation.setParams({
reportID: reportId,
reportActionID: undefined,
referrer: undefined,
});
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR
backTo = params?.backTo ?? backTo;
}
const nextThreadReportID = nextParentReportAction?.childReportID;
const navigationParams = {reportID: nextThreadReportID, backTo};
const navigationParams = {reportID: nextThreadReportID, reportActionID: undefined, backTo};

if (nextThreadReportID) {
markReportIDAsExpense(nextThreadReportID);
Expand Down Expand Up @@ -161,7 +161,7 @@ function MoneyRequestReportTransactionsNavigation({currentTransactionID, isFromR
backTo = params?.backTo ?? backTo;
}
const prevThreadReportID = prevParentReportAction?.childReportID;
const navigationParams = {reportID: prevThreadReportID, backTo};
const navigationParams = {reportID: prevThreadReportID, reportActionID: undefined, backTo};

if (prevThreadReportID) {
markReportIDAsExpense(prevThreadReportID);
Expand Down
20 changes: 13 additions & 7 deletions src/components/ParentNavigationSubtitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,14 +186,12 @@ function ParentNavigationSubtitle({
// avoid stacking RHPs by going back to the search report if it's already there
const previousRoute = currentFocusedNavigator?.state?.routes.at(-2);

if (previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT && lastRoute?.name === SCREENS.RIGHT_MODAL.EXPENSE_REPORT) {
if (previousRoute.params && 'reportID' in previousRoute.params) {
const reportIDFromParams = previousRoute.params.reportID;
if (previousRoute?.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT && previousRoute.params && 'reportID' in previousRoute.params) {
const reportIDFromParams = previousRoute.params.reportID;

if (reportIDFromParams === parentReportID) {
Navigation.goBack();
return;
}
if (reportIDFromParams === parentReportID) {
Navigation.goBack();
return;
}
}

Expand All @@ -213,6 +211,14 @@ function ParentNavigationSubtitle({
return;
}
}

// Stay in the Search tab when the parent link is tapped from a SEARCH_REPORT RHP
// and the parent isn't already in the stack — otherwise the REPORT_WITH_ID fallback
// would yank the user to Inbox.
if (isReportInRHP) {
Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: parentReportID, reportActionID: isVisibleAction ? parentReportActionID : undefined}));
return;
}
}

// If the parent report is already the previous screen in the main stack, go back to it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) {
const containerRef = useRef(null);
const isExecutingRef = useRef<boolean>(false);
const screenOptions = useRHPScreenOptions();
const {superWideRHPRouteKeys, shouldRenderTertiaryOverlay} = useWideRHPState();
const {superWideRHPRouteKeys, wideRHPRouteKeys, shouldRenderTertiaryOverlay} = useWideRHPState();
const {clearWideRHPKeys, syncRHPKeys} = useWideRHPActions();
const {windowWidth} = useWindowDimensions();
const modalStackScreenOptions = useModalStackScreenOptions();
Expand All @@ -130,7 +130,7 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) {

// Animation should be disabled when we open the wide rhp from the narrow one.
// When the wide rhp page is opened as first one, it will be animated with the entire RightModalNavigator.
const animationEnabledOnSearchReport = superWideRHPRouteKeys.length > 0 || isSmallScreenWidth;
const animationEnabledOnSearchReport = superWideRHPRouteKeys.length > 0 || wideRHPRouteKeys.length > 0 || isSmallScreenWidth;

const animatedWidth = expandedRHPProgress.interpolate({
inputRange: [0, 1, 2],
Expand Down
24 changes: 24 additions & 0 deletions src/libs/Navigation/helpers/getTopmostFullScreenRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {navigationRef} from '@libs/Navigation/Navigation';
import type {NavigationRoute, RootNavigatorParamList, State} from '@libs/Navigation/types';
import NAVIGATORS from '@src/NAVIGATORS';

/**
* Returns the active tab route of the topmost TAB_NAVIGATOR in the root navigation state.
* Use this to determine which full-screen tab (Search, Inbox, etc.) is currently focused.
*/
function getTopmostFullScreenRoute(): NavigationRoute | undefined {
const rootState = navigationRef.getRootState() as State<RootNavigatorParamList>;

if (!rootState) {
return undefined;
}

const topmostTabNavigatorRoute = rootState.routes.findLast((route) => route.name === NAVIGATORS.TAB_NAVIGATOR);
if (!topmostTabNavigatorRoute?.state) {
return undefined;
}
const index = topmostTabNavigatorRoute.state.index ?? 0;
return topmostTabNavigatorRoute.state.routes?.at(index);
}

export default getTopmostFullScreenRoute;
14 changes: 4 additions & 10 deletions src/libs/Navigation/helpers/isReportTopmostSplitNavigator.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
import {navigationRef} from '@libs/Navigation/Navigation';
import type {RootNavigatorParamList, State} from '@libs/Navigation/types';
import NAVIGATORS from '@src/NAVIGATORS';
import getActiveTabName from './getActiveTabName';
import {isFullScreenName} from './isNavigatorName';
import getTopmostFullScreenRoute from './getTopmostFullScreenRoute';

const isReportTopmostSplitNavigator = (): boolean => {
const rootState = navigationRef.getRootState() as State<RootNavigatorParamList>;

if (!rootState) {
const topmostFullScreenRoute = getTopmostFullScreenRoute();
if (!topmostFullScreenRoute) {
return false;
}

const topmostFullScreenRoute = rootState.routes.findLast((route) => isFullScreenName(route.name));
return getActiveTabName(topmostFullScreenRoute) === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR;
return topmostFullScreenRoute.name === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR;
};

export default isReportTopmostSplitNavigator;
14 changes: 4 additions & 10 deletions src/libs/Navigation/helpers/isSearchTopmostFullScreenRoute.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
import {navigationRef} from '@libs/Navigation/Navigation';
import type {RootNavigatorParamList, State} from '@libs/Navigation/types';
import NAVIGATORS from '@src/NAVIGATORS';
import getActiveTabName from './getActiveTabName';
import {isFullScreenName} from './isNavigatorName';
import getTopmostFullScreenRoute from './getTopmostFullScreenRoute';

const isSearchTopmostFullScreenRoute = (): boolean => {
const rootState = navigationRef.getRootState() as State<RootNavigatorParamList>;

if (!rootState) {
const topmostFullScreenRoute = getTopmostFullScreenRoute();
if (!topmostFullScreenRoute) {
return false;
}

const topmostFullScreenRoute = rootState.routes.findLast((route) => isFullScreenName(route.name));
return getActiveTabName(topmostFullScreenRoute) === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR;
return topmostFullScreenRoute.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR;
};

export default isSearchTopmostFullScreenRoute;
13 changes: 13 additions & 0 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10248,6 +10248,19 @@ function navigateToLinkedReportAction(
visibleReportActionsData?: VisibleReportActionsDerivedValue,
) {
if (isInNarrowPaneModal) {
const rootState = navigationRef.current?.getRootState();
const topmostRHP = rootState?.routes.findLast((route) => route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR);
const rhpState = topmostRHP?.state;
const rhpIndex = rhpState?.index ?? (rhpState?.routes.length ? rhpState.routes.length - 1 : -1);
const routeBelow = rhpIndex > 0 ? rhpState?.routes.at(rhpIndex - 1) : undefined;
const routeBelowParams = routeBelow?.params as {reportID?: string} | undefined;

if (routeBelow?.key && routeBelowParams?.reportID === ancestor.report.reportID) {
Navigation.setParams({reportActionID: ancestor.reportAction.reportActionID}, routeBelow.key, rhpState?.key);
Navigation.goBack();
return;
}

Navigation.navigate(
ROUTES.SEARCH_REPORT.getRoute({
reportID: ancestor.report.reportID,
Expand Down
13 changes: 11 additions & 2 deletions src/libs/actions/Report/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import Log from '@libs/Log';
import {isEmailPublicDomain} from '@libs/LoginUtils';
import {getMovedReportID} from '@libs/ModifiedExpenseMessage';
import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute';
import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
import type {LinkToOptions} from '@libs/Navigation/helpers/linkTo/types';
import Navigation from '@libs/Navigation/Navigation';
import enhanceParameters from '@libs/Network/enhanceParameters';
Expand Down Expand Up @@ -2440,7 +2441,11 @@ function navigateToAndOpenChildReport(
) {
const report = childReport ?? createChildReport(childReport, parentReportAction, parentReport, currentUserAccountID, introSelected, betas, isSelfTourViewed, personalDetails);

Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID, undefined, undefined, Navigation.getActiveRoute()));
if (isSearchTopmostFullScreenRoute()) {
Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: report.reportID, backTo: Navigation.getActiveRoute()}));
} else {
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID, undefined, undefined, Navigation.getActiveRoute()));
}
}

/**
Expand Down Expand Up @@ -2533,7 +2538,11 @@ function explain(
// Check if explanation thread report already exists
const report = childReport ?? createChildReport(childReport, reportAction, originalReport, currentUserAccountID, introSelected, betas, isSelfTourViewed);

Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID, undefined, undefined, Navigation.getActiveRoute()));
if (isSearchTopmostFullScreenRoute()) {
Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID: report.reportID, backTo: Navigation.getActiveRoute()}));
} else {
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(report.reportID, undefined, undefined, Navigation.getActiveRoute()));
}
// Schedule adding the explanation comment on the next animation frame
// so it runs immediately after navigation completes.
requestAnimationFrame(() => {
Expand Down
2 changes: 2 additions & 0 deletions src/pages/inbox/HeaderView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ function HeaderView({onNavigationMenuButtonClicked, reportID}: HeaderViewProps)
const {isSmallScreenWidth, shouldUseNarrowLayout, isInLandscapeMode} = useResponsiveLayout();
const isInSidePanel = useIsInSidePanel();
const route = useRoute();
const openParentReportInCurrentTab = route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT;
const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(report?.parentReportID) ?? getNonEmptyStringOnyxID(report?.reportID)}`);
const [grandParentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(parentReport?.parentReportID)}`);
const grandParentReportAction = useParentReportAction(parentReport);
Expand Down Expand Up @@ -346,6 +347,7 @@ function HeaderView({onNavigationMenuButtonClicked, reportID}: HeaderViewProps)
parentReportID={parentNavigationReport?.parentReportID}
parentReportActionID={isParentOneTransactionThread ? undefined : parentNavigationReport?.parentReportActionID}
pressableStyles={[styles.alignSelfStart, styles.mw100]}
openParentReportInCurrentTab={openParentReportInCurrentTab}
humanAgentAccountID={humanAgentAccountID}
humanAgentName={humanAgentName}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,9 @@ function ComposerWithSuggestions({

const commentRef = useRef(initialText);

const {superWideRHPRouteKeys} = useWideRHPState();
// When SearchReport is stacked above another RHP, delay autofocus until after the transition completes to avoid animation jank
const shouldDelayAutoFocus = superWideRHPRouteKeys.length > 0 && route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT;
const {superWideRHPRouteKeys, wideRHPRouteKeys} = useWideRHPState();
// When SearchReport is stacked above another RHP (wide or super-wide), delay autofocus until after the transition completes to avoid animation jank
const shouldDelayAutoFocus = (superWideRHPRouteKeys.length > 0 || wideRHPRouteKeys.length > 0) && route.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT;
const shouldDelayAutoFocusRef = useRef(shouldDelayAutoFocus);
shouldDelayAutoFocusRef.current = shouldDelayAutoFocus;

Expand Down
93 changes: 93 additions & 0 deletions tests/unit/Navigation/getTopmostFullScreenRouteTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import getTopmostFullScreenRoute from '@libs/Navigation/helpers/getTopmostFullScreenRoute';
import NAVIGATORS from '@src/NAVIGATORS';

const mockGetRootState = jest.fn();

jest.mock('@libs/Navigation/Navigation', () => ({
navigationRef: {
getRootState: () => mockGetRootState() as unknown,
},
}));

describe('getTopmostFullScreenRoute', () => {
beforeEach(() => {
mockGetRootState.mockReset();
});

it('returns undefined when there is no root state', () => {
mockGetRootState.mockReturnValue(undefined);
expect(getTopmostFullScreenRoute()).toBeUndefined();
});

it('returns undefined when there is no TAB_NAVIGATOR route in the root state', () => {
mockGetRootState.mockReturnValue({
routes: [{name: NAVIGATORS.RIGHT_MODAL_NAVIGATOR}, {name: NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR}],
});
expect(getTopmostFullScreenRoute()).toBeUndefined();
});

it('returns undefined when the TAB_NAVIGATOR has no nested state yet', () => {
mockGetRootState.mockReturnValue({
routes: [{name: NAVIGATORS.TAB_NAVIGATOR}],
});
expect(getTopmostFullScreenRoute()).toBeUndefined();
});

it('returns the focused tab route based on state.index', () => {
const reportsRoute = {name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR};
const searchRoute = {name: NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR};
mockGetRootState.mockReturnValue({
routes: [
{
name: NAVIGATORS.TAB_NAVIGATOR,
state: {
index: 1,
routes: [reportsRoute, searchRoute],
},
},
],
});
expect(getTopmostFullScreenRoute()).toBe(searchRoute);
});

it('falls back to the first child route when state.index is missing', () => {
const reportsRoute = {name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR};
const searchRoute = {name: NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR};
mockGetRootState.mockReturnValue({
routes: [
{
name: NAVIGATORS.TAB_NAVIGATOR,
state: {
routes: [reportsRoute, searchRoute],
},
},
],
});
expect(getTopmostFullScreenRoute()).toBe(reportsRoute);
});

it('returns the focused tab of the topmost TAB_NAVIGATOR when multiple exist', () => {
const oldFocused = {name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR};
const newFocused = {name: NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR};
mockGetRootState.mockReturnValue({
routes: [
{
name: NAVIGATORS.TAB_NAVIGATOR,
state: {
index: 0,
routes: [oldFocused],
},
},
{name: NAVIGATORS.RIGHT_MODAL_NAVIGATOR},
{
name: NAVIGATORS.TAB_NAVIGATOR,
state: {
index: 0,
routes: [newFocused],
},
},
],
});
expect(getTopmostFullScreenRoute()).toBe(newFocused);
});
});
Loading