Skip to content

Commit f456efe

Browse files
authored
Merge pull request Expensify#82720 from Expensify/scott-deleteMentionWithAddComment
Delete user MENTIONWHISPER when ADDCOMMENT is deleted
2 parents adc5f4a + fb54737 commit f456efe

7 files changed

Lines changed: 383 additions & 23 deletions

File tree

src/libs/ReportActionsUtils.ts

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,10 +1084,38 @@ const supportedActionTypes = new Set<ReportActionName>([...Object.values(otherAc
10841084
* Checks whether an action is actionable track expense and resolved.
10851085
*
10861086
*/
1087-
function isResolvedActionableWhisper(reportAction: OnyxEntry<ReportAction>): boolean {
1087+
function isResolvedActionableWhisper(reportAction: OnyxEntry<ReportAction>, allActionsForReport?: OnyxEntry<ReportActions>): boolean {
10881088
const originalMessage = getOriginalMessage(reportAction);
1089-
const resolution = originalMessage && typeof originalMessage === 'object' && 'resolution' in originalMessage ? originalMessage?.resolution : null;
1090-
return !!resolution;
1089+
if (!originalMessage || typeof originalMessage !== 'object') {
1090+
return false;
1091+
}
1092+
const resolution = 'resolution' in originalMessage ? originalMessage?.resolution : null;
1093+
if (resolution) {
1094+
return true;
1095+
}
1096+
1097+
const deleted = 'deleted' in originalMessage ? originalMessage?.deleted : null;
1098+
if (!deleted) {
1099+
return false;
1100+
}
1101+
1102+
// For mention whispers, only treat as deleted if the parent comment is also deleted.
1103+
// This distinguishes cascade deletion (parent deleted -> whisper should hide) from
1104+
// the backend's one-per-user cleanup (parent still exists -> whisper should stay visible).
1105+
if (reportAction?.reportActionID && (isActionableMentionWhisper(reportAction) || isActionableReportMentionWhisper(reportAction))) {
1106+
const reportID = reportAction.reportID;
1107+
const actions = allActionsForReport ?? (reportID ? allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] : undefined);
1108+
if (actions) {
1109+
const parentOffset = isActionableReportMentionWhisper(reportAction) ? 2n : 1n;
1110+
const parentActionID = String(BigInt(reportAction.reportActionID) - parentOffset);
1111+
const parentAction = actions[parentActionID];
1112+
if (parentAction && !isDeletedAction(parentAction)) {
1113+
return false;
1114+
}
1115+
}
1116+
}
1117+
1118+
return true;
10911119
}
10921120

10931121
/**
@@ -1112,7 +1140,13 @@ function isResolvedConciergeDescriptionOptions(reportAction: OnyxEntry<ReportAct
11121140
* Checks if a reportAction is fit for display, meaning that it's not deprecated, is of a valid
11131141
* and supported type, it's not deleted and also not closed.
11141142
*/
1115-
function shouldReportActionBeVisible(reportAction: OnyxEntry<ReportAction>, key: string | number, canUserPerformWriteAction?: boolean, reportsParam?: OnyxCollection<Report>): boolean {
1143+
function shouldReportActionBeVisible(
1144+
reportAction: OnyxEntry<ReportAction>,
1145+
key: string | number,
1146+
canUserPerformWriteAction?: boolean,
1147+
reportsParam?: OnyxCollection<Report>,
1148+
allActionsForReport?: OnyxEntry<ReportActions>,
1149+
): boolean {
11161150
if (!reportAction) {
11171151
return false;
11181152
}
@@ -1189,7 +1223,7 @@ function shouldReportActionBeVisible(reportAction: OnyxEntry<ReportAction>, key:
11891223
}
11901224

11911225
// If action is actionable whisper and resolved by user, then we don't want to render anything
1192-
if (isActionableWhisper(reportAction) && isResolvedActionableWhisper(reportAction)) {
1226+
if (isActionableWhisper(reportAction) && isResolvedActionableWhisper(reportAction, allActionsForReport)) {
11931227
return false;
11941228
}
11951229

@@ -1225,22 +1259,27 @@ function isReportActionVisible(
12251259
return false;
12261260
}
12271261

1262+
// Look up all actions for this report so the parent-check in isResolvedActionableWhisper
1263+
// can determine whether a whisper's `deleted` flag reflects a real cascade deletion
1264+
// (parent comment deleted) vs. the backend's one-per-user cleanup (parent still exists).
1265+
const reportActionsForReport = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`];
1266+
12281267
// Actions with pendingAction are optimistic or in-flight, so their visibility may differ
12291268
// from what's cached in visibleReportActions (which reflects persisted Onyx data).
12301269
// We must recalculate visibility at runtime to ensure accuracy for these transient states.
12311270
if (reportAction.pendingAction) {
1232-
return shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction);
1271+
return shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction, undefined, reportActionsForReport);
12331272
}
12341273

12351274
if (visibleReportActions) {
12361275
const reportCache = visibleReportActions[reportID];
12371276
if (!reportCache) {
1238-
return shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction);
1277+
return shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction, undefined, reportActionsForReport);
12391278
}
12401279
const staticVisibility = reportCache[reportAction.reportActionID];
12411280
// If action is not in derived value cache, fall back to runtime calculation
12421281
if (staticVisibility === undefined) {
1243-
return shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction);
1282+
return shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction, undefined, reportActionsForReport);
12441283
}
12451284
if (!staticVisibility) {
12461285
return false;
@@ -1250,7 +1289,7 @@ function isReportActionVisible(
12501289
}
12511290
return true;
12521291
}
1253-
return shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction);
1292+
return shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction, undefined, reportActionsForReport);
12541293
}
12551294

12561295
/**

src/libs/actions/OnyxDerived/configs/visibleReportActions.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export default createOnyxDerivedValueConfig({
7171
if (shouldSkipCachingAction(action)) {
7272
continue;
7373
}
74-
reportVisibility[action.reportActionID] = shouldReportActionBeVisible(action, actionID, undefined, allReports);
74+
reportVisibility[action.reportActionID] = shouldReportActionBeVisible(action, actionID, undefined, allReports, reportActions);
7575
}
7676
}
7777
}
@@ -108,7 +108,7 @@ export default createOnyxDerivedValueConfig({
108108
delete reportVisibility[action.reportActionID];
109109
continue;
110110
}
111-
reportVisibility[action.reportActionID] = shouldReportActionBeVisible(action, actionID, undefined, allReports);
111+
reportVisibility[action.reportActionID] = shouldReportActionBeVisible(action, actionID, undefined, allReports, reportActions);
112112
}
113113
}
114114
}
@@ -158,7 +158,7 @@ export default createOnyxDerivedValueConfig({
158158
continue;
159159
}
160160

161-
reportVisibility[action.reportActionID] = shouldReportActionBeVisible(action, actionID, undefined, allReports);
161+
reportVisibility[action.reportActionID] = shouldReportActionBeVisible(action, actionID, undefined, allReports, reportActions);
162162
}
163163
}
164164

src/libs/actions/Report/index.ts

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2494,6 +2494,7 @@ function deleteReportComment(
24942494
isOriginalReportArchived: boolean | undefined,
24952495
currentEmail: string,
24962496
visibleReportActionsDataParam?: VisibleReportActionsDerivedValue,
2497+
currentReportActionsParam?: OnyxEntry<ReportActions>,
24972498
) {
24982499
const reportID = report?.reportID;
24992500
const originalReportID = getOriginalReportID(reportID, reportAction, undefined);
@@ -2545,6 +2546,42 @@ function deleteReportComment(
25452546
},
25462547
};
25472548

2549+
// Optimistically hide whispers associated with the deleted comment. We use the same conventions
2550+
// as the backend (UpdateReportComment.cpp) to locate the correct whisper(s):
2551+
// - ACTIONABLE_MENTION_WHISPER has ID = parentCommentID + 1
2552+
// - ACTIONABLE_REPORT_MENTION_WHISPER has ID = parentCommentID + 2, and its originalMessage
2553+
// also stores the parent's reportActionID so we can verify the match.
2554+
// We prefer the actions passed directly from the calling component (currentReportActionsParam)
2555+
// since those come from useOnyx and are guaranteed to be up to date. We fall back to the
2556+
// module-level allReportActions cache.
2557+
const reportActionsForReport = currentReportActionsParam ?? allReportActions?.[originalReportID] ?? {};
2558+
const unresolvedMentionWhisperIDs: string[] = [];
2559+
2560+
const mentionWhisperID = String(BigInt(reportActionID) + 1n);
2561+
const mentionWhisperAction = reportActionsForReport[mentionWhisperID];
2562+
if (ReportActionsUtils.isActionableMentionWhisper(mentionWhisperAction)) {
2563+
const originalMessage = ReportActionsUtils.getOriginalMessage(mentionWhisperAction);
2564+
if (!originalMessage?.resolution && !originalMessage?.deleted) {
2565+
unresolvedMentionWhisperIDs.push(mentionWhisperID);
2566+
}
2567+
}
2568+
2569+
const reportMentionWhisperID = String(BigInt(reportActionID) + 2n);
2570+
const reportMentionWhisperAction = reportActionsForReport[reportMentionWhisperID];
2571+
if (ReportActionsUtils.isActionableReportMentionWhisper(reportMentionWhisperAction)) {
2572+
const originalMessage = ReportActionsUtils.getOriginalMessage(reportMentionWhisperAction);
2573+
if (!originalMessage?.resolution && !originalMessage?.deleted) {
2574+
unresolvedMentionWhisperIDs.push(reportMentionWhisperID);
2575+
}
2576+
}
2577+
2578+
const deletedTime = DateUtils.getDBTime();
2579+
for (const whisperID of unresolvedMentionWhisperIDs) {
2580+
optimisticReportActions[whisperID] = {
2581+
originalMessage: {deleted: deletedTime},
2582+
};
2583+
}
2584+
25482585
// If we are deleting the last visible message, let's find the previous visible one (or set an empty one if there are none) and update the lastMessageText in the LHN.
25492586
// Similarly, if we are deleting the last read comment we will want to update the lastVisibleActionCreated to use the previous visible message.
25502587
const canUserPerformWriteAction = canUserPerformWriteActionReportUtils(report, isReportArchived);
@@ -2556,28 +2593,35 @@ function deleteReportComment(
25562593

25572594
const didCommentMentionCurrentUser = ReportActionsUtils.didMessageMentionCurrentUser(reportAction, currentEmail);
25582595
if (didCommentMentionCurrentUser && reportAction.created === report?.lastMentionedTime) {
2559-
const reportActionsForReport = allReportActions?.[reportID];
2560-
const latestMentionedReportAction = Object.values(reportActionsForReport ?? {}).find(
2596+
const reportActionsForReportID = allReportActions?.[reportID];
2597+
const latestMentionedReportAction = Object.values(reportActionsForReportID ?? {}).find(
25612598
(action) =>
25622599
action.reportActionID !== reportAction.reportActionID &&
25632600
ReportActionsUtils.didMessageMentionCurrentUser(action, currentEmail) &&
25642601
ReportActionsUtils.isReportActionVisible(action, reportID, undefined, visibleReportActionsDataParam),
25652602
);
25662603
optimisticReport.lastMentionedTime = latestMentionedReportAction?.created ?? null;
25672604
}
2605+
25682606
// If the API call fails we must show the original message again, so we revert the message content back to how it was
2569-
// and and remove the pendingAction so the strike-through clears
2607+
// and remove the pendingAction so the strike-through clears
2608+
const failureReportActionsData: NullishDeep<ReportActions> = {
2609+
[reportActionID]: {
2610+
message: reportAction.message,
2611+
pendingAction: null,
2612+
previousMessage: null,
2613+
},
2614+
};
2615+
for (const whisperID of unresolvedMentionWhisperIDs) {
2616+
failureReportActionsData[whisperID] = {
2617+
originalMessage: {deleted: null},
2618+
};
2619+
}
25702620
const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS>> = [
25712621
{
25722622
onyxMethod: Onyx.METHOD.MERGE,
25732623
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${originalReportID}`,
2574-
value: {
2575-
[reportActionID]: {
2576-
message: reportAction.message,
2577-
pendingAction: null,
2578-
previousMessage: null,
2579-
},
2580-
},
2624+
value: failureReportActionsData,
25812625
},
25822626
];
25832627

src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro
5858
const reportActionDraftMessageRef = useRef<string | undefined>(undefined);
5959
const isReportArchived = useReportIsArchived(reportIDRef.current);
6060
const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportIDRef.current}`);
61+
const reportActionsRef = useRef(reportActions);
62+
reportActionsRef.current = reportActions;
6163
const isOriginalReportArchived = useReportIsArchived(getOriginalReportID(reportIDRef.current, reportActionRef.current, reportActions));
6264
const {iouReport, chatReport, isChatIOUReportArchived} = useGetIOUReportFromReportAction(reportActionRef.current);
6365
const {transitionActionSheetState} = useActionSheetAwareScrollViewActions();
@@ -388,7 +390,16 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro
388390
} else if (reportAction) {
389391
// eslint-disable-next-line @typescript-eslint/no-deprecated
390392
InteractionManager.runAfterInteractions(() => {
391-
deleteReportComment(report, reportAction, ancestorsRef.current, isReportArchived, isOriginalReportArchived, email ?? '', visibleReportActionsData ?? undefined);
393+
deleteReportComment(
394+
report,
395+
reportAction,
396+
ancestorsRef.current,
397+
isReportArchived,
398+
isOriginalReportArchived,
399+
email ?? '',
400+
visibleReportActionsData ?? undefined,
401+
reportActionsRef.current ?? undefined,
402+
);
392403
});
393404
}
394405

src/types/onyx/OriginalMessage.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ type OriginalMessageActionableMentionWhisper = {
147147

148148
/** Collection of accountIDs of users mentioned in message */
149149
whisperedTo?: number[];
150+
151+
/** Timestamp of when the whisper was deleted (set by the backend when the parent comment is deleted) */
152+
deleted?: string | null;
150153
};
151154

152155
/** Model of `actionable card fraud alert` report action */
@@ -189,6 +192,12 @@ type OriginalMessageActionableReportMentionWhisper = {
189192

190193
/** Collection of accountIDs of users mentioned in message */
191194
whisperedTo?: number[];
195+
196+
/** Timestamp of when the whisper was deleted (set by the backend when the parent comment is deleted) */
197+
deleted?: string | null;
198+
199+
/** The reportActionID of the parent comment that triggered this whisper */
200+
reportActionID?: number;
192201
};
193202

194203
/** Model of `welcome whisper` report action */

0 commit comments

Comments
 (0)