Skip to content

Commit 6e87d5a

Browse files
authored
Merge pull request #89363 from Expensify/claude-fixMentionWhisperAncestorParticipants
Fix stale members list when resolving mention whisper in transaction thread
2 parents b349b81 + 767f276 commit 6e87d5a

2 files changed

Lines changed: 99 additions & 5 deletions

File tree

src/libs/actions/Report/index.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,9 @@ import {
159159
isExpenseReport,
160160
isGroupChat as isGroupChatReportUtils,
161161
isHiddenForCurrentUser,
162+
isInvoiceReport,
162163
isIOUReportUsingReport,
164+
isMoneyRequestReport,
163165
isOpenExpenseReport,
164166
isProcessingReport,
165167
isReportManuallyReimbursed,
@@ -5535,7 +5537,20 @@ function resolveActionableMentionWhisper(
55355537

55365538
// When the action belongs to a child report (e.g. a one-transaction thread), also update
55375539
// the parent report's participants so the members list the user is viewing updates immediately.
5538-
const parentInviteData = isInviteResolution && parentReport?.reportID && parentReport.reportID !== reportID ? buildParticipantsInviteData(parentReport, inviteeAccountIDs) : undefined;
5540+
// When parentReport is the same as the current report (e.g. viewing a transaction thread directly),
5541+
// fall back to the report's parentReportID to find the actual ancestor (IOU/expense/invoice report).
5542+
const isParentReportDifferent = !!parentReport?.reportID && parentReport.reportID !== reportID;
5543+
let parentInviteData = isInviteResolution && isParentReportDifferent ? buildParticipantsInviteData(parentReport, inviteeAccountIDs) : undefined;
5544+
if (!parentInviteData && isInviteResolution && report?.parentReportID && report.parentReportID !== reportID) {
5545+
const ancestorReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.parentReportID}`];
5546+
if (ancestorReport && (isMoneyRequestReport(ancestorReport) || isInvoiceReport(ancestorReport))) {
5547+
parentInviteData = buildParticipantsInviteData(ancestorReport, inviteeAccountIDs);
5548+
}
5549+
}
5550+
let parentReportIDForUpdate: string | undefined;
5551+
if (parentInviteData) {
5552+
parentReportIDForUpdate = isParentReportDifferent ? parentReport.reportID : report?.parentReportID;
5553+
}
55395554
const parentParticipantsOptimisticData = parentInviteData?.optimistic;
55405555
const parentParticipantsFailureData = parentInviteData?.failure;
55415556

@@ -5562,10 +5577,10 @@ function resolveActionableMentionWhisper(
55625577
},
55635578
];
55645579

5565-
if (parentParticipantsOptimisticData && parentReport?.reportID) {
5580+
if (parentParticipantsOptimisticData && parentReportIDForUpdate) {
55665581
optimisticData.push({
55675582
onyxMethod: Onyx.METHOD.MERGE,
5568-
key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`,
5583+
key: `${ONYXKEYS.COLLECTION.REPORT}${parentReportIDForUpdate}`,
55695584
value: parentParticipantsOptimisticData,
55705585
});
55715586
}
@@ -5593,10 +5608,10 @@ function resolveActionableMentionWhisper(
55935608
},
55945609
];
55955610

5596-
if (parentParticipantsFailureData && parentReport?.reportID) {
5611+
if (parentParticipantsFailureData && parentReportIDForUpdate) {
55975612
failureData.push({
55985613
onyxMethod: Onyx.METHOD.MERGE,
5599-
key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`,
5614+
key: `${ONYXKEYS.COLLECTION.REPORT}${parentReportIDForUpdate}`,
56005615
value: parentParticipantsFailureData,
56015616
});
56025617
}

tests/actions/ReportTest.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7650,6 +7650,85 @@ describe('actions/Report', () => {
76507650
});
76517651
});
76527652

7653+
it('should fall back to ancestor report via parentReportID when parentReport matches current report', async () => {
7654+
global.fetch = TestHelper.getGlobalFetchMock();
7655+
7656+
const TRANSACTION_THREAD_ID = '20';
7657+
const ANCESTOR_IOU_REPORT_ID = '21';
7658+
const WHISPER_ACTION_ID = '20001';
7659+
const EXISTING_PARTICIPANT_ID = 100;
7660+
const INVITEE_ACCOUNT_ID = 200;
7661+
7662+
// Transaction thread report — parentReportID points to the IOU ancestor
7663+
const transactionThreadReport: OnyxTypes.Report = {
7664+
...createRandomReport(20, undefined),
7665+
reportID: TRANSACTION_THREAD_ID,
7666+
parentReportID: ANCESTOR_IOU_REPORT_ID,
7667+
participants: {
7668+
[EXISTING_PARTICIPANT_ID]: {
7669+
notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS,
7670+
role: CONST.REPORT.ROLE.ADMIN,
7671+
},
7672+
},
7673+
lastMessageText: 'Receipt',
7674+
lastVisibleActionCreated: '2024-11-19 08:04:13.728',
7675+
lastActorAccountID: EXISTING_PARTICIPANT_ID,
7676+
};
7677+
7678+
// Ancestor IOU report (money request report)
7679+
const ancestorIOUReport: OnyxTypes.Report = {
7680+
...createRandomReport(21, undefined),
7681+
reportID: ANCESTOR_IOU_REPORT_ID,
7682+
type: CONST.REPORT.TYPE.IOU,
7683+
participants: {
7684+
[EXISTING_PARTICIPANT_ID]: {
7685+
notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS,
7686+
role: CONST.REPORT.ROLE.ADMIN,
7687+
},
7688+
},
7689+
};
7690+
7691+
const whisperAction = {
7692+
reportActionID: WHISPER_ACTION_ID,
7693+
reportID: TRANSACTION_THREAD_ID,
7694+
actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_MENTION_WHISPER,
7695+
created: '2024-11-19 08:04:13.730',
7696+
message: [{html: 'Mentioned @user1', text: 'Mentioned @user1', type: 'COMMENT'}],
7697+
originalMessage: {
7698+
inviteeAccountIDs: [INVITEE_ACCOUNT_ID],
7699+
inviteeEmails: ['user1@example.com'],
7700+
whisperedTo: [EXISTING_PARTICIPANT_ID],
7701+
},
7702+
} as unknown as OnyxTypes.ReportAction;
7703+
7704+
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${TRANSACTION_THREAD_ID}`, transactionThreadReport);
7705+
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${ANCESTOR_IOU_REPORT_ID}`, ancestorIOUReport);
7706+
await Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {
7707+
[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${TRANSACTION_THREAD_ID}`]: {
7708+
[WHISPER_ACTION_ID]: whisperAction,
7709+
},
7710+
});
7711+
await waitForBatchedUpdates();
7712+
7713+
// Pass parentReport as the same report (simulating viewing transaction thread directly)
7714+
Report.resolveActionableMentionWhisper(transactionThreadReport, whisperAction, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE, false, transactionThreadReport);
7715+
await waitForBatchedUpdates();
7716+
7717+
// Verify the invitee was added to the transaction thread participants
7718+
const updatedThreadReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${TRANSACTION_THREAD_ID}` as const);
7719+
expect(updatedThreadReport?.participants?.[INVITEE_ACCOUNT_ID]).toMatchObject({
7720+
notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS,
7721+
role: CONST.REPORT.ROLE.MEMBER,
7722+
});
7723+
7724+
// Verify the invitee was also added to the ancestor IOU report via the fallback path
7725+
const updatedAncestorReport = await getOnyxValue(`${ONYXKEYS.COLLECTION.REPORT}${ANCESTOR_IOU_REPORT_ID}` as const);
7726+
expect(updatedAncestorReport?.participants?.[INVITEE_ACCOUNT_ID]).toMatchObject({
7727+
notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS,
7728+
role: CONST.REPORT.ROLE.MEMBER,
7729+
});
7730+
});
7731+
76537732
it('should remove optimistically added participants on failure rollback', async () => {
76547733
const mockFetch = TestHelper.getGlobalFetchMock() as MockFetch;
76557734
global.fetch = mockFetch;

0 commit comments

Comments
 (0)