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
25 changes: 20 additions & 5 deletions src/libs/actions/Report/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,9 @@ import {
isExpenseReport,
isGroupChat as isGroupChatReportUtils,
isHiddenForCurrentUser,
isInvoiceReport,
isIOUReportUsingReport,
isMoneyRequestReport,
isOpenExpenseReport,
isProcessingReport,
isReportManuallyReimbursed,
Expand Down Expand Up @@ -5496,7 +5498,20 @@ function resolveActionableMentionWhisper(

// When the action belongs to a child report (e.g. a one-transaction thread), also update
// the parent report's participants so the members list the user is viewing updates immediately.
const parentInviteData = isInviteResolution && parentReport?.reportID && parentReport.reportID !== reportID ? buildParticipantsInviteData(parentReport, inviteeAccountIDs) : undefined;
// When parentReport is the same as the current report (e.g. viewing a transaction thread directly),
// fall back to the report's parentReportID to find the actual ancestor (IOU/expense/invoice report).
const isParentReportDifferent = !!parentReport?.reportID && parentReport.reportID !== reportID;
let parentInviteData = isInviteResolution && isParentReportDifferent ? buildParticipantsInviteData(parentReport, inviteeAccountIDs) : undefined;
if (!parentInviteData && isInviteResolution && report?.parentReportID && report.parentReportID !== reportID) {
const ancestorReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`];
if (ancestorReport && (isMoneyRequestReport(ancestorReport) || isInvoiceReport(ancestorReport))) {
parentInviteData = buildParticipantsInviteData(ancestorReport, inviteeAccountIDs);
}
}
let parentReportIDForUpdate: string | undefined;
if (parentInviteData) {
parentReportIDForUpdate = isParentReportDifferent ? parentReport.reportID : report?.parentReportID;
}
const parentParticipantsOptimisticData = parentInviteData?.optimistic;
const parentParticipantsFailureData = parentInviteData?.failure;

Expand All @@ -5523,10 +5538,10 @@ function resolveActionableMentionWhisper(
},
];

if (parentParticipantsOptimisticData && parentReport?.reportID) {
if (parentParticipantsOptimisticData && parentReportIDForUpdate) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`,
key: `${ONYXKEYS.COLLECTION.REPORT}${parentReportIDForUpdate}`,
value: parentParticipantsOptimisticData,
});
}
Expand Down Expand Up @@ -5554,10 +5569,10 @@ function resolveActionableMentionWhisper(
},
];

if (parentParticipantsFailureData && parentReport?.reportID) {
if (parentParticipantsFailureData && parentReportIDForUpdate) {
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`,
key: `${ONYXKEYS.COLLECTION.REPORT}${parentReportIDForUpdate}`,
value: parentParticipantsFailureData,
});
}
Expand Down
79 changes: 79 additions & 0 deletions tests/actions/ReportTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7440,6 +7440,85 @@ describe('actions/Report', () => {
});
});

it('should fall back to ancestor report via parentReportID when parentReport matches current report', async () => {
global.fetch = TestHelper.getGlobalFetchMock();

const TRANSACTION_THREAD_ID = '20';
const ANCESTOR_IOU_REPORT_ID = '21';
const WHISPER_ACTION_ID = '20001';
const EXISTING_PARTICIPANT_ID = 100;
const INVITEE_ACCOUNT_ID = 200;

// Transaction thread report — parentReportID points to the IOU ancestor
const transactionThreadReport: OnyxTypes.Report = {
...createRandomReport(20, undefined),
reportID: TRANSACTION_THREAD_ID,
parentReportID: ANCESTOR_IOU_REPORT_ID,
participants: {
[EXISTING_PARTICIPANT_ID]: {
notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS,
role: CONST.REPORT.ROLE.ADMIN,
},
},
lastMessageText: 'Receipt',
lastVisibleActionCreated: '2024-11-19 08:04:13.728',
lastActorAccountID: EXISTING_PARTICIPANT_ID,
};

// Ancestor IOU report (money request report)
const ancestorIOUReport: OnyxTypes.Report = {
...createRandomReport(21, undefined),
reportID: ANCESTOR_IOU_REPORT_ID,
type: CONST.REPORT.TYPE.IOU,
participants: {
[EXISTING_PARTICIPANT_ID]: {
notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS,
role: CONST.REPORT.ROLE.ADMIN,
},
},
};

const whisperAction = {
reportActionID: WHISPER_ACTION_ID,
reportID: TRANSACTION_THREAD_ID,
actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_MENTION_WHISPER,
created: '2024-11-19 08:04:13.730',
message: [{html: 'Mentioned @user1', text: 'Mentioned @user1', type: 'COMMENT'}],
originalMessage: {
inviteeAccountIDs: [INVITEE_ACCOUNT_ID],
inviteeEmails: ['user1@example.com'],
whisperedTo: [EXISTING_PARTICIPANT_ID],
},
} as unknown as OnyxTypes.ReportAction;

await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${TRANSACTION_THREAD_ID}`, transactionThreadReport);
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${ANCESTOR_IOU_REPORT_ID}`, ancestorIOUReport);
await Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {
[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${TRANSACTION_THREAD_ID}`]: {
[WHISPER_ACTION_ID]: whisperAction,
},
});
await waitForBatchedUpdates();

// Pass parentReport as the same report (simulating viewing transaction thread directly)
Report.resolveActionableMentionWhisper(transactionThreadReport, whisperAction, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE, false, transactionThreadReport);
await waitForBatchedUpdates();

// Verify the invitee was added to the transaction thread participants
const updatedThreadReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${TRANSACTION_THREAD_ID}` as const);
expect(updatedThreadReport?.participants?.[INVITEE_ACCOUNT_ID]).toMatchObject({
notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS,
role: CONST.REPORT.ROLE.MEMBER,
});

// Verify the invitee was also added to the ancestor IOU report via the fallback path
const updatedAncestorReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${ANCESTOR_IOU_REPORT_ID}` as const);
expect(updatedAncestorReport?.participants?.[INVITEE_ACCOUNT_ID]).toMatchObject({
notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS,
role: CONST.REPORT.ROLE.MEMBER,
});
});

it('should remove optimistically added participants on failure rollback', async () => {
const mockFetch = TestHelper.getGlobalFetchMock() as MockFetch;
global.fetch = mockFetch;
Expand Down
Loading