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
86 changes: 84 additions & 2 deletions src/libs/actions/Report/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5376,11 +5376,55 @@ function clearNewRoomFormError() {
});
}

/**
* Builds optimistic and failure rollback data for adding invitees to a report's participants.
* Skips IDs that already exist in participants to avoid overwriting their settings.
* On failure, only nulls out the newly added keys so Onyx removes them without overwriting
* concurrent participant changes.
*/
function buildParticipantsInviteData(
targetReport: OnyxEntry<Report>,
inviteeAccountIDs: number[],
): {optimistic: Pick<Report, 'participants'>; failure: Pick<Report, 'participants'>} | undefined {
if (!targetReport || inviteeAccountIDs.length === 0) {
return undefined;
}

const defaultPref = getDefaultNotificationPreferenceForReport(targetReport);
const participantsAfterInvitation = inviteeAccountIDs.reduce(
(acc: Participants, accountID: number) => {
if (accountID in (targetReport.participants ?? {})) {
return acc;
}
// eslint-disable-next-line no-param-reassign -- Mutating the reduce accumulator is intentional
acc[accountID] = {
notificationPreference: defaultPref,
role: CONST.REPORT.ROLE.MEMBER,
};
return acc;
},
{...targetReport.participants},
);

const rollback: Record<number, null> = {};
for (const accountID of inviteeAccountIDs) {
if (!(accountID in (targetReport.participants ?? {}))) {
rollback[accountID] = null;
}
}

return {
optimistic: {participants: participantsAfterInvitation},
failure: {participants: rollback as unknown as Participants},
};
}

function resolveActionableMentionWhisper(
report: OnyxEntry<Report>,
reportAction: OnyxEntry<ReportAction>,
resolution: ValueOf<typeof CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION> | ValueOf<typeof CONST.REPORT.ACTIONABLE_MENTION_INVITE_TO_SUBMIT_EXPENSE_CONFIRM_WHISPER>,
isReportArchived: boolean | undefined,
parentReport?: OnyxEntry<Report>,
) {
const reportID = report?.reportID;
if (!reportAction || !reportID) {
Expand Down Expand Up @@ -5413,6 +5457,22 @@ function resolveActionableMentionWhisper(
lastActorAccountID: report.lastActorAccountID,
};

// When the resolution is 'invited', optimistically add the invited users to report.participants
// so the members list updates immediately without waiting for the server response.
const isInviteResolution = resolution === CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE;
const originalMessage = ReportActionsUtils.getOriginalMessage(reportAction as ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_MENTION_WHISPER>);
const inviteeAccountIDs = isInviteResolution ? (originalMessage?.inviteeAccountIDs ?? []) : [];

const participantsInviteData = isInviteResolution && report ? buildParticipantsInviteData(report, inviteeAccountIDs) : undefined;
const participantsOptimisticData = participantsInviteData?.optimistic;
const participantsFailureData = participantsInviteData?.failure;

// 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;
const parentParticipantsOptimisticData = parentInviteData?.optimistic;
const parentParticipantsFailureData = parentInviteData?.failure;

const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS | typeof ONYXKEYS.COLLECTION.REPORT>> = [
{
onyxMethod: Onyx.METHOD.MERGE,
Expand All @@ -5429,10 +5489,21 @@ function resolveActionableMentionWhisper(
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
value: reportUpdateDataWithPreviousLastMessage,
value: {
...reportUpdateDataWithPreviousLastMessage,
...participantsOptimisticData,
},
},
];

if (parentParticipantsOptimisticData && parentReport?.reportID) {
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`,
value: parentParticipantsOptimisticData,
});
}

const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS | typeof ONYXKEYS.COLLECTION.REPORT>> = [
{
onyxMethod: Onyx.METHOD.MERGE,
Expand All @@ -5449,10 +5520,21 @@ function resolveActionableMentionWhisper(
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
value: reportUpdateDataWithCurrentLastMessage, // revert back to the current report last message data in case of failure
value: {
...reportUpdateDataWithCurrentLastMessage, // revert back to the current report last message data in case of failure
...participantsFailureData,
},
},
];

if (parentParticipantsFailureData && parentReport?.reportID) {
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`,
value: parentParticipantsFailureData,
});
}

const parameters: ResolveActionableMentionWhisperParams = {
reportActionID: reportAction.reportActionID,
resolution,
Expand Down
1 change: 1 addition & 0 deletions src/pages/inbox/report/PureReportActionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ type PureReportActionItemProps = {
reportAction: OnyxEntry<OnyxTypes.ReportAction>,
resolution: ValueOf<typeof CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION>,
isReportArchived: boolean,
parentReport?: OnyxEntry<OnyxTypes.Report>,
) => void;

/** Whether the provided report is a closed expense report with no expenses */
Expand Down
28 changes: 25 additions & 3 deletions src/pages/inbox/report/actionContents/MentionWhisperContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type MentionWhisperContentProps = {
reportAction: OnyxEntry<ReportAction>,
resolution: ValueOf<typeof CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION>,
isReportArchived: boolean,
parentReport?: OnyxEntry<Report>,
) => void;
};

Expand All @@ -44,19 +45,40 @@ function MentionWhisperContent({action, report, originalReport, policy, personal
buttons.push({
text: 'actionableMentionWhisperOptions.inviteToSubmitExpense',
key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE_TO_SUBMIT_EXPENSE}`,
onPress: () => resolveActionableMentionWhisper(reportActionReport, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE_TO_SUBMIT_EXPENSE, isOriginalReportArchived),
onPress: () =>
resolveActionableMentionWhisper(
reportActionReport,
action,
CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE_TO_SUBMIT_EXPENSE,
isOriginalReportArchived,
originalReport ? report : undefined,
),
});
}
buttons.push(
{
text: 'actionableMentionWhisperOptions.inviteToChat',
key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE}`,
onPress: () => resolveActionableMentionWhisper(reportActionReport, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE, isOriginalReportArchived),
onPress: () =>
resolveActionableMentionWhisper(
reportActionReport,
action,
CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE,
isOriginalReportArchived,
originalReport ? report : undefined,
),
},
{
text: 'actionableMentionWhisperOptions.nothing',
key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING}`,
onPress: () => resolveActionableMentionWhisper(reportActionReport, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING, isOriginalReportArchived),
onPress: () =>
resolveActionableMentionWhisper(
reportActionReport,
action,
CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING,
isOriginalReportArchived,
originalReport ? report : undefined,
),
},
);

Expand Down
Loading
Loading