diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 73715eabf92b..d67f46a069e0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2852,6 +2852,26 @@ function isOneOnOneChat(report: OnyxEntry, currentUserAccountID?: number ); } +/** + * Returns the other participant of a cached 1:1 DM as OpenReport participant info, so the server can + * resolve a stale/optimistic reportID to the real chat (returned as preexistingReportID) instead of + * failing with "Report not found". Returns an empty list for any report that is not a 1:1 DM. + */ +function getOneOnOneChatParticipants( + report: OnyxEntry, + personalDetails: OnyxEntry, + currentUserAccountID: number | undefined, +): Array<{login: string; accountID: number}> { + if (!currentUserAccountID || !isOneOnOneChat(report, currentUserAccountID)) { + return []; + } + return Object.keys(report?.participants ?? {}) + .map(Number) + .filter((accountID) => accountID !== currentUserAccountID) + .map((accountID) => ({login: personalDetails?.[accountID]?.login ?? '', accountID})) + .filter((participant) => !!participant.login); +} + /** * Checks if the current user is a payer of the expense */ @@ -14252,6 +14272,7 @@ export { getNonHeldAndFullAmount, getReimbursableTotal, getUnheldReimbursableTotal, + getOneOnOneChatParticipants, getOptimisticDataForAncestors, getOriginalReportID, getOutstandingChildRequest, diff --git a/src/pages/inbox/ReportFetchHandler.tsx b/src/pages/inbox/ReportFetchHandler.tsx index 37b9ee90132d..7033f72aefaf 100644 --- a/src/pages/inbox/ReportFetchHandler.tsx +++ b/src/pages/inbox/ReportFetchHandler.tsx @@ -23,6 +23,7 @@ import type {CancelHandle} from '@libs/Navigation/TransitionTracker'; import {isSupportedInviteOnboardingChoice, isSupportedPendingInviteOnboarding} from '@libs/OnboardingUtils'; import {getFilteredReportActionsForReportView, getIOUActionForReportID, getOneTransactionThreadReportID, isCreatedAction} from '@libs/ReportActionsUtils'; import { + getOneOnOneChatParticipants, isChatThread, isHiddenForCurrentUser, isMoneyRequestReport, @@ -203,10 +204,14 @@ function ReportFetchHandler() { return; } + // For a cached 1:1 DM, pass the other participant so the server can resolve a stale/optimistic + // reportID to the real chat (via preexistingReportID) instead of failing with "Report not found". + const dmParticipants = getOneOnOneChatParticipants(report, personalDetails, currentUserAccountID); openReport({ reportID: reportIDFromRoute, introSelected, reportActionID: reportActionIDFromRoute, + participants: dmParticipants, betas, hasReportActions, currentUserAccountID, diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index a630178f88b4..268535048459 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -5217,6 +5217,29 @@ describe('actions/Report', () => { }); }); + describe('openReport with participants', () => { + it('should send passed participants as emailList/accountIDList so the server can resolve a stale optimistic reportID', async () => { + global.fetch = TestHelper.createGlobalFetchMock(); + const REPORT_ID = 'dm1'; + + Report.openReport({ + reportID: REPORT_ID, + introSelected: undefined, + betas: undefined, + hasReportActions: true, + currentUserAccountID: 1, + participants: [{login: 'other@test.com', accountID: 2}], + }); + await waitForBatchedUpdates(); + + TestHelper.expectAPICommandToHaveBeenCalledWith(WRITE_COMMANDS.OPEN_REPORT, 0, { + reportID: REPORT_ID, + emailList: 'other@test.com', + accountIDList: '2', + }); + }); + }); + describe('setOptimisticTransactionThread', () => { it('should set optimistic transaction thread data with the provided parameters', async () => { const reportID = 'report12'; diff --git a/tests/unit/ReportUtilsTest.ts b/tests/unit/ReportUtilsTest.ts index e45057c18c78..bcf4d289c75c 100644 --- a/tests/unit/ReportUtilsTest.ts +++ b/tests/unit/ReportUtilsTest.ts @@ -116,6 +116,7 @@ import { getMovedTransactionMessage, getNextApproverAccountID, getNonHeldAndFullAmount, + getOneOnOneChatParticipants, getOriginalReportID, getOutstandingChildRequest, getParentNavigationSubtitle, @@ -11688,6 +11689,39 @@ describe('ReportUtils', () => { }); }); + describe('getOneOnOneChatParticipants', () => { + const OTHER_ACCOUNT_ID = 222; + const personalDetailsList: PersonalDetailsList = { + [OTHER_ACCOUNT_ID]: {accountID: OTHER_ACCOUNT_ID, login: 'other@test.com'}, + }; + const dmReport: Report = { + ...createRandomReport(0, undefined), + type: CONST.REPORT.TYPE.CHAT, + policyID: CONST.POLICY.ID_FAKE, + participants: buildParticipantsFromAccountIDs([currentUserAccountID, OTHER_ACCOUNT_ID]), + }; + + it('should return the other participant of a 1:1 DM with their login and accountID', () => { + expect(getOneOnOneChatParticipants(dmReport, personalDetailsList, currentUserAccountID)).toEqual([{login: 'other@test.com', accountID: OTHER_ACCOUNT_ID}]); + }); + + it('should return an empty list for reports that are not 1:1 DMs', () => { + const roomReport: Report = { + ...createRandomReport(1, CONST.REPORT.CHAT_TYPE.POLICY_ADMINS), + participants: buildParticipantsFromAccountIDs([currentUserAccountID, OTHER_ACCOUNT_ID]), + }; + expect(getOneOnOneChatParticipants(roomReport, personalDetailsList, currentUserAccountID)).toEqual([]); + }); + + it('should return an empty list when the other participant has no known login', () => { + expect(getOneOnOneChatParticipants(dmReport, {}, currentUserAccountID)).toEqual([]); + }); + + it('should return an empty list when currentUserAccountID is missing', () => { + expect(getOneOnOneChatParticipants(dmReport, personalDetailsList, undefined)).toEqual([]); + }); + }); + describe('getReportNotificationPreference', () => { it('should read the notification preference of the passed currentUserAccountID', () => { const report: Report = {