Skip to content

Fix alternate text prefix mismatch between LHN and search - #98961

Merged
francoisl merged 17 commits into
Expensify:mainfrom
callstack-internal:fix/alt-diff-search-lhn
Aug 25, 2026
Merged

francoisl merged 17 commits into
Expensify:mainfrom
callstack-internal:fix/alt-diff-search-lhn

Conversation

@sosek108

@sosek108 sosek108 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

The alternate text preview shown for a chat in the search router / pickers (built by OptionsListUtils.getAlternateText) could disagree with the preview shown for the same chat in the LHN (built by SidebarUtils.getOptionData). Two mismatches were fixed:

  1. Missing/incorrect actor name prefix ("Name: message"): The LHN uses two different rules to decide whether to prefix the last message with the sender's name — one for chat rooms/policy expense chats/threads/task reports/group chats (always prefixed for regular messages, no extra gating) and another for DMs/expense reports (gated by shouldShowLastActorDisplayName). Search was not mirroring this logic. Added an internal helper, getChatPreviewActorPrefix, in OptionsListUtils that reproduces the LHN's exact two-regime behavior, including the actor resolution fallback chain (last action's actor → action.personreport.lastActorAccountID).
  2. Actor prefix shown for actions that shouldn't have one: The LHN special-cases certain actions (policy/room changelog actions, renamed, task actions, invite/remove, card issued, OldDot actions, etc.) and renders custom text for them without any actor prefix. Added isActionWithCustomAlternateText in OptionsListUtils, mirroring that same if/else chain, so search no longer adds a prefix to those actions.
  3. HTML-like text not shown as typed: For issue [Due for Payment 2026-04-09][$500] Search - HTML message is displayed different on LHN and on chat preview on search #82036, SearchAutocompleteList had a display-layer override that ran the alternate text through Parser.htmlToText unless report.lastActionType === ADD_COMMENT. This logic has been moved into getAlternateText itself, so literal HTML-like text a user types (e.g. <b>test</b>) now displays as typed consistently, matching the LHN. The now-redundant override in SearchAutocompleteList was removed, leaving alternateText with a single source of truth.

Fixed Issues

$ #98958
PROPOSAL:

Tests

  1. Open the app and note a few LHN rows and their preview lines (second line under the report name):
    • a 1:1 DM where you sent the last message (shows You: <message>)
    • a room, thread, or group chat where someone else sent the last message (shows <Name>: <message>)
    • a chat whose last message contains literal HTML-like text, e.g. send <b>test</b> (LHN shows it as typed)
  2. Open the search router (Cmd+K / magnifying glass).
  3. In the "Recent chats" section, verify each of the reports from step 1 shows the exact same preview line as in the LHN (same You: / <Name>: prefix, HTML-like text shown as typed).
  4. Verify a report whose last action is a special/changelog action (e.g. workspace setting change, renamed room) shows no actor prefix in search, matching the LHN.
  • Verify that no errors appear in the JS console

Offline tests

N/A

QA Steps

Same as tests

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
Nagranie.z.ekranu.2026-08-19.o.11.28.32.mov
iOS: mWeb Safari
MacOS: Chrome / Safari
Nagranie.z.ekranu.2026-08-19.o.10.27.04.mov
Nagranie.z.ekranu.2026-08-19.o.10.27.51.mov

@codecov

codecov Bot commented Aug 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
src/components/Search/SearchAutocompleteList.tsx 92.63% <ø> (-0.08%) ⬇️
src/libs/OptionsListUtils/index.ts 87.42% <100.00%> (+0.11%) ⬆️
src/libs/ReportActionsUtils.ts 80.40% <ø> (+0.34%) ⬆️
src/libs/SidebarUtils.ts 80.11% <ø> (ø)
src/libs/OptionsListUtils/getChatPreviewParts.ts 95.53% <95.53%> (ø)
... and 47 files with indirect coverage changes

@sosek108
sosek108 marked this pull request as ready for review August 19, 2026 12:05
@sosek108
sosek108 requested review from a team as code owners August 19, 2026 12:05
@melvin-bot
melvin-bot Bot requested review from JmillsExpensify and thelullabyy and removed request for a team August 19, 2026 12:05
@melvin-bot

melvin-bot Bot commented Aug 19, 2026

Copy link
Copy Markdown

@thelullabyy Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot
melvin-bot Bot removed request for a team and JmillsExpensify August 19, 2026 12:05
@sosek108

Copy link
Copy Markdown
Contributor Author

We are adding some logic to Opening Search so there is expected some performance regression from this. For big account on iOS simulator I calculated 7% regression

image

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 08213a14cc

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/libs/OptionsListUtils/index.ts Outdated
Comment thread src/libs/OptionsListUtils/index.ts Outdated
@sosek108

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e3a4dc826a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/libs/OptionsListUtils/index.ts Outdated
Comment on lines +558 to +561
const sortedActionsForReport = sortedActions?.[report.reportID];
const lastAction = sortedActionsForReport
? sortedActionsForReport.find((action) => isReportActionVisibleAsLastAction(action, canUserPerformWrite, visibleReportActionsData, report.reportID, currentUserAccountID))
: getLastVisibleAction(report.reportID, canUserPerformWrite, {}, undefined, visibleReportActionsData);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Forward sorted actions when deriving the preview actor

When createOption() receives sortedActions, it uses them to compute result.lastMessageText but does not forward them into getAlternateText(), so this fallback scans only the parent report's actions. For a one-transaction report whose newest visible comment is in the transaction thread, the preview text therefore comes from the child action while the newly added actor prefix comes from the parent's last action; OptionRowLHNData includes the transaction thread when selecting lastAction, so the picker/search option can attribute the message to a different person than the LHN. Forward sortedActions through that call or make this fallback include the transaction thread.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sosek108 Could you please check this comment?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment: #98961 (comment)

reportAttributesDerived,
})
: {actorPrefix: '', customAlternateText: undefined};
const formattedLastMessageTextWithPrefix = reportPrefix + actorPrefix + (customAlternateText ?? formattedLastMessageText);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Format the actor and message as one preview

For regular messages at the 200-character preview limit, formattedLastMessageText is truncated before actorPrefix is prepended. The LHN instead calls formatReportLastMessageText() on the combined "Name: message", so it truncates the message early enough that the whole actor-prefixed preview remains within the limit. This produces different preview strings—and an over-limit search alternate text—precisely for long messages; format the combined actor and message before adding the report subtitle prefix.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sosek108 Could you please check this comment?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is ongoing effort to completely merge LHN and Search visibility of alternateText so I treat this PR as immediate fix for not showing the actor in Search.

I've tested solution for this comment and it may take a lot of time for this edge case to resolve so I proposed that we leave this and focus on the next step. Let me know if you disagree with me - I'm open for discussion

check https://expensify.slack.com/archives/C08CZDJFJ77/p1787150347363979?thread_ts=1787056222.560989&cid=C08CZDJFJ77

@thelullabyy

thelullabyy commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
Android: mWeb Chrome
iOS: HybridApp
Screen.Recording.2026-08-25.at.17.41.16.mov
iOS: mWeb Safari
MacOS: Chrome / Safari
Screen.Recording.2026-08-25.at.17.36.44.mov

Comment thread src/libs/OptionsListUtils/index.ts Outdated
isPolicyCopyReportAction(lastAction) ||
isMovedTransactionAction(lastAction) ||
(isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT) && !!getOriginalMessage(lastAction)?.resolution) ||
POLICY_CHANGE_LOG_ACTION_NAMES.has(actionName) ||

@thelullabyy thelullabyy Aug 20, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I don't think we handle alternative text for all actions in POLICY_CHANGE_LOG

They fall through to the generic Name: message branch and do get a prefix in the LHN, example: LEAVE_POLICY, DELETE_CATEGORIES,...

So for those, search now drops a prefix the LHN shows https://github.com/Expensify/App/pull/98961/changes#diff-48f7edf19f169a2c5131f019a4701a02e1207efdf4ce3d00b1e81c247330e80fR617 @sosek108

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good comment, done

Comment thread src/libs/OptionsListUtils/index.ts Outdated
const sortedActionsForReport = sortedActions?.[report.reportID];
const lastAction = sortedActionsForReport
? sortedActionsForReport.find((action) => isReportActionVisibleAsLastAction(action, canUserPerformWrite, visibleReportActionsData, report.reportID, currentUserAccountID))
: getLastVisibleAction(report.reportID, canUserPerformWrite, {}, undefined, visibleReportActionsData);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should pass currentUserAccountID, we are should not introduce new caller which missing that param as per #66408

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, done

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, done

@thelullabyy

Copy link
Copy Markdown
Contributor

I think we should add UTs for new functions @sosek108

@sosek108

Copy link
Copy Markdown
Contributor Author

@thelullabyy thanks for feedback, I'm working on these right now

@sosek108

Copy link
Copy Markdown
Contributor Author

@thelullabyy I've merged fresh main

@sosek108

Copy link
Copy Markdown
Contributor Author

@thelullabyy do you need anything from me to continue this review?

@thelullabyy

Copy link
Copy Markdown
Contributor

Reviewing...

@melvin-bot

melvin-bot Bot commented Aug 25, 2026

Copy link
Copy Markdown

We did not find an internal engineer to review this PR, trying to assign a random engineer to #98958 as well as to this PR... Please reach out for help on Slack if no one gets assigned!

@melvin-bot
melvin-bot Bot requested a review from francoisl August 25, 2026 09:52
@mountiny
mountiny self-requested a review August 25, 2026 10:24
@mountiny

Copy link
Copy Markdown
Contributor

@codex review
@MelvinBot review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c396864f87

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/libs/OptionsListUtils/index.ts Outdated
Comment on lines +561 to +564
const sortedActionsForReport = sortedActions?.[report.reportID];
const lastAction = sortedActionsForReport
? sortedActionsForReport.find((action) => isReportActionVisibleAsLastAction(action, canUserPerformWrite, visibleReportActionsData, report.reportID, currentUserAccountID))
: getLastVisibleAction(report.reportID, canUserPerformWrite, {}, undefined, visibleReportActionsData, currentUserAccountID);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve the actor from the transaction thread

For a one-transaction report whose newest visible comment is in its transaction thread, this lookup only examines sortedActions[report.reportID] (or the parent's cached actions), while OptionRowLHNData selects across both the parent and transaction-thread reports and getLastMessageTextForReport() can likewise derive the preview text from that child. The resulting search row can therefore show the child comment with the parent's actor prefix. Fresh evidence in this revision is that the newly added helper explicitly restricts actor resolution to the parent report; include the one-transaction thread when selecting lastAction.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sosek108 can you take a look please?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — with one clarification on scope: when callers pass the derived sortedActions, the one-transaction thread was already covered, because RAM_ONLY_SORTED_REPORT_ACTIONS stores the combined parent + transaction-thread actions (via getCombinedReportActions), so the find() over sortedActions[report.reportID] already selects child comments. The gap was only in the fallback branch for callers that don't pass sortedActions yet (#66381): it used getLastVisibleAction, which scans the parent report only.

The fallback now uses getLastVisibleActionIncludingTransactionThread with deprecatedCachedOneTransactionThreadReportIDs — the exact same lookup getLastMessageTextForReport uses to derive the preview text — so the actor prefix and the message text are selected from the same action. Added a regression test with a one-transaction expense report whose newest visible action is a transaction-thread comment; it fails on the previous revision (parent actor prefix) and passes now (thread comment actor).

Comment on lines +697 to +699
const isLastActionAddComment = report?.lastActionType === CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT;
const formattedLastMessageText =
formatReportLastMessageText(Parser.htmlToText(option.lastMessageText ?? '')) ||
formatReportLastMessageText(isLastActionAddComment ? (option.lastMessageText ?? '') : Parser.htmlToText(option.lastMessageText ?? '')) ||

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Determine comment formatting from the resolved last action

When a one-transaction report's newest visible message is an ADD_COMMENT in its transaction thread, the parent commonly still has lastActionType === REPORT_PREVIEW. Although option.lastMessageText is derived from the child comment, this parent-level check takes the HTML-stripping branch, so a literal comment such as <b>test</b> becomes test in search while the LHN preserves the typed text. Base this decision on the same resolved last visible action used for the preview rather than report.lastActionType.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this exact scenario was identified and handled earlier. It's even in proposed test steps of this PR.

@MelvinBot

Copy link
Copy Markdown
Contributor

Code review

Overview: This PR makes the search-router/picker chat preview (OptionsListUtils.getAlternateText) match the LHN preview (SidebarUtils.getOptionData) by reproducing the LHN's actor-name prefix rules (getChatPreviewParts), its custom-alternate-text action list (isActionWithCustomAlternateText), and its HTML-vs-typed handling (moved into getAlternateText, override removed from SearchAutocompleteList). The approach is sound and well tested. I verified the mirror is faithful in almost every respect — the two-regime prefix split, the shared shouldShowLastActorDisplayName gate, the last-actor fallback chain, the ROOM_CHANGE_LOG/copy-action coverage, and the HTML-strip refactor are all equivalent to the LHN. One concrete prefix-mismatch gap remains, plus a few things worth confirming.

🟠 Prefix mismatch still possible for ADD_CUSTOM_UNIT / DELETE_CUSTOM_UNIT

isActionWithCustomAlternateText treats every POLICY_CHANGE_LOG action as custom (so search drops the actor prefix) except the six listed in POLICY_CHANGE_LOG_ACTIONS_WITHOUT_CUSTOM_TEXT. But SidebarUtils.getOptionData has no custom branch for the bare POLICY_CHANGE_LOG.ADD_CUSTOM_UNIT or POLICY_CHANGE_LOG.DELETE_CUSTOM_UNIT (it only special-cases UPDATE_CUSTOM_UNIT and the *_RATE / *_SUB_RATE variants), so the LHN falls through to the generic Name: message prefix for them.

Result: in a room/policy-expense/thread/task/group chat whose last action is ADD_CUSTOM_UNIT or DELETE_CUSTOM_UNIT, the LHN shows Jane: <msg> while search shows <msg> — the exact class of mismatch this PR fixes.

Fix: add ADD_CUSTOM_UNIT and DELETE_CUSTOM_UNIT to POLICY_CHANGE_LOG_ACTIONS_WITHOUT_CUSTOM_TEXT. (Real-world frequency of these as a last action is low, so impact is minor — but the fix is one line and keeps the mirror exact.)

🟡 Custom-text body may still differ for non-actor-embedding actions

getChatPreviewParts only reconstructs the custom text for rename, leave-room, and invite/remove. For the other custom actions (RETRACTED, REOPENED, card issued, task, integration-sync-failed, etc.) it correctly suppresses the prefix but then falls back to htmlToText(option.lastMessageText). That matches the LHN only if report.lastMessageText already equals the LHN's rendered text — but the LHN renders several of these as translated constants (e.g. RETRACTEDtranslate('iou.retracted')). The prefix will match; the body text can still diverge. This looks like an intentional scope boundary — just calling it out so it's a conscious decision, and worth a follow-up note if full body parity is expected.

Lower-priority notes & things I verified
  • Invite/remove room-name resolver differs. Search resolves the room name via getReportName + a self-fetched getReportOrDraftReport, whereas the LHN uses deprecatedGetReportName on a caller-passed report. Different resolvers could yield a different room-name suffix in edge cases.
  • Last-action identity. getChatPreviewParts re-resolves lastAction from sortedActions/getLastVisibleAction rather than trusting a caller-passed action like getOptionData does. If the two resolve to different last actions for the same report, previews can diverge independently of the mirror logic.
  • Performance — not a concern. getAlternateText runs per option, but the option list is capped to ~20 per group before the loop, and in the search path sortedActions is supplied so getChatPreviewParts uses the in-memory .find branch, not an Onyx getLastVisibleAction read. The heavy Onyx branch is only reachable when sortedActions is absent, and the createOption callers that lack it all pass showChatPreviewLine: false, so they never enter getChatPreviewParts. Net cost is a bounded ~20-option pass per keystroke — low.
  • HTML-strip refactor is behavior-equivalent. The removed SearchAutocompleteList override was already keyed on report.lastActionType === ADD_COMMENT; getAlternateText now uses the same key. The separate parent-action-based decision (shouldParserToHTML) governs the text/title field, not alternateText, and is untouched.
  • lastActorDetails: null → real value in prepareReportOptionsForDisplay only affects the archive-reason preview text for archived non-expense reports (empty lastMessageText path), and it aligns that call with createOption/LHN. Narrow, likely intended.
  • currentUserAccountID === undefined silently drops the prefix, but both call sites forward it — so at most a transient no-prefix during initial personal-details load, not a persistent bug.
  • Test coverage is strong: rooms, DMs, You:/Name: prefixes, archived, report-preview suppression, person-text fallback, rename/leave/invite/remove text, HTML-as-typed, the Onyx-vs-sortedActions equivalence, and a direct getAlternateTextSidebarUtils.getOptionData parity assertion. Adding a case for ADD_CUSTOM_UNIT/DELETE_CUSTOM_UNIT would lock in the fix above.

Nice work — the parity test comparing directly against SidebarUtils.getOptionData is exactly the right guard for this kind of "keep two code paths in sync" change. Addressing the custom-unit gap is the only change I'd consider blocking; the rest are confirmations and follow-up notes.


view run

Comment thread src/libs/OptionsListUtils/index.ts Outdated
Comment on lines +528 to +531
type ChatPreviewParts = {
actorPrefix: string;
customAlternateText?: string;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added docs

Comment thread src/libs/OptionsListUtils/index.ts Outdated
Comment on lines +561 to +564
const sortedActionsForReport = sortedActions?.[report.reportID];
const lastAction = sortedActionsForReport
? sortedActionsForReport.find((action) => isReportActionVisibleAsLastAction(action, canUserPerformWrite, visibleReportActionsData, report.reportID, currentUserAccountID))
: getLastVisibleAction(report.reportID, canUserPerformWrite, {}, undefined, visibleReportActionsData, currentUserAccountID);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sosek108 can you take a look please?

@sosek108

Copy link
Copy Markdown
Contributor Author

Response to MelvinBot's comment:

finding was correct. I added ADD_CUSTOM_UNIT/DELETE_CUSTOM_UNIT to POLICY_CHANGE_LOG_ACTIONS_WITHOUT_CUSTOM_TEXT and test asserting getAlternateText results is same as SidebarUtils.getOptionData().alternateText for both.

On the 🟡 custom-text body divergence: correct and intentional. This PR scopes to prefix parity plus the actions whose LHN text embeds the actor (rename, leave, invite/remove). Full body parity for the remaining custom actions belongs to the ongoing LHN/Search alternate-text merge effort and will be handled there. It will be resolved with next PR that will be posted to review right after this one is merged

@sosek108
sosek108 requested a review from mountiny August 25, 2026 13:13

@mountiny mountiny left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing those

@francoisl
francoisl merged commit 9170eb4 into Expensify:main Aug 25, 2026
37 of 38 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

🚧 francoisl has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/francoisl in version: 9.4.61-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@MelvinBot

Copy link
Copy Markdown
Contributor

Help site review — no docs changes required

I reviewed the changes in this PR against the help site articles under App/docs/articles and no help-site updates are needed. I did not create a draft PR.

Why: This PR is an internal UI-consistency bug fix. It makes the chat preview line shown in the search router / pickers (OptionsListUtils.getAlternateText) match the preview already shown in the LHN (SidebarUtils.getOptionData) — specifically the Name: message actor prefix, the suppression of that prefix for special/changelog actions, and rendering literal HTML-like text (e.g. <b>test</b>) as typed. It adds no new feature, setting, tab, button, or workflow.

The help site documents user-facing features and how to use them; it does not document the micro-formatting of chat message preview lines in the LHN or search. I searched App/docs/articles for any article describing message-preview formatting, the search router "Recent chats" preview, or the actor-name prefix behavior, and found none — so there is nothing to update to reflect this change.

If you believe a specific help article does describe this behavior and should be updated, point me to it and I'll take another look.


view run

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/AndrewGable in version: 9.4.61-4 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

Bundle Size Analysis (Sentry):

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants