From 6403dd7cc85d886dc69eab0e06ae6cf08e1b6470 Mon Sep 17 00:00:00 2001 From: Scott Deeter Date: Tue, 10 Feb 2026 14:34:27 -0800 Subject: [PATCH 1/6] Fix MODIFIEDEXPENSE description display to strip HTML formatting Change ModifiedExpenseMessage to use Parser.htmlToText() instead of Parser.htmlToMarkdown() when displaying oldComment/newComment values. Problem: When descriptions contained HTML formatting like Test, the code converted it to ExpensiMark markdown (~Test~) but then displayed that markdown literally as text instead of rendering it. Users saw: "changed the description to \"~Test~\"" with literal tildes. Solution: Use Parser.htmlToText() which strips HTML tags to plain text. Now displays: "changed the description to \"Test\"" cleanly without formatting markers. This matches how other fields (merchant, category) are displayed in MODIFIEDEXPENSE messages - as plain text values in quotes. Note: The actual description value is still stored with HTML formatting in the backend and renders correctly when viewing the transaction. This only affects the system message display. Co-Authored-By: Claude Sonnet 4.5 --- src/libs/ModifiedExpenseMessage.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 94519b1c86c86..99c6c945d16bd 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -277,8 +277,8 @@ function getForReportAction({ buildMessageFragmentForValue( // eslint-disable-next-line @typescript-eslint/no-deprecated translateLocal, - Parser.htmlToMarkdown(reportActionOriginalMessage?.newComment ?? ''), - Parser.htmlToMarkdown(reportActionOriginalMessage?.oldComment ?? ''), + Parser.htmlToText(reportActionOriginalMessage?.newComment ?? ''), + Parser.htmlToText(reportActionOriginalMessage?.oldComment ?? ''), descriptionLabel, true, setFragments, @@ -574,8 +574,8 @@ function getForReportActionTemp({ buildMessageFragmentForValue( translate, - Parser.htmlToMarkdown(reportActionOriginalMessage?.newComment ?? ''), - Parser.htmlToMarkdown(reportActionOriginalMessage?.oldComment ?? ''), + Parser.htmlToText(reportActionOriginalMessage?.newComment ?? ''), + Parser.htmlToText(reportActionOriginalMessage?.oldComment ?? ''), descriptionLabel, true, setFragments, From e64b2a83d078a8df98cf433fa94a766fc66dd253 Mon Sep 17 00:00:00 2001 From: Scott Deeter Date: Tue, 10 Feb 2026 14:40:35 -0800 Subject: [PATCH 2/6] Preserve HTML formatting in MODIFIEDEXPENSE description display The ReportActionItemMessageWithExplain component uses RenderHTML to render the modifiedExpenseMessage, which means it can properly display HTML tags. Previously we were stripping HTML to plain text with Parser.htmlToText(), which removed all formatting. Before that, we were converting to markdown with Parser.htmlToMarkdown(), which showed literal markdown markers. The correct approach is to pass the HTML through unchanged, allowing RenderHTML to render formatting tags like , , properly. Now messages like "changed the description to \"Strikethrough\"" will render with actual strikethrough formatting instead of showing as plain text or literal markers. Co-Authored-By: Claude Sonnet 4.5 --- src/libs/ModifiedExpenseMessage.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 99c6c945d16bd..b941e13145efb 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -277,8 +277,8 @@ function getForReportAction({ buildMessageFragmentForValue( // eslint-disable-next-line @typescript-eslint/no-deprecated translateLocal, - Parser.htmlToText(reportActionOriginalMessage?.newComment ?? ''), - Parser.htmlToText(reportActionOriginalMessage?.oldComment ?? ''), + reportActionOriginalMessage?.newComment ?? '', + reportActionOriginalMessage?.oldComment ?? '', descriptionLabel, true, setFragments, @@ -574,8 +574,8 @@ function getForReportActionTemp({ buildMessageFragmentForValue( translate, - Parser.htmlToText(reportActionOriginalMessage?.newComment ?? ''), - Parser.htmlToText(reportActionOriginalMessage?.oldComment ?? ''), + reportActionOriginalMessage?.newComment ?? '', + reportActionOriginalMessage?.oldComment ?? '', descriptionLabel, true, setFragments, From 48f2c5dea554e8d5054958160cf9bd1dc459c25a Mon Sep 17 00:00:00 2001 From: Scott Deeter Date: Tue, 10 Feb 2026 14:46:57 -0800 Subject: [PATCH 3/6] Remove Parser since it's unused --- src/libs/ModifiedExpenseMessage.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index b941e13145efb..eccbb0ff5fc80 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -14,7 +14,6 @@ import {getEnvironmentURL} from './Environment/Environment'; // eslint-disable-next-line @typescript-eslint/no-deprecated import {formatList, translateLocal} from './Localize'; import Log from './Log'; -import Parser from './Parser'; import {getPersonalDetailByEmail} from './PersonalDetailsUtils'; import {getCleanedTagName, getPolicy, getSortedTagKeys, isPolicyAdmin} from './PolicyUtils'; import {getOriginalMessage, isModifiedExpenseAction} from './ReportActionsUtils'; From 882cb5d566ec9543a3f0a323a39082a06d06c21c Mon Sep 17 00:00:00 2001 From: Scott Deeter Date: Tue, 10 Feb 2026 15:56:00 -0800 Subject: [PATCH 4/6] Strip HTML from MODIFIEDEXPENSE messages for plain-text consumers Fixed issue where passing raw HTML through getForReportAction() was breaking plain-text consumers that now received literal HTML tags instead of readable text. Updated the following plain-text consumers to strip HTML using Parser.htmlToText(): - BrowserNotifications: For notification bodies - ReportNameUtils: For report preview text - ReportUtils: For formatReportLastMessageText - OptionsListUtils: For options list display - ContextMenuActions: For clipboard copy The ReportActionItem component (which uses RenderHTML) continues to receive HTML for proper formatting display. Fixes review feedback from https://github.com/Expensify/App/pull/82057 Co-Authored-By: Claude Sonnet 4.5 --- .../Notification/LocalNotification/BrowserNotifications.ts | 4 +++- src/libs/OptionsListUtils/index.ts | 4 +++- src/libs/ReportNameUtils.ts | 4 +++- src/libs/ReportUtils.ts | 4 +++- src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx | 4 +++- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.ts b/src/libs/Notification/LocalNotification/BrowserNotifications.ts index 82a0321497898..6b73093ea86d1 100644 --- a/src/libs/Notification/LocalNotification/BrowserNotifications.ts +++ b/src/libs/Notification/LocalNotification/BrowserNotifications.ts @@ -132,12 +132,14 @@ export default { pushModifiedExpenseNotification({report, reportAction, movedFromReport, movedToReport, onClick, usesIcon = false}: LocalNotificationModifiedExpensePushParams) { const title = reportAction.person?.map((f) => f.text).join(', ') ?? ''; - const body = getForReportAction({ + const bodyWithHTML = getForReportAction({ reportAction, policyID: report.policyID, movedFromReport, movedToReport, }); + // Strip HTML tags for plain text notification body + const body = getTextFromHtml(bodyWithHTML); const icon = usesIcon ? EXPENSIFY_ICON_URL : ''; const data = { reportID: report.reportID, diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index 37db6ad9d467e..53dabae598e89 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -692,13 +692,15 @@ function getLastMessageTextForReport({ } else if (isReportMessageAttachment({text: report?.lastMessageText ?? '', html: report?.lastMessageHtml, type: ''})) { lastMessageTextFromReport = `[${translate('common.attachment')}]`; } else if (isModifiedExpenseAction(lastReportAction)) { - const properSchemaForModifiedExpenseMessage = getForReportAction({ + const properSchemaForModifiedExpenseMessageWithHTML = getForReportAction({ reportAction: lastReportAction, policyID: report?.policyID, movedFromReport, movedToReport, policyForMovingExpensesID, }); + // Strip HTML tags for plain text display in options list + const properSchemaForModifiedExpenseMessage = Parser.htmlToText(properSchemaForModifiedExpenseMessageWithHTML); lastMessageTextFromReport = formatReportLastMessageText(properSchemaForModifiedExpenseMessage, true); } else if (isMovedTransactionAction(lastReportAction)) { lastMessageTextFromReport = Parser.htmlToText(getMovedTransactionMessage(translate, lastReportAction)); diff --git a/src/libs/ReportNameUtils.ts b/src/libs/ReportNameUtils.ts index 9d3a1b4ccc7b0..d3452f0932167 100644 --- a/src/libs/ReportNameUtils.ts +++ b/src/libs/ReportNameUtils.ts @@ -697,12 +697,14 @@ function computeChatThreadReportName( const movedFromReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(parentReportAction, CONST.REPORT.MOVE_TYPE.FROM)}`]; const movedToReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(parentReportAction, CONST.REPORT.MOVE_TYPE.TO)}`]; - const modifiedMessage = getForReportAction({ + const modifiedMessageWithHTML = getForReportAction({ reportAction: parentReportAction, policyID, movedFromReport, movedToReport, }); + // Strip HTML tags for plain text display in report previews + const modifiedMessage = Parser.htmlToText(modifiedMessageWithHTML); return formatReportLastMessageText(modifiedMessage); } if (isTripRoom(report) && report?.reportName !== CONST.REPORT.DEFAULT_REPORT_NAME) { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 6389c84019d9f..c29643171c21d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5782,12 +5782,14 @@ function getReportName( const movedFromReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(parentReportAction, CONST.REPORT.MOVE_TYPE.FROM)}`]; const movedToReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(parentReportAction, CONST.REPORT.MOVE_TYPE.TO)}`]; - const modifiedMessage = getForReportAction({ + const modifiedMessageWithHTML = getForReportAction({ reportAction: parentReportAction, policyID, movedFromReport, movedToReport, }); + // Strip HTML tags for plain text display in report last message + const modifiedMessage = Parser.htmlToText(modifiedMessageWithHTML); return formatReportLastMessageText(modifiedMessage); } if (isTripRoom(report) && report?.reportName !== CONST.REPORT.DEFAULT_REPORT_NAME) { diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx index e507a4be8d03c..4785a41f1eafe 100644 --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx @@ -762,7 +762,7 @@ const ContextMenuActions: ContextMenuAction[] = [ const displayMessage = html ?? text; setClipboardMessage(displayMessage); } else if (isModifiedExpenseAction(reportAction)) { - const modifyExpenseMessage = getForReportActionTemp({ + const modifyExpenseMessageWithHTML = getForReportActionTemp({ translate, reportAction, policy, @@ -770,6 +770,8 @@ const ContextMenuActions: ContextMenuAction[] = [ movedToReport, policyTags, }); + // Strip HTML tags for plain text clipboard copy + const modifyExpenseMessage = Parser.htmlToText(modifyExpenseMessageWithHTML); Clipboard.setString(modifyExpenseMessage); } else if (isReimbursementDeQueuedOrCanceledAction(reportAction)) { const displayMessage = getReimbursementDeQueuedOrCanceledActionMessage(translate, reportAction, report); From 9e663fd1a8a8e2e7480051c79c0b0ff6b8dd46b5 Mon Sep 17 00:00:00 2001 From: Scott Deeter Date: Fri, 6 Mar 2026 12:45:32 -0500 Subject: [PATCH 5/6] Add back Parser --- src/libs/ModifiedExpenseMessage.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/ModifiedExpenseMessage.ts b/src/libs/ModifiedExpenseMessage.ts index 5fb5f62b25e4d..32ea673a297d2 100644 --- a/src/libs/ModifiedExpenseMessage.ts +++ b/src/libs/ModifiedExpenseMessage.ts @@ -16,6 +16,7 @@ import {getEnvironmentURL} from './Environment/Environment'; // eslint-disable-next-line @typescript-eslint/no-deprecated import {formatList, translateLocal} from './Localize'; import Log from './Log'; +import Parser from './Parser'; import {getPersonalDetailByEmail} from './PersonalDetailsUtils'; import {getCleanedTagName, getCommaSeparatedTagNameWithSanitizedColons, getPolicy, getSortedTagKeys, isPolicyAdmin} from './PolicyUtils'; import {getOriginalMessage, isModifiedExpenseAction} from './ReportActionsUtils'; From a11dfb52577a4664141c225892b038cb030ae8db Mon Sep 17 00:00:00 2001 From: Scott Deeter Date: Thu, 12 Mar 2026 16:04:37 -0700 Subject: [PATCH 6/6] Use htmlToMarkdown when copying --- src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx index c3fd298cab242..c43efc10f50e8 100644 --- a/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/inbox/report/ContextMenu/ContextMenuActions.tsx @@ -781,8 +781,8 @@ const ContextMenuActions: ContextMenuAction[] = [ policyTags, currentUserLogin: currentUserPersonalDetails?.email ?? '', }); - // Strip HTML tags for plain text clipboard copy - const modifyExpenseMessage = Parser.htmlToText(modifyExpenseMessageWithHTML); + // Convert HTML to markdown for clipboard copy to preserve links and formatting + const modifyExpenseMessage = Parser.htmlToMarkdown(modifyExpenseMessageWithHTML); Clipboard.setString(modifyExpenseMessage); } else if (isReimbursementDeQueuedOrCanceledAction(reportAction)) { const displayMessage = getReimbursementDeQueuedOrCanceledActionMessage(translate, reportAction, report);