Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ee4c182
Pass NVPs to last accessed report calls
ShridharGoel Aug 16, 2026
44d2d8f
Fix report NVP collection name
ShridharGoel Aug 16, 2026
1364c4a
Refresh warm-link report NVPs
ShridharGoel Aug 16, 2026
d74cca1
refactor(report-utils): return slice from findLastAccessedReport
ShridharGoel Aug 23, 2026
c6b9466
Merge origin/main into refactor/66422-last-accessed-report-nvp
ShridharGoel Aug 23, 2026
a74cc2f
refactor(report-utils): trim findLastAccessedReport comments
ShridharGoel Aug 23, 2026
05b073c
refactor(report-utils): drop unused exports
ShridharGoel Aug 23, 2026
f6ce10d
Encapsulate last accessed report lookup in useFindLastAccessedReport …
ShridharGoel Aug 23, 2026
3c15cfe
Trim verbose hook doc and drop stale pattern reference
ShridharGoel Aug 23, 2026
aee6b5c
Fix knip, eslint and typecheck on useFindLastAccessedReport
ShridharGoel Aug 23, 2026
4c526cc
Merge duplicate ReportUtils type imports in navigateAfterOnboardingTest
ShridharGoel Aug 23, 2026
b69fe5d
Merge branch 'main' of github.com:Expensify/App into refactor/66422-l…
ShridharGoel Aug 24, 2026
369410b
Pass last accessed report ID instead of NVP collection
ShridharGoel Aug 25, 2026
6e9a90a
Derive last accessed report options inside the hook
ShridharGoel Aug 25, 2026
95941fd
Look up last accessed report on leave and delete tap
ShridharGoel Sep 2, 2026
6cea4b2
Merge remote-tracking branch 'origin/main' into refactor/66422-last-a…
ShridharGoel Sep 2, 2026
5d2f9c3
Drop empty-string last accessed report IDs and rebuild GH action bundle
ShridharGoel Sep 2, 2026
3c844a4
Merge remote-tracking branch 'origin/main' into refactor/66422-last-a…
ShridharGoel Sep 2, 2026
7f3d858
Use nullish coalescing for last accessed report fallback
ShridharGoel Sep 2, 2026
53f9832
Restore proposalPoliceComment GH action bundle from main
ShridharGoel Sep 2, 2026
80b3704
Merge remote-tracking branch 'origin/main' into refactor/66422-last-a…
ShridharGoel Sep 2, 2026
783faa9
Address review: hook returns reportID, drop selector layer, on-tap lo…
ShridharGoel Sep 10, 2026
691a926
Merge branch 'main' of github.com:Expensify/App into refactor/66422-l…
ShridharGoel Sep 10, 2026
491c6a4
Use useFindLastAccessedReport in navigator
ShridharGoel Sep 18, 2026
6b1f247
Merge branch 'main' into refactor/66422-last-accessed-report-nvp
ShridharGoel Sep 18, 2026
3cf5b60
Fix no-default-id-values in navigator
ShridharGoel Sep 18, 2026
f0e714f
Fix Link mock in DeepLinkHandlerTest
ShridharGoel Sep 18, 2026
364edf1
Merge branch 'main' into refactor/66422-last-accessed-report-nvp
ShridharGoel Sep 21, 2026
55099e7
Merge branch 'main' into refactor/66422-last-accessed-report-nvp
ShridharGoel Sep 21, 2026
cae920b
Fix formatting and drop unused navigator imports
ShridharGoel Sep 21, 2026
1a0b6fe
Drop redundant excludeReportID from hook
ShridharGoel Sep 22, 2026
35acbfc
Drop redundant openOnAdminRoom from navigator hook
ShridharGoel Sep 22, 2026
b56aeaf
Merge remote-tracking branch 'origin/main' into refactor/66422-last-a…
ShridharGoel Sep 22, 2026
377c796
Merge remote-tracking branch 'origin/main' into refactor/66422-last-a…
ShridharGoel Sep 22, 2026
bdf90a8
Use line comments in useFindLastAccessedReport
ShridharGoel Sep 22, 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
21 changes: 20 additions & 1 deletion src/DeepLinkHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ function DeepLinkHandler({onInitialUrl}: DeepLinkHandlerProps) {
const hasRefetchedPublicRoom = useRef(false);

const [allReports, allReportsMetadata] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
const [reportNameValuePairs, reportNameValuePairsMetadata] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS);
const reportNameValuePairsRef = useRef(reportNameValuePairs);
const [isLoadingApp = true] = useOnyx(ONYXKEYS.IS_LOADING_APP);
const [session, sessionMetadata] = useOnyx(ONYXKEYS.SESSION);
const [conciergeReportID, conciergeReportIDMetadata] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID);
Expand All @@ -45,6 +47,10 @@ function DeepLinkHandler({onInitialUrl}: DeepLinkHandlerProps) {
const [betas, betasMetadata] = useOnyx(ONYXKEYS.BETAS);
const isAuthenticated = useIsAuthenticated();

useEffect(() => {
reportNameValuePairsRef.current = reportNameValuePairs;
}, [reportNameValuePairs]);

// An anonymous deep link into a public room needs to be re-fetched after OpenApp settles (see the effect
// below). Track the pending reportID so both the initial-URL and the url-change paths stay in sync.
const trackPendingPublicRoomFromDeepLink = useCallback((url: string, isCurrentlyAuthenticated: boolean) => {
Expand All @@ -57,7 +63,17 @@ function DeepLinkHandler({onInitialUrl}: DeepLinkHandlerProps) {
}, []);

useEffect(() => {
if (isLoadingOnyxValue(allReportsMetadata, sessionMetadata, conciergeReportIDMetadata, introSelectedMetadata, guidedSetupAndTourStatusMetadata, betasMetadata)) {
if (
isLoadingOnyxValue(
allReportsMetadata,
reportNameValuePairsMetadata,
sessionMetadata,
conciergeReportIDMetadata,
introSelectedMetadata,
guidedSetupAndTourStatusMetadata,
betasMetadata,
)
) {
return;
}

Expand Down Expand Up @@ -114,6 +130,7 @@ function DeepLinkHandler({onInitialUrl}: DeepLinkHandlerProps) {
guidedSetupAndTourStatus?.isSelfTourViewed,
betas,
session?.accountID ?? CONST.DEFAULT_NUMBER_ID,
reportNameValuePairsRef.current,
);
trackPendingPublicRoomFromDeepLink(url, isCurrentlyAuthenticated);
} else {
Expand Down Expand Up @@ -157,6 +174,7 @@ function DeepLinkHandler({onInitialUrl}: DeepLinkHandlerProps) {
guidedSetupAndTourStatus?.isSelfTourViewed,
betas,
session?.accountID ?? CONST.DEFAULT_NUMBER_ID,
reportNameValuePairsRef.current,
);
trackPendingPublicRoomFromDeepLink(state.url, isCurrentlyAuthenticated);
});
Expand All @@ -172,6 +190,7 @@ function DeepLinkHandler({onInitialUrl}: DeepLinkHandlerProps) {
introSelected,
betas,
allReportsMetadata.status,
reportNameValuePairsMetadata.status,
sessionMetadata.status,
conciergeReportIDMetadata.status,
introSelectedMetadata.status,
Expand Down
30 changes: 30 additions & 0 deletions src/hooks/useFindLastAccessedReport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import shouldOpenOnAdminRoom from '@libs/Navigation/helpers/shouldOpenOnAdminRoom';
import {findLastAccessedReport} from '@libs/ReportUtils';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';

import useOnyx from './useOnyx';
import usePermissions from './usePermissions';

type UseFindLastAccessedReportOptions = {
// Prefer the policy admins room when one exists. Defaults to the openOnAdminRoom URL param.
openOnAdminRoom?: boolean;
// When false, skip the lookup and return undefined without scanning reports.
enabled?: boolean;
};

// Resolves the last accessed report for navigation fallbacks from a view-scoped subscription.
function useFindLastAccessedReport({openOnAdminRoom, enabled = true}: UseFindLastAccessedReportOptions = {}) {
const {isBetaEnabled} = usePermissions();
const ignoreDomainRooms = !isBetaEnabled(CONST.BETAS.DEFAULT_ROOMS);
const shouldPreferAdminRoom = openOnAdminRoom ?? shouldOpenOnAdminRoom();
const [guideAccountIDs] = useOnyx(ONYXKEYS.DERIVED.GUIDE_ACCOUNT_IDS);
const [reportNameValuePairs, reportNameValuePairsMetadata] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS);
const [lastAccessedReportID, reportsMetadata] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {
selector: (reports) => (enabled ? findLastAccessedReport(ignoreDomainRooms, guideAccountIDs, shouldPreferAdminRoom, undefined, reportNameValuePairs, reports)?.reportID : undefined),
});
return {lastAccessedReportID, reportsMetadata, reportNameValuePairsMetadata};
}

export default useFindLastAccessedReport;
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import useFindLastAccessedReport from '@hooks/useFindLastAccessedReport';
import {IsInPreloadedTabContext} from '@hooks/useIsInPreloadedTab';
import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';

import createSplitNavigator from '@libs/Navigation/AppNavigator/createSplitNavigator';
import FreezeWrapper from '@libs/Navigation/AppNavigator/FreezeWrapper';
Expand All @@ -10,13 +9,11 @@ import shouldOpenOnAdminRoom from '@libs/Navigation/helpers/shouldOpenOnAdminRoo
import {isTabRoutePreloaded} from '@libs/Navigation/helpers/tabNavigatorUtils';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {NavigationStateRoute, ReportsSplitNavigatorParamList, TabNavigatorParamList} from '@libs/Navigation/types';
import * as ReportUtils from '@libs/ReportUtils';

import type {ReportScreenProps} from '@pages/inbox/ReportScreen';

import CONST from '@src/CONST';
import type NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import type ReactComponentModule from '@src/types/utils/ReactComponentModule';
Expand All @@ -33,25 +30,29 @@ const Split = createSplitNavigator<ReportsSplitNavigatorParamList>();
* There can be multiple report screens in the stack with different report IDs.
*/
function ReportsSplitNavigator({navigation, route}: PlatformStackScreenProps<TabNavigatorParamList, typeof NAVIGATORS.REPORTS_SPLIT_NAVIGATOR>) {
const {isBetaEnabled} = usePermissions();
const splitNavigatorScreenOptions = useSplitNavigatorScreenOptions(SCREENS.INBOX);
const [reportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS);
const isOpenOnAdminRoom = shouldOpenOnAdminRoom();
const shouldClearInitialReportActionsDefer = !!route.params && 'shouldDeferInitialReportActions' in route.params && route.params.shouldDeferInitialReportActions === true;
const [shouldDeferInitialReportActions] = useState(() => shouldClearInitialReportActionsDefer);
const isInPreloadedTab = useNavigationState((tabState) => isTabRoutePreloaded(tabState, route.key));

const routeReportID = route.params && 'screen' in route.params && route.params.screen === SCREENS.REPORT ? route.params.params?.reportID : undefined;
const currentURL = getCurrentUrl();
const isTransitioning = currentURL.includes(ROUTES.TRANSITION_BETWEEN_APPS);
const reportIdFromPath = currentURL ? new URL(currentURL).pathname.match(CONST.REGEX.REPORT_ID_FROM_PATH)?.at(1) : undefined;
const shouldResolveReportID = !routeReportID && !reportIdFromPath && !isTransitioning;

const {lastAccessedReportID} = useFindLastAccessedReport({
enabled: shouldResolveReportID,
});

const [initialReportID] = useState(() => {
// Deep links and REPORT_WITH_ID navigation pass the reportID in nested params,
// which lets us skip the O(n) findLastAccessedReport scan over all reports.
if (route.params && 'screen' in route.params && route.params.screen === SCREENS.REPORT && route.params.params?.reportID) {
return route.params.params.reportID;
// which lets us skip the scan over all reports.
if (routeReportID) {
return routeReportID;
}

const currentURL = getCurrentUrl();
const isTransitioning = currentURL.includes(ROUTES.TRANSITION_BETWEEN_APPS);

const reportIdFromPath = currentURL ? new URL(currentURL).pathname.match(CONST.REGEX.REPORT_ID_FROM_PATH)?.at(1) : undefined;
if (reportIdFromPath) {
return reportIdFromPath;
}
Expand All @@ -62,10 +63,8 @@ function ReportsSplitNavigator({navigation, route}: PlatformStackScreenProps<Tab
return '';
}

// TODO: Pass guideAccountIDs once callers are fully migrated — PR 33 (https://github.com/Expensify/App/issues/66413); findLastAccessedReport falls back to hasExpensifyGuidesEmails → allPersonalDetails
const initialReport = ReportUtils.findLastAccessedReport(!isBetaEnabled(CONST.BETAS.DEFAULT_ROOMS), undefined, isOpenOnAdminRoom, undefined, reportNameValuePairs);
// eslint-disable-next-line rulesdir/no-default-id-values
return initialReport?.reportID ?? '';
return lastAccessedReportID ?? '';
});

useEffect(() => {
Expand Down
36 changes: 21 additions & 15 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2491,13 +2491,21 @@ function getMostRecentlyVisitedReport(reports: Array<OnyxEntry<Report>>, lastVis
return lodashMaxBy(filteredReports, (a) => [(a?.reportID && lastVisitTimes?.[a.reportID]) ?? '', a?.lastReadTime ?? '']);
}

/** Fields of a Report that `findLastAccessedReport` callers consume. */
type LastAccessedReport = Pick<Report, 'reportID' | 'policyID' | 'chatType'>;

function toLastAccessedReport(report: OnyxEntry<Report>): LastAccessedReport | undefined {
if (!report?.reportID) {
return undefined;
}
return {reportID: report.reportID, policyID: report.policyID, chatType: report.chatType};
}

/**
* This function is used to find the last accessed report and we don't need to subscribe the data in the UI.
* So please use `Onyx.connectWithoutView()` to get the necessary data when we remove the `Onyx.connect()`
*
* Callers that need to react to the report data arriving (rather than reading whatever the module-scoped
* copy happens to hold at call time) can pass their own subscribed collections through `reportNameValuePairs`
* and `reports`. Both fall back to the module-scoped copies when omitted.
* Finds the last accessed report for navigation fallbacks using the module-scoped
* Onyx subscriptions (non-UI, no view-based re-renders). `reportNameValuePairs` and
* `reports` let a caller pass its own subscribed collections and fall back to the
* module-scoped copies when omitted. Returns a minimal slice, not the full Report.
*/
function findLastAccessedReport(
ignoreDomainRooms: boolean,
Expand All @@ -2506,17 +2514,14 @@ function findLastAccessedReport(
excludeReportID?: string,
reportNameValuePairs?: OnyxCollection<ReportNameValuePairs>,
reports?: OnyxCollection<Report>,
): OnyxEntry<Report> {
): LastAccessedReport | undefined {
const reportNameValuePairsCollection = reportNameValuePairs ?? allReportNameValuePair;
let reportsValues = Object.values(reports ?? deprecatedAllReports ?? {});

if (openOnAdminRoom) {
const adminReport = reportsValues.find((report) => {
const chatType = getChatType(report);
return chatType === CONST.REPORT.CHAT_TYPE.POLICY_ADMINS;
});
const adminReport = reportsValues.find((report) => getChatType(report) === CONST.REPORT.CHAT_TYPE.POLICY_ADMINS);
if (adminReport) {
return adminReport;
return toLastAccessedReport(adminReport);
Comment thread
ShridharGoel marked this conversation as resolved.
}
}

Expand Down Expand Up @@ -2556,11 +2561,11 @@ function findLastAccessedReport(
const visibleReports = reportsValues.filter((report) => !!report?.isPinned || !isHiddenForCurrentUser(report) || (isPublicRoom(report) && isAnonymousUserSession()));
const ownedReports = visibleReports.filter((report) => report?.ownerAccountID === deprecatedCurrentUserAccountID);
if (ownedReports.length > 0) {
return lodashMaxBy(ownedReports, (a) => a?.lastReadTime ?? '');
return toLastAccessedReport(lodashMaxBy(ownedReports, (a) => a?.lastReadTime ?? ''));
}
return lodashMaxBy(reportsValues, (a) => a?.lastReadTime ?? '');
return toLastAccessedReport(lodashMaxBy(reportsValues, (a) => a?.lastReadTime ?? ''));
}
return getMostRecentlyVisitedReport(reportsValues, allReportLastVisitTimes);
return toLastAccessedReport(getMostRecentlyVisitedReport(reportsValues, allReportLastVisitTimes));
}

/**
Expand Down Expand Up @@ -14762,4 +14767,5 @@ export type {
SelfDMParameters,
OptimisticReportAction,
ActionErrorsByTransaction,
LastAccessedReport,
};
5 changes: 3 additions & 2 deletions src/libs/actions/Link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import type {Route} from '@src/ROUTES';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import {hasCompletedGuidedSetupFlowSelector} from '@src/selectors/Onboarding';
import type {Beta, IntroSelected, Report} from '@src/types/onyx';
import type {Beta, IntroSelected, Report, ReportNameValuePairs} from '@src/types/onyx';

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

Expand Down Expand Up @@ -433,6 +433,7 @@ function openReportFromDeepLink(
isSelfTourViewed: boolean | undefined,
betas: OnyxEntry<Beta[]>,
callerAccountID: number,
reportNameValuePairs: OnyxCollection<ReportNameValuePairs>,
) {
const reportID = getReportIDFromLink(url);

Expand Down Expand Up @@ -601,7 +602,7 @@ function openReportFromDeepLink(
// If the report does not exist, navigate to the last accessed report or Concierge chat
if (reportID && (!report?.reportID || report.errorFields?.notFound)) {
// TODO: Pass guideAccountIDs once callers are fully migrated — PR 33 (https://github.com/Expensify/App/issues/66413); findLastAccessedReport falls back to hasExpensifyGuidesEmails → allPersonalDetails
const lastAccessedReportID = findLastAccessedReport(false, undefined, shouldOpenOnAdminRoom(), reportID)?.reportID;
const lastAccessedReportID = findLastAccessedReport(false, undefined, shouldOpenOnAdminRoom(), reportID, reportNameValuePairs)?.reportID;
if (lastAccessedReportID) {
const lastAccessedReportRoute = ROUTES.REPORT_WITH_ID.getRoute(lastAccessedReportID);
Navigation.navigate(lastAccessedReportRoute, {forceReplace: Navigation.getTopmostReportId() === reportID, waitForTransition: true});
Expand Down
15 changes: 6 additions & 9 deletions src/libs/actions/Report/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ import {
buildTransactionThread,
canUserPerformWriteAction as canUserPerformWriteActionReportUtils,
computeOptimisticReportName,
findLastAccessedReport,
findSelfDMReportID,
formatReportLastMessageText,
generateReportID,
Expand Down Expand Up @@ -5313,10 +5312,8 @@ function navigateToMostRecentReport(
introSelected: OnyxEntry<IntroSelected>,
isSelfTourViewed: boolean | undefined,
betas: OnyxEntry<Beta[]>,
lastAccessedReportID?: string,
Comment thread
ShridharGoel marked this conversation as resolved.
) {
// TODO: Pass guideAccountIDs once callers are fully migrated — PR 30 (https://github.com/Expensify/App/issues/66413); findLastAccessedReport falls back to hasExpensifyGuidesEmails → allPersonalDetails
const lastAccessedReportID = findLastAccessedReport(false, undefined, false, currentReport?.reportID)?.reportID;

if (lastAccessedReportID) {
// Check if route exists for super wide RHP vs regular full screen report
const topmostSuperWideRHP = Navigation.getTopmostSuperWideRHPReportID();
Expand Down Expand Up @@ -5354,9 +5351,7 @@ function getSearchThreadLeaveRoute(report: Report, activeRoute: string): Route |
});
}

function getMostRecentReportID(currentReport: OnyxEntry<Report>, conciergeReportID: string | undefined) {
// TODO: Pass guideAccountIDs once callers are fully migrated — PR 30 (https://github.com/Expensify/App/issues/66413); findLastAccessedReport falls back to hasExpensifyGuidesEmails → allPersonalDetails
const lastAccessedReportID = findLastAccessedReport(false, undefined, false, currentReport?.reportID)?.reportID;
function getMostRecentReportID(conciergeReportID: string | undefined, lastAccessedReportID?: string) {
Comment thread
ShridharGoel marked this conversation as resolved.
return lastAccessedReportID ?? conciergeReportID;
}

Expand All @@ -5382,6 +5377,7 @@ function leaveGroupChat(
introSelected: OnyxEntry<IntroSelected>,
isSelfTourViewed: boolean | undefined,
betas: OnyxEntry<Beta[]>,
lastAccessedReportID?: string,
) {
const reportID = report.reportID;
// Use merge instead of set to avoid deleting the report too quickly, which could cause a brief "not found" page to appear.
Expand Down Expand Up @@ -5432,7 +5428,7 @@ function leaveGroupChat(
if (isSearchTopmostFullScreenRoute()) {
Navigation.revealRouteBeforeDismissingModal(getReportRouteForCurrentContext({reportID}));
} else {
navigateToMostRecentReport(report, conciergeReportID, currentUserAccountID, introSelected, isSelfTourViewed, betas);
navigateToMostRecentReport(report, conciergeReportID, currentUserAccountID, introSelected, isSelfTourViewed, betas, lastAccessedReportID);
}
API.write(WRITE_COMMANDS.LEAVE_GROUP_CHAT, {reportID}, {optimisticData, successData, failureData});
}
Expand All @@ -5446,6 +5442,7 @@ function leaveRoom(
isSelfTourViewed: boolean | undefined,
betas: OnyxEntry<Beta[]>,
isWorkspaceMemberLeavingWorkspaceRoom = false,
lastAccessedReportID?: string,
) {
const reportID = report.reportID;
const isChatThread = isChatThreadReportUtils(report);
Expand Down Expand Up @@ -5557,7 +5554,7 @@ function leaveRoom(
return;
}
// In other cases, the report is deleted and we should move the user to another report.
navigateToMostRecentReport(report, conciergeReportID, currentUserAccountID, introSelected, isSelfTourViewed, betas);
navigateToMostRecentReport(report, conciergeReportID, currentUserAccountID, introSelected, isSelfTourViewed, betas, lastAccessedReportID);
}

function buildInviteToRoomOnyxData(
Expand Down
Loading
Loading