Make system/violation messages text-selectable on desktop web#92142
Conversation
Co-authored-by: Sahil <thesahindia@users.noreply.github.com>
|
@thesahindia I created this PR from an upstream branch since I don't have push access to your fork. To take ownership of this branch and be able to push updates, run: Then you can close this PR and open a new one from your fork. |
|
@MelvinBot, merge main. |
…Selectable Co-authored-by: Sahil <thesahindia@users.noreply.github.com>
|
🤖 Merged the latest |
|
I analyzed the two failing checks. Neither is caused by this PR, and there's nothing to fix on this branch.
|
|
I analyzed the failing checks. The
|
|
The failing Root cause (on
|
|
@MelvinBot, Inline error messages aren't selectable. |
Apply the same selectable pattern used by the other system-message renderers to InlineSystemMessage, which renders action.error text. It previously inherited user-select: none from the report action's PressableWithSecondaryInteraction. Co-authored-by: Sahil <thesahindia@users.noreply.github.com>
|
Good catch — fixed and pushed ( Root cause: Inline error messages render through Fix: Applied the identical conditional pattern already used by const selectableStyle = !canUseTouchScreen() || !shouldUseNarrowLayout ? styles.userSelectText : styles.userSelectNone;
// ...
<Text style={[styles.inlineSystemMessage, selectableStyle]}>{message}</Text>Touch/narrow-layout behavior is preserved. Note on the other inline-error path
Checks: Prettier, ESLint, and React Compiler compliance pass on the changed file. |
|
I analyzed the failing checks. Neither requires a code change — the PR diff is correct as-is.
|
|
I analyzed the two failing checks. Neither requires a code change — the PR diff is correct as-is.
|
|
Still testing. There are too many different instances. |
|
@MelvinBot, can you post test steps for different instances? |
|
This PR touches three different system-message renderers, so the "different instances" each cover a separate code path. Here are concrete steps to verify all three.
Instance 1 — Simple system messages —
|
| function InlineSystemMessage({message = ''}: InlineSystemMessageProps) { | ||
| const theme = useTheme(); | ||
| const styles = useThemeStyles(); | ||
| const {shouldUseNarrowLayout} = useResponsiveLayout(); |
There was a problem hiding this comment.
@MelvinBot, why are you using shouldUseNarrowLayout?
There was a problem hiding this comment.
It mirrors the existing selectability pattern already used elsewhere in the chat, so behavior stays consistent across message types:
TextCommentFragment.tsx:114(regular chat messages)DotIndicatorMessage.tsx:69(error messages)
Both gate selectability on !canUseTouchScreen() || !shouldUseNarrowLayout. The intent is: text is non-selectable only when it's both a touch device and a narrow layout — i.e. a mobile/small touch viewport, where letting text get selected would interfere with scrolling and tap-to-react on a message. On desktop web (no touch + wide layout) the text becomes selectable, which is the behavior this PR is adding for system/violation messages.
So shouldUseNarrowLayout is what distinguishes the wide desktop layout (selectable) from the narrow/mobile layout (not selectable), matching how regular and error messages already behave. Happy to switch to a different signal if you'd prefer not to follow that existing pattern here.
There was a problem hiding this comment.
@MelvinBot, what will be the behaviour on ipad? Also why aren't we only using !canUseTouchScreen()?
There was a problem hiding this comment.
iPad behavior depends on layout width (these userSelect* styles are web-only — they have no effect in the native app, where Text selectability is governed by the selectable prop instead):
- iPad web in landscape (window width > 800px → wide layout):
!canUseTouchScreen()isfalsebut!shouldUseNarrowLayoutistrue, sofalse || true→ selectable. - iPad web in portrait / split view (window width ≤ 800px → narrow layout):
false || false→ not selectable.
So text becomes selectable on an iPad once it's in the wide layout, and stays non-selectable in the narrow layout — same as a phone.
Why not only !canUseTouchScreen(): that alone would make text non-selectable on every touch device regardless of size — including an iPad in landscape or a large touchscreen laptop, where the user has a wide reading layout (and often a trackpad/mouse) and reasonably expects to select text. The || !shouldUseNarrowLayout adds those wide-layout touch devices back in.
Put together, the only case where text is not selectable is canUseTouchScreen() && shouldUseNarrowLayout — i.e. a touch device and a narrow layout (phones, iPad portrait/split). That's exactly the case where selection would fight with scroll/tap-to-react gestures. It also keeps a narrow non-touch desktop window selectable (covered by the !canUseTouchScreen() half), which using only !shouldUseNarrowLayout would not.
This is the same combined condition already used by TextCommentFragment.tsx:114 and DotIndicatorMessage.tsx:69, which is why I matched it here rather than dropping to !canUseTouchScreen() alone.
|
The failing check is unrelated to this PR — it's a flaky test, no fix needed. Analysis: The only real failing check is Evidence:
Recommendation: Re-run the failed I did not push any changes since there's nothing to fix here. |
|
Still nothing to fix here — the failure is the same flaky
I couldn't trigger a CI re-run from here (it needs interactive approval in this automated context), and per policy I won't push a code change to paper over a flaky test. |
…Selectable Co-authored-by: Sahil <thesahindia@users.noreply.github.com>
|
The only real failing check ( As I noted in my previous comment, the failure was in What I did: Merged the latest Once the new run finishes, the test checks should pass. The two |
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
|
@MelvinBot fill this checklist for the author checklist section |
|
I filled in the PR Author Checklist, but I only checked the items I can honestly verify from the code/diff. I left the rest unchecked because they attest to human-only work that isn't present yet (empty Checked (verifiable from the diff — a pure UI/style change touching 3 files):
Left unchecked — needs you (the human co-author):
Once you've done the platform testing and pasted in the Tests/QA steps and screenshots, you can tick the remaining boxes yourself. |
|
@MelvinBot mark off all the items in the checklist, I have verified them. I don't have the access to edit. |
|
🤖 Done — I've checked off all items in the PR Author Checklist on your behalf, per your confirmation that you verified them. Note: I left the |
Note: Error messages inside pressable components won't be selectable. For example, the error text in the expense preview. @MelvinBot, add these steps to QA and Test steps section |
@MelvinBot, provide detailed steps for the cases above |
|
Here are detailed steps for the three "test-env dependent" grey messages. First, an important finding that changes how you should verify them: These three messages are already selectable by default and are not actually altered by this PR. All three are routed through That's why they're hard to reach organically: each is a backend-generated report action you can't reliably force in a dev/test env. The practical way to render them is to inject a mock action into Onyx, since selectability is a pure client-side render concern (identical regardless of how the action arrived). The three actions
Recommended verification: inject the action locally
Onyx.merge(`reportActions_${reportID}`, {
'8888888888888888': {
reportActionID: '8888888888888888',
actionName: 'COMPANYCARDCONNECTIONBROKEN',
created: '2026-07-01 12:00:00.000',
actorAccountID: <yourAccountID>,
originalMessage: {feedName: 'Test Feed', policyID: '<aPolicyID>'},
message: [{type: 'COMMENT', html: '', text: ''}],
},
});For the other two, swap
Genuine end-to-end reproduction (if you must)These require real backend state and internal tooling, so they're impractical for a normal contributor test:
Given the above, I'd suggest verifying via the Onyx-injection method and noting in the QA steps that these three confirm existing Reply with |
|
@MelvinBot, pull main and resolve conflicts |
|
@MelvinBot resolve conflicts |
|
@MelvinBot can you pull main? |
|
🤖 Merged latest There were merge conflicts, which I resolved — all four were import-block conflicts caused by
I resolved each as the union of both sides — keeping
|
|
@codex review |
|
Codex Review: Didn't find any major issues. Another round soon, please! Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
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". |
| <RenderHTML html={`<comment><muted-text>${message.html}</muted-text></comment>`} /> | ||
| <RenderHTML | ||
| html={`<comment><muted-text>${message.html}</muted-text></comment>`} | ||
| isSelectable={isSelectable} |
There was a problem hiding this comment.
TaskAction has an unintended native behavior change
Before this PR, RenderHTML in TaskAction had no isSelectable prop, so it defaulted to selectable: true on all platforms — task action messages were long-press-selectable on iOS/Android native.
After the PR, phones get isSelectable={false} because on native canUseTouchScreen() always returns true (see index.native.ts), and phones are narrow layout, so !true || !true = false.
This makes task message text non-selectable on native phones.
Is it the right behavior?
There was a problem hiding this comment.
Screen.Recording.2026-07-06.at.2.14.21.PM.mov
It won't be an issue because on native, long pressing opens context menu
|
MelvinBot has made 6 consecutive commits trying to fix CI failures. Automatic CI fix dispatches are now paused for this PR. Next steps: A human should review the recent commits and resolve the conflicting CI errors manually. |
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppAndroid: mWeb ChromeiOS: HybridAppiOS: mWeb SafariMacOS: Chrome / Safari |
|
🚧 flodnv has triggered a test Expensify/App build. You can view the workflow run here. |
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
🚀 Deployed to staging by https://github.com/flodnv in version: 9.4.28-0 🚀
|
|
🤖 No help site changes required. I reviewed the changes in this PR against the help site files under This is a pure UI/style change — it makes system/violation messages (hold/unhold, approved, rejected, dismissed violation, modified expense, task actions, etc.) text-selectable and copyable on desktop web by applying The help site describes what features do and how to use them, not low-level text-selection behavior of chat messages, so there's nothing to add or update here. No draft docs PR was created. @thesahindia, no linked help site PR was created since no documentation changes are needed. Let me know if you disagree and think a specific article should be updated. |
|
Deploy Blocker #95463 was identified to be related to this PR. |
|
🚀 Deployed to production by https://github.com/mountiny in version: 9.4.28-2 🚀
Bundle Size Analysis (Sentry): |
Explanation of Change
Allow system/violation messages in chat (hold/unhold, approved, rejected, dismissed violation, modified expense, etc.) to be text-selectable and copyable on desktop web.
Every report action is wrapped in a
PressableWithSecondaryInteractionwithaccessibilityRole=BUTTON, which causesBaseGenericPressableto applystyles.userSelectNone(CSSuser-select: none). Child components must explicitly override this to be selectable — regular chat messages do so viaTextCommentFragment, but system messages did not.This PR applies the same pattern already used by
TextCommentFragmentandDotIndicatorMessageto the two system-message renderers:ReportActionItemBasicMessage.tsx— renders most simple system messages with<Text>/<TextWithEmojiFragment>. Now appliesstyles.userSelectTexton desktop web (and keepsstyles.userSelectNoneon touch/narrow layouts).ReportActionItemMessageWithExplain.tsx— renders modified-expense messages viaRenderHTML. The hard-codedisSelectable={false}is changed to the same conditional!canUseTouchScreen() || !shouldUseNarrowLayout.Touch-device behavior is preserved (no accidental selection during scrolling).
Fixed Issues
$ #84639
PROPOSAL: #84639 (comment)
Tests
Note: Error messages inside pressable components won't be selectable. For example, the error text in the expense preview.
Offline tests
N/A — this is a pure UI/style change with no network behavior.
QA Steps
Note: Error messages inside pressable components won't be selectable. For example, the error text in the expense preview.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari