From 21ea5cd2ee656e10c3237a3b1a52047c7fd8b9bf Mon Sep 17 00:00:00 2001 From: Scott Deeter Date: Tue, 24 Mar 2026 11:38:14 -0700 Subject: [PATCH 1/5] fix: correctly resolve mention whispers created during message edits When a message is edited to add a new @mention, the backend creates a second ACTIONABLEMENTIONWHISPER with a random ID (not following the parentID+1 convention). When that parent comment is deleted, the backend now stores the parent's reportActionID in the whisper's originalMessage. Update isResolvedActionableWhisper to use this stored reportActionID (when present) to find the parent, falling back to offset arithmetic for legacy whispers that predate this field. Add reportActionID to the OriginalMessageActionableMentionWhisper type accordingly. Co-Authored-By: Claude Sonnet 4.6 --- src/libs/ReportActionsUtils.ts | 9 +++++++-- src/types/onyx/OriginalMessage.ts | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 8e137510bfa7..4feaed126755 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1106,8 +1106,13 @@ function isResolvedActionableWhisper(reportAction: OnyxEntry, allA const reportID = reportAction.reportID; const actions = allActionsForReport ?? (reportID ? allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] : undefined); if (actions) { - const parentOffset = isActionableReportMentionWhisper(reportAction) ? 2n : 1n; - const parentActionID = String(BigInt(reportAction.reportActionID) - parentOffset); + // Prefer the stored reportActionID from the whisper's originalMessage when available (set for + // whispers created during message edits, which don't follow the parentID+1 ID convention). + // Fall back to offset arithmetic for legacy whispers that predate this field. + const storedParentID = isActionableMentionWhisper(reportAction) ? (originalMessage as {reportActionID?: number}).reportActionID : undefined; + const parentActionID = storedParentID + ? String(storedParentID) + : String(BigInt(reportAction.reportActionID) - (isActionableReportMentionWhisper(reportAction) ? 2n : 1n)); const parentAction = actions[parentActionID]; if (parentAction && !isDeletedAction(parentAction)) { return false; diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 1cc3f331a492..c0e376941d41 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -150,6 +150,10 @@ type OriginalMessageActionableMentionWhisper = { /** Timestamp of when the whisper was deleted (set by the backend when the parent comment is deleted) */ deleted?: string | null; + + /** The reportActionID of the parent comment that triggered this whisper. Used to find the parent when this + * whisper was created during a message edit (and therefore doesn't follow the parentID+1 ID convention). */ + reportActionID?: number; }; /** Model of `actionable card fraud alert` report action */ From 40dc6d7f168c5699343900f1fe7e72750f49d1a6 Mon Sep 17 00:00:00 2001 From: Scott Deeter Date: Tue, 24 Mar 2026 15:35:35 -0700 Subject: [PATCH 2/5] style: apply prettier formatting --- src/libs/ReportActionsUtils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 4feaed126755..e8ffc5f0fb77 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1110,9 +1110,7 @@ function isResolvedActionableWhisper(reportAction: OnyxEntry, allA // whispers created during message edits, which don't follow the parentID+1 ID convention). // Fall back to offset arithmetic for legacy whispers that predate this field. const storedParentID = isActionableMentionWhisper(reportAction) ? (originalMessage as {reportActionID?: number}).reportActionID : undefined; - const parentActionID = storedParentID - ? String(storedParentID) - : String(BigInt(reportAction.reportActionID) - (isActionableReportMentionWhisper(reportAction) ? 2n : 1n)); + const parentActionID = storedParentID ? String(storedParentID) : String(BigInt(reportAction.reportActionID) - (isActionableReportMentionWhisper(reportAction) ? 2n : 1n)); const parentAction = actions[parentActionID]; if (parentAction && !isDeletedAction(parentAction)) { return false; From 9c96e5df7591d4c6da5bfc7312c626c5e9b41b91 Mon Sep 17 00:00:00 2001 From: Scott Deeter Date: Tue, 24 Mar 2026 16:07:13 -0700 Subject: [PATCH 3/5] Type reportActionID as string to preserve int64 precision The backend stores the parent comment's reportActionID as a JSON string (to avoid precision loss when parsed by JavaScript). Update the TypeScript type and remove the String() wrapper since the value is already a string. Co-Authored-By: Claude Sonnet 4.6 --- src/libs/ReportActionsUtils.ts | 4 ++-- src/types/onyx/OriginalMessage.ts | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index e8ffc5f0fb77..2b9a796a0290 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1109,8 +1109,8 @@ function isResolvedActionableWhisper(reportAction: OnyxEntry, allA // Prefer the stored reportActionID from the whisper's originalMessage when available (set for // whispers created during message edits, which don't follow the parentID+1 ID convention). // Fall back to offset arithmetic for legacy whispers that predate this field. - const storedParentID = isActionableMentionWhisper(reportAction) ? (originalMessage as {reportActionID?: number}).reportActionID : undefined; - const parentActionID = storedParentID ? String(storedParentID) : String(BigInt(reportAction.reportActionID) - (isActionableReportMentionWhisper(reportAction) ? 2n : 1n)); + const storedParentID = isActionableMentionWhisper(reportAction) ? (originalMessage as {reportActionID?: string}).reportActionID : undefined; + const parentActionID = storedParentID ?? String(BigInt(reportAction.reportActionID) - (isActionableReportMentionWhisper(reportAction) ? 2n : 1n)); const parentAction = actions[parentActionID]; if (parentAction && !isDeletedAction(parentAction)) { return false; diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index c0e376941d41..6d13d7556f0c 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -152,8 +152,9 @@ type OriginalMessageActionableMentionWhisper = { deleted?: string | null; /** The reportActionID of the parent comment that triggered this whisper. Used to find the parent when this - * whisper was created during a message edit (and therefore doesn't follow the parentID+1 ID convention). */ - reportActionID?: number; + * whisper was created during a message edit (and therefore doesn't follow the parentID+1 ID convention). + * Stored as a string by the backend to preserve full int64 precision. */ + reportActionID?: string; }; /** Model of `actionable card fraud alert` report action */ @@ -200,8 +201,9 @@ type OriginalMessageActionableReportMentionWhisper = { /** Timestamp of when the whisper was deleted (set by the backend when the parent comment is deleted) */ deleted?: string | null; - /** The reportActionID of the parent comment that triggered this whisper */ - reportActionID?: number; + /** The reportActionID of the parent comment that triggered this whisper. + * Stored as a string by the backend to preserve full int64 precision. */ + reportActionID?: string; }; /** Model of `welcome whisper` report action */ From c756b7e5d3e3fec681175f9f8d5cfdd14c9640ea Mon Sep 17 00:00:00 2001 From: Scott Deeter Date: Wed, 25 Mar 2026 17:01:49 -0700 Subject: [PATCH 4/5] Rename reportActionID to parentReportActionID --- src/libs/ReportActionsUtils.ts | 2 +- src/types/onyx/OriginalMessage.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 2b9a796a0290..05de16527907 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1109,7 +1109,7 @@ function isResolvedActionableWhisper(reportAction: OnyxEntry, allA // Prefer the stored reportActionID from the whisper's originalMessage when available (set for // whispers created during message edits, which don't follow the parentID+1 ID convention). // Fall back to offset arithmetic for legacy whispers that predate this field. - const storedParentID = isActionableMentionWhisper(reportAction) ? (originalMessage as {reportActionID?: string}).reportActionID : undefined; + const storedParentID = isActionableMentionWhisper(reportAction) ? (originalMessage as {parentReportActionID?: string}).parentReportActionID : undefined; const parentActionID = storedParentID ?? String(BigInt(reportAction.reportActionID) - (isActionableReportMentionWhisper(reportAction) ? 2n : 1n)); const parentAction = actions[parentActionID]; if (parentAction && !isDeletedAction(parentAction)) { diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 6d13d7556f0c..23f9a1e901a3 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -154,7 +154,7 @@ type OriginalMessageActionableMentionWhisper = { /** The reportActionID of the parent comment that triggered this whisper. Used to find the parent when this * whisper was created during a message edit (and therefore doesn't follow the parentID+1 ID convention). * Stored as a string by the backend to preserve full int64 precision. */ - reportActionID?: string; + parentReportActionID?: string; }; /** Model of `actionable card fraud alert` report action */ @@ -203,7 +203,7 @@ type OriginalMessageActionableReportMentionWhisper = { /** The reportActionID of the parent comment that triggered this whisper. * Stored as a string by the backend to preserve full int64 precision. */ - reportActionID?: string; + parentReportActionID?: string; }; /** Model of `welcome whisper` report action */ From b3aafabf695df3e61b2245f9c02a88b1cfc224d4 Mon Sep 17 00:00:00 2001 From: Scott Deeter Date: Fri, 10 Apr 2026 11:36:50 -0700 Subject: [PATCH 5/5] Scan all actions for unresolved action whispers --- src/libs/actions/Report/index.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 2eafeb7280e3..2742fd1d5ce1 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -2624,6 +2624,22 @@ function deleteReportComment( } } + // Whispers created during a message edit receive a random ID (not parentID+1/+2), but the + // backend stores the parent's reportActionID in originalMessage.parentReportActionID. Scan + // all actions to catch any such whispers that the offset lookup above would miss. + for (const [actionID, action] of Object.entries(reportActionsForReport)) { + if (unresolvedMentionWhisperIDs.includes(actionID)) { + continue; + } + if (!ReportActionsUtils.isActionableMentionWhisper(action) && !ReportActionsUtils.isActionableReportMentionWhisper(action)) { + continue; + } + const originalMessage = ReportActionsUtils.getOriginalMessage(action); + if (originalMessage?.parentReportActionID === reportActionID && !originalMessage?.resolution && !originalMessage?.deleted) { + unresolvedMentionWhisperIDs.push(actionID); + } + } + const deletedTime = DateUtils.getDBTime(); for (const whisperID of unresolvedMentionWhisperIDs) { optimisticReportActions[whisperID] = {