Skip to content

fix(messages): keep text selection inside the message history - #3505

Open
joshfriend wants to merge 3 commits into
mainfrom
jfriend/bounded-text-selection
Open

joshfriend wants to merge 3 commits into
mainfrom
jfriend/bounded-text-selection

Conversation

@joshfriend

@joshfriend joshfriend commented Nov 13, 2026 •

Copy link
Copy Markdown

Dragging a selection in a channel or thread used to run straight out of the message history and pick up the sidebar, channel header, composer and the other pane, and the copy included reaction emoji, reply counts and "Jump to latest". A selection that starts in a message history now stops at that history's edges, the way Slack bounds it to the channel. Reactions, reply summaries and the jump button are no longer selectable; a copy still carries each reaction with its count (👍 2, 🎉 1) through a data-copy-text attribute on the reaction row, and a byline copies as Name 2:31 PM instead of Name2:31 PM (the copy serializer now reads a flex row's gap as a space).

The clamp is a selectionchange listener registered alongside the existing timeline copy handler in selection-copy.ts; selections in editors or outside message histories are untouched. Unit tests cover the clamp and the byline spacing, and a new Chromium and WebKit browser case in message-selection.spec.mjs drags from the last message into the sidebar and checks the selection and copied text; it fails on main and passes here.

Before (drag from the last message up into the sidebar):

Before

After:

After

Copied text after:

Fixture Reader 6:54 PM
I think it's the new integration suite. It runs serially right now.
3fafa73cd7 6:56 PM
That would explain it. Can we shard it the way we do unit tests?
👍 1

A drag that starts in a channel or thread history now stops at that
history's edges instead of selecting the sidebar, headers, composer and
the other pane. Reactions, reply summaries and Jump to latest are no
longer selectable, so copies carry only the conversation, and a byline
copies as "Name 2:31 PM" rather than "Name2:31 PM".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@joshfriend
joshfriend requested a review from a team as a code owner November 13, 2026 20:13
The reaction row stays unselectable, but a selection across it now
copies each reaction with how many people chose it, e.g. "👍 2, 🎉 1".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@joahg joahg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Commenting only, not approving: this is a UI behavior change and needs a design or core-team reviewer. No FOUNDATION-marked files are edited.

Needs attention before merge: CI has real failures that look caused by this change.

  • emoji.spec.mjs ("community picker uses keyboard, proxy thumbnails...") fails on chromium and webkit. It still expects the old copied text (Fixture ReaderJan 1, 1970..., no reaction line). It now receives :party: 1 and Fixture Reader Jan 1, 1970..., so the expectation needs updating.
  • composer-paste.spec.mjs ("a triple-clicked heading message pastes into the current line") fails on chromium: copied html is <div data-buzz-copy="timeline">Title</div> instead of ...<h2>Title</h2>.... The test comment says a native triple-click range ends past the reaction row. Making that row user-select: none plausibly changes where the browser ends that range, so the heading no longer serializes as a heading and pastes without ##. I did not reproduce it locally, so I can't confirm the cause. This is the one finding that is a real copy/paste regression rather than a stale expectation.
  • Other red jobs (webkit local-agent-activity, route-teardown) look unrelated, but a rerun would confirm.

Looked fine:

  • Listener cleanup is ref-counted and removes selectionchange together with copy. The clamp is idempotent: after setBaseAndExtent the next event returns at side === 0, so there is no loop.
  • Editors and selections whose anchor is outside a [data-message-scroller] are left alone, so composer and sidebar selection still work.
  • user-select: none does not change focus order or what screen readers announce. The reaction buttons, reply summaries and jump button stay focusable and operable, and data-copy-text on the group is not exposed to assistive tech.
  • Copying across rows still carries reaction counts, and the unit tests cover partial ranges.

Not reviewed or verified: keyboard selection (Shift+arrows at a history edge, Select All, triple-click on the last message in each engine), Firefox and Windows WebView behavior (the browser test only covers chromium and webkit), and the screenshots, which I could not judge visually.

Comment thread src/features/messages/selection-copy.ts Resolved
Comment thread src/features/messages/selection-copy.ts Resolved
With the reaction row unselectable, a native triple-click ends inside the
block's own text, so a heading copied as bare text. A selection holding
all of a block's text now serializes that block. Update the emoji copy
expectation for the spaced byline and reaction count line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@joshfriend

Copy link
Copy Markdown
Author

🤖 Addressed in c22ee1e: emoji.spec now expects the spaced byline and the :party: 1 reaction line, and the triple-click heading paste regression is fixed (see the inline reply). Both specs, plus message-selection and timeline-copy, pass locally in Chromium and WebKit. I didn't rerun the webkit local-agent-activity and route-teardown jobs; CI will show whether they're unrelated flakes.

@joahg joahg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Commenting only, not approving: this account isn't approving UI changes, and this changes selection and copy behavior, so it needs a design or core-team reviewer.

Fixed since my last review:

  • CI: every job on this head is green (browser chromium 9/9 and webkit 18/18, flake checks, measurements, CI required); only "Windows native validation" is skipped. The earlier webkit failures did not recur.
  • emoji.spec: expectation updated to the spaced byline plus the :party: 1 line.
  • Triple-clicked heading losing its <h2>: fixed by wholeBlock, which serializes the outermost block whose text the range holds entirely. The cause was the one suspected: with the reaction row unselectable, the native range ends inside the heading's own text. composer-paste.spec now passes.
  • Shift+arrow at the history edges: the author reports it stays clamped in Chromium and WebKit. I didn't re-test that in a browser.
  • Flex-gap byline spacing: the author's reply is right; the space is only added for a nonzero column-gap.

Still open:

  • P3 (new, from wholeBlock): it applies to any range that covers a block's whole text, not just a triple-click. I ran serializeSelection before and after this commit on a range covering exactly one block's text. A heading went from Title to ## Title (markdown) and <h2>Title</h2> (html). A one-word list item went from word to - word (markdown) and <ul><li>word</li></ul> (html). A paragraph is unchanged. So double-clicking a one-word heading or list item, or dragging exactly over its text, now copies block markup. This may be intended, but it is a visible change that no unit test pins.
  • Firefox and Windows WebView are still not covered by the browser suite, so the selection clamp isn't verified there.

None of the edited files is marked FOUNDATION.

/** The outermost block of a row whose text a range holds entirely. A
* triple-click ends inside a block's own text when only unselectable chrome
* follows it, and the block still copies as itself, a heading as a heading. */
function wholeBlock(element: Element | null, range: Range) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 This also fires when the range simply covers one block's full text, not only for a triple-click. I checked serializeSelection before and after this commit on a range over exactly one block's text. A heading goes from Title to ## Title (markdown), and a one-word <li> from word to - word. A paragraph is unchanged. Double-clicking a one-word heading or list item, or dragging exactly over its text, now carries block markup into the composer. If that isn't wanted, limit it to a range that also ends past the block, which is what a triple-click leaves. Either way, a unit case in selection-copy.test.tsx would pin the intent; this commit adds none.

This branch has not been deployed

No deployments
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.

2 participants