Skip to content

Add message multi-select for bulk copy, forward and delete#6918

Open
manfrommedan wants to merge 1 commit into
element-hq:developfrom
manfrommedan:multiselect-messages
Open

Add message multi-select for bulk copy, forward and delete#6918
manfrommedan wants to merge 1 commit into
element-hq:developfrom
manfrommedan:multiselect-messages

Conversation

@manfrommedan
Copy link
Copy Markdown
Contributor

@manfrommedan manfrommedan commented May 30, 2026

Content

Adds an opt-in multi-select mode to the timeline, behind a new Labs flag
(Multi-select messages, off by default).

  • Long-press a message and drag up or down to sweep a range; the list
    auto-scrolls near the edges.
  • Or pick "Select" from the message action menu. This is the entry point for
    media, where a long-press already opens the menu.
  • While selecting, a top bar shows the count and offers bulk Copy, Forward and
    Delete. The selection is capped at 30 and survives rotation and process death.

Delete only appears when you have redaction rights and only redacts the messages
you are allowed to. Bulk redact and forward run through the active timeline, so
they work in threads and focused timelines too. With the flag off, nothing
changes.

Motivation and context

Relates to #6737. Selecting messages one at a time through the action sheet is
slow and breaks the reading flow; this keeps the timeline stable and lets you
act on many or non-adjacent messages at once.

Screenshots / GIFs

@PreviewsDayNight previews cover the selection top bar, the selection
indicator and a timeline in selection mode, so the screenshot tests pick them
up in light and dark.

Tests

Unit tests cover enter/toggle/clear, the 30 cap, bulk copy and forward ordering,
bulk delete (success and partial failure), the "Select" menu intercept, the
selectable-content filter, the drag-select registry, and the forward presenter
with multiple events.

Tested devices

  • Physical
  • Emulator
  • OS version(s): Android 12, Android 16

Checklist

  • Changes are based on the develop branch.
  • The PR title describes the user-facing change (for the release notes).
  • I added unit tests.
  • Tested on a device in light and dark themes.
  • Accessibility considered (Checkbox role and state on selectable rows).
  • Self-reviewed the PR.

Note for the reviewer: the new English strings are in
features/messages/impl/src/main/res/values/temporary.xml. Please import them
into Localazy and I will rebase onto the generated keys.

@github-actions
Copy link
Copy Markdown
Contributor

Thank you for your contribution! Here are a few things to check in the PR to ensure it's reviewed as quickly as possible:

  • If your pull request adds a feature or modifies the UI, this should have an equivalent pull request in the Element X iOS repo unless it only affects an Android-only behaviour or is behind a disabled feature flag, since we need parity in both clients to consider a feature done. It will also need to be approved by our product and design teams before being merged, so it's usually a good idea to discuss the changes in a Github issue first and then start working on them once the approach has been validated.
  • Your branch should be based on origin/develop, at least when it was created.
  • The title of the PR will be used for release notes, so it needs to describe the change visible to the user.
  • The test pass locally running ./gradlew test.
  • The code quality check suite pass locally running ./gradlew runQualityChecks.
  • If you modified anything related to the UI, including previews, you'll have to run the Record screenshots GH action in your forked repo: that will generate compatible new screenshots. However, given Github Actions limitations, it will prevent the CI from running temporarily, until you upload a new commit after that one. To do so, just pull the latest changes and push an empty commit.

@github-actions github-actions Bot added the Z-Community-PR Issue is solved by a community member's PR label May 30, 2026
@manfrommedan manfrommedan marked this pull request as ready for review May 30, 2026 20:51
@manfrommedan manfrommedan requested a review from a team as a code owner May 30, 2026 20:51
@manfrommedan manfrommedan requested review from bmarty and removed request for a team May 30, 2026 20:51
@manfrommedan manfrommedan force-pushed the multiselect-messages branch from c3fdea1 to 6351e84 Compare May 30, 2026 22:14
Adds an optional message multi-selection mode behind a new Labs flag
(MessageMultiSelect, off by default). Long-press a message to start
selecting and drag up or down to sweep a whole range; the list
auto-scrolls at the edges. A "Select" action in the message menu also
enters the mode, which is the entry point for media (long-press there
opens the menu rather than entering selection directly).

While selecting, a top bar shows the count and offers bulk copy,
forward and delete (capped at 30 messages). Delete is always shown but
disabled when the selection contains a message the user cannot redact
(or when the user has no redact rights at all); it only redacts the
events they are permitted to.
Bulk redact runs through the active timeline so it works in threaded and
focused timelines. Each selectable row is a single Checkbox-role toggle
for TalkBack. Count strings use plurals.

Selection state survives configuration changes and process death via a
Saver. Forwarding is generalised to accept a list of events; single-event
callers wrap their id in a singleton list, so existing behaviour is
unchanged.

English strings are added to a temporary.xml for the core team to import
into Localazy.
@manfrommedan manfrommedan force-pushed the multiselect-messages branch from 6351e84 to 5ff59c1 Compare May 31, 2026 08:49
@manfrommedan
Copy link
Copy Markdown
Contributor Author

Small follow-up: while testing this on a device I found a bug in the selection handling and folded the fix into this PR.

Repro: enter selection on a media message, scroll it slightly so it leaves the screen and comes back, cancel the selection, then tap the message. It would toggle selection again instead of opening the viewer. It was intermittent, and scrolling far enough that the row gets disposed and recreated made it go away.

Cause: the tap/long-press handlers read selectionState.isActive from the state snapshot captured when the row last composed. A LazyColumn row that is retained off-screen across the clear is not recomposed, so it kept a stale isActive=true and toggled on the next tap. The fix reads the flag through a single rememberUpdatedState value, so a handler captured by a stale row still observes the current state.

I did not add a unit test for it: the handler logic in isolation is correct, and the bug only appears from LazyColumn retention combined with a recomposition and input timing window. A Compose UI test recomposes deterministically (waitForIdle), so it would pass on both the broken and the fixed code and give false confidence rather than catch the regression. I verified it by hand on-device. Better to have caught it here than after a release.

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

Labels

Z-Community-PR Issue is solved by a community member's PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant