Skip to content

Commit 49290f5

Browse files
authored
Merge pull request Expensify#88795 from Expensify/claude-fixMembersListRefreshAfterWhisperInvite
Add optimistic participant update for actionable mention whisper invite
2 parents 76d4135 + b2ed4b8 commit 49290f5

4 files changed

Lines changed: 453 additions & 5 deletions

File tree

src/libs/actions/Report/index.ts

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5389,11 +5389,55 @@ function clearNewRoomFormError() {
53895389
});
53905390
}
53915391

5392+
/**
5393+
* Builds optimistic and failure rollback data for adding invitees to a report's participants.
5394+
* Skips IDs that already exist in participants to avoid overwriting their settings.
5395+
* On failure, only nulls out the newly added keys so Onyx removes them without overwriting
5396+
* concurrent participant changes.
5397+
*/
5398+
function buildParticipantsInviteData(
5399+
targetReport: OnyxEntry<Report>,
5400+
inviteeAccountIDs: number[],
5401+
): {optimistic: Pick<Report, 'participants'>; failure: Pick<Report, 'participants'>} | undefined {
5402+
if (!targetReport || inviteeAccountIDs.length === 0) {
5403+
return undefined;
5404+
}
5405+
5406+
const defaultPref = getDefaultNotificationPreferenceForReport(targetReport);
5407+
const participantsAfterInvitation = inviteeAccountIDs.reduce(
5408+
(acc: Participants, accountID: number) => {
5409+
if (accountID in (targetReport.participants ?? {})) {
5410+
return acc;
5411+
}
5412+
// eslint-disable-next-line no-param-reassign -- Mutating the reduce accumulator is intentional
5413+
acc[accountID] = {
5414+
notificationPreference: defaultPref,
5415+
role: CONST.REPORT.ROLE.MEMBER,
5416+
};
5417+
return acc;
5418+
},
5419+
{...targetReport.participants},
5420+
);
5421+
5422+
const rollback: Record<number, null> = {};
5423+
for (const accountID of inviteeAccountIDs) {
5424+
if (!(accountID in (targetReport.participants ?? {}))) {
5425+
rollback[accountID] = null;
5426+
}
5427+
}
5428+
5429+
return {
5430+
optimistic: {participants: participantsAfterInvitation},
5431+
failure: {participants: rollback as unknown as Participants},
5432+
};
5433+
}
5434+
53925435
function resolveActionableMentionWhisper(
53935436
report: OnyxEntry<Report>,
53945437
reportAction: OnyxEntry<ReportAction>,
53955438
resolution: ValueOf<typeof CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION> | ValueOf<typeof CONST.REPORT.ACTIONABLE_MENTION_INVITE_TO_SUBMIT_EXPENSE_CONFIRM_WHISPER>,
53965439
isReportArchived: boolean | undefined,
5440+
parentReport?: OnyxEntry<Report>,
53975441
) {
53985442
const reportID = report?.reportID;
53995443
if (!reportAction || !reportID) {
@@ -5426,6 +5470,22 @@ function resolveActionableMentionWhisper(
54265470
lastActorAccountID: report.lastActorAccountID,
54275471
};
54285472

5473+
// When the resolution is 'invited', optimistically add the invited users to report.participants
5474+
// so the members list updates immediately without waiting for the server response.
5475+
const isInviteResolution = resolution === CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE;
5476+
const originalMessage = ReportActionsUtils.getOriginalMessage(reportAction as ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_MENTION_WHISPER>);
5477+
const inviteeAccountIDs = isInviteResolution ? (originalMessage?.inviteeAccountIDs ?? []) : [];
5478+
5479+
const participantsInviteData = isInviteResolution && report ? buildParticipantsInviteData(report, inviteeAccountIDs) : undefined;
5480+
const participantsOptimisticData = participantsInviteData?.optimistic;
5481+
const participantsFailureData = participantsInviteData?.failure;
5482+
5483+
// When the action belongs to a child report (e.g. a one-transaction thread), also update
5484+
// the parent report's participants so the members list the user is viewing updates immediately.
5485+
const parentInviteData = isInviteResolution && parentReport?.reportID && parentReport.reportID !== reportID ? buildParticipantsInviteData(parentReport, inviteeAccountIDs) : undefined;
5486+
const parentParticipantsOptimisticData = parentInviteData?.optimistic;
5487+
const parentParticipantsFailureData = parentInviteData?.failure;
5488+
54295489
const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS | typeof ONYXKEYS.COLLECTION.REPORT>> = [
54305490
{
54315491
onyxMethod: Onyx.METHOD.MERGE,
@@ -5442,10 +5502,21 @@ function resolveActionableMentionWhisper(
54425502
{
54435503
onyxMethod: Onyx.METHOD.MERGE,
54445504
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
5445-
value: reportUpdateDataWithPreviousLastMessage,
5505+
value: {
5506+
...reportUpdateDataWithPreviousLastMessage,
5507+
...participantsOptimisticData,
5508+
},
54465509
},
54475510
];
54485511

5512+
if (parentParticipantsOptimisticData && parentReport?.reportID) {
5513+
optimisticData.push({
5514+
onyxMethod: Onyx.METHOD.MERGE,
5515+
key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`,
5516+
value: parentParticipantsOptimisticData,
5517+
});
5518+
}
5519+
54495520
const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS | typeof ONYXKEYS.COLLECTION.REPORT>> = [
54505521
{
54515522
onyxMethod: Onyx.METHOD.MERGE,
@@ -5462,10 +5533,21 @@ function resolveActionableMentionWhisper(
54625533
{
54635534
onyxMethod: Onyx.METHOD.MERGE,
54645535
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
5465-
value: reportUpdateDataWithCurrentLastMessage, // revert back to the current report last message data in case of failure
5536+
value: {
5537+
...reportUpdateDataWithCurrentLastMessage, // revert back to the current report last message data in case of failure
5538+
...participantsFailureData,
5539+
},
54665540
},
54675541
];
54685542

5543+
if (parentParticipantsFailureData && parentReport?.reportID) {
5544+
failureData.push({
5545+
onyxMethod: Onyx.METHOD.MERGE,
5546+
key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`,
5547+
value: parentParticipantsFailureData,
5548+
});
5549+
}
5550+
54695551
const parameters: ResolveActionableMentionWhisperParams = {
54705552
reportActionID: reportAction.reportActionID,
54715553
resolution,

src/pages/inbox/report/PureReportActionItem.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ type PureReportActionItemProps = {
260260
reportAction: OnyxEntry<OnyxTypes.ReportAction>,
261261
resolution: ValueOf<typeof CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION>,
262262
isReportArchived: boolean,
263+
parentReport?: OnyxEntry<OnyxTypes.Report>,
263264
) => void;
264265

265266
/** Whether the provided report is a closed expense report with no expenses */

src/pages/inbox/report/actionContents/MentionWhisperContent.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type MentionWhisperContentProps = {
2525
reportAction: OnyxEntry<ReportAction>,
2626
resolution: ValueOf<typeof CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION>,
2727
isReportArchived: boolean,
28+
parentReport?: OnyxEntry<Report>,
2829
) => void;
2930
};
3031

@@ -44,19 +45,40 @@ function MentionWhisperContent({action, report, originalReport, policy, personal
4445
buttons.push({
4546
text: 'actionableMentionWhisperOptions.inviteToSubmitExpense',
4647
key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE_TO_SUBMIT_EXPENSE}`,
47-
onPress: () => resolveActionableMentionWhisper(reportActionReport, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE_TO_SUBMIT_EXPENSE, isOriginalReportArchived),
48+
onPress: () =>
49+
resolveActionableMentionWhisper(
50+
reportActionReport,
51+
action,
52+
CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE_TO_SUBMIT_EXPENSE,
53+
isOriginalReportArchived,
54+
originalReport ? report : undefined,
55+
),
4856
});
4957
}
5058
buttons.push(
5159
{
5260
text: 'actionableMentionWhisperOptions.inviteToChat',
5361
key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE}`,
54-
onPress: () => resolveActionableMentionWhisper(reportActionReport, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE, isOriginalReportArchived),
62+
onPress: () =>
63+
resolveActionableMentionWhisper(
64+
reportActionReport,
65+
action,
66+
CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE,
67+
isOriginalReportArchived,
68+
originalReport ? report : undefined,
69+
),
5570
},
5671
{
5772
text: 'actionableMentionWhisperOptions.nothing',
5873
key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING}`,
59-
onPress: () => resolveActionableMentionWhisper(reportActionReport, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING, isOriginalReportArchived),
74+
onPress: () =>
75+
resolveActionableMentionWhisper(
76+
reportActionReport,
77+
action,
78+
CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING,
79+
isOriginalReportArchived,
80+
originalReport ? report : undefined,
81+
),
6082
},
6183
);
6284

0 commit comments

Comments
 (0)