Skip to content

feat(chat): select several messages and forward each on its own - #6926

Open
ToteMeiSter wants to merge 4 commits into
masterfrom
feat/chat-multiselect-v2
Open

ToteMeiSter wants to merge 4 commits into
masterfrom
feat/chat-multiselect-v2

Conversation

@ToteMeiSter

Copy link
Copy Markdown
Contributor

Resolves #6925

What changes in the chat:

  • The bubble keeps its behaviour: a tap and a long press open the content or the message menu (the Automatic transition to the link along with the display of the message menu #5533 behaviour is kept). The bubble now takes the gesture itself, so the row around it never sees a gesture that started on the bubble.
  • Tap on the empty part of the row: message menu, as before.
  • Long press on the empty part of the row: starts a selection (MessageSelection in ChatViewModel). While it is active a tap on any row toggles it, the toolbar (ChatToolbar) shows the count with close, forward and copy, and back ends it.
  • Forward sends every selected message on its own, in chat order: the list of texts (planForward) goes to the conversation list and the chosen chat sends each text through the existing send queue (MessageInputViewModel.sendChatMessage). Only messages that the single-message menu would allow to forward are used; if some were left out (files, system messages) a toast says so. Copy reuses copyText with the texts joined by a new line.
  • Rows of selected text messages show the checkmark (ChatItem.isChecked, passed by ChatView); the forwarded messages are stored with the room token of the chat, so they are sent while the conversation is still loading, and a failed send shows an error toast. An empty forward plan (nothing forwardable) is reported with the same toast and does not leave the chat.
  • The row key of a temporary text message is made of its reference id, not of the clock in milliseconds (OfflineFirstChatRepository.createChatMessageEntity): messages added within one millisecond, as a forward of several does, replaced each other and the earlier one was never sent.
  • Not included: delete and forwarding of files (the file forward of the media viewer exists only in the fork's develop; master has no such flow).
  • gradle/verification-metadata.xml: checksums of espresso-core:3.5.0 and runner:1.5.0 (pom and aar) that the new testImplementation of ui-test-junit4 pulls in for the unit-test classpath.

Tested on: Huawei DEL-LX9, Samsung SM-S926B.

🏁 Checklist

  • ⛑️ Tests (unit and/or integration) are included or not needed
  • 🔖 Capability is checked or not needed
  • 🔙 Backport requests are created or not needed: /backport to stable-xx.x
  • 📅 Milestone is set
  • 🌸 PR title is meaningful (if it should be in the changelog: is it meaningful to users?)

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI

Commit trailer: Assisted-by: Claude-Code:claude-sonnet-5-5.

A long press on a chat row outside of the bubble starts a selection, a tap
on the row there still opens the message menu. While messages are selected
a tap anywhere on a row toggles it, the toolbar shows the number of selected
messages with close, forward and copy actions, and back ends the selection.
A tap and a long press on the bubble itself keep opening the message menu.

Each selected message is forwarded as its own message in the order of the
chat. Selected rows show their checkmark. Forwarded messages are sent while
the chat opens, before the conversation has been read from the database, so
the temporary message takes the room token of the chat instead of the
conversation, and the user is told when a send fails. Every temporary
message gets its own row key, so messages sent one after another do not
replace each other in the list.

Assisted-by: Claude-Code:claude-sonnet-5-5
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
@github-actions

github-actions Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

📱 QA build

Download app-qa-debug.apk
QR code Open the QR code for this download
Commit f66dc9e
Version 6926
Available until 7 days after this build

The QA build installs alongside a released Nextcloud app, so you can keep
using your existing install while testing.

Downloading the file requires a GitHub account, so open this link on the
device you want to test on, or transfer the APK to it.

@rapterjet2004 rapterjet2004 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, this is a nice feature, and keying temporary messages by referenceId instead of the millisecond fixes a real loss of messages. Reusing showForward per message, sorting by id and sending through the existing queue all look good, and the forward extras are removed after reading so a recreation doesn't send again.

Change requested: crash when a selected message is still being sent. isSelectable() doesn't exclude temporary messages (negative ids). Select a message while it is pending; when its server copy arrives, the temporary row is deleted but the selection keeps the negative id. The next selection change (the toolbar's produceState → loadSelectedMessages, ChatActivity.kt:2152), or tapping forward or copy (:3700, :3718), calls getMessageById(-x). OfflineFirstChatRepository.getMessage doesn't find it locally and calls getContextForChatMessage(-x), which throws an HttpException on the error status, and nothing catches it, so the app crashes. The same happens for a selected message removed by the expiry cleanup. Please exclude temporary messages in isSelectable(), drop ids from the selection when they disappear from the list, and catch failures in loadSelectedMessages.

Non-blocking:

  • Forwarded texts are the raw message, so mentions arrive as {mention-user1} placeholders. Same as the single-message forward today, so fine for this PR.
  • A selection can only start from the empty part of a row, and full-width bubbles leave little of it. A "Select" entry in the message menu would make the feature easier to find.
  • There's no limit on how many messages can be forwarded at once; each is its own send request.

…the selection

A pending message has a negative id until the server answers. When it was
selected, its row was replaced by the server copy, the selected id stayed,
and the next look-up of the selected messages asked the server for the
negative id. The request failed with an unhandled HttpException, which
crashed the chat when the toolbar, forward or copy read the selection.

Pending messages cannot be selected any more. Ids that are no longer in the
list of the chat, e.g. replaced pending messages or messages removed by the
expired cleanup, are dropped from the selection whenever the list changes.
A selected message that still cannot be loaded is left out and logged
instead of failing the whole selection.

Assisted-by: Claude-Code:claude-sonnet-5-5
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
Make the message menu start the existing selection. Limit it to 100 messages and 32,000 forwarded characters. Both row gestures and the menu use the same check and show a notice when a limit is reached.

Assisted-by: Claude-Code:claude-sonnet-5-5
Assisted-by: Codex:gpt-6.1-sol
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
@ToteMeiSter

Copy link
Copy Markdown
Contributor Author

Thanks for the review, the crash is real. I checked each step of the chain against the head of the PR (2f40e7d):

  • ChatMessageUi.isSelectable() (MessageRowFrame.kt:56) only excluded system and deleted messages, so a pending message (negative id) could be selected.
  • When the server copy arrives the pending row is removed, but its id stayed in MessageSelection.
  • loadSelectedMessages (ChatActivity.kt) called chatViewModel.getMessageById(-x).first(). OfflineFirstChatRepository.getMessage finds nothing in the database and calls fetchAndPersistSingleMessage, which calls network.getContextForChatMessage with the negative id. Nothing catches the HttpException on the way, so it reaches the toolbar's produceState, forward and copy.

Fixed in one commit (fix and test together):

  • (a) Pending messages cannot be selected any more: isSelectable() also requires a positive id (isSelectableMessageId).
  • (b) Whenever the list of the chat changes, MessageSelection.retainOnly drops the ids that are not in the list any more (replaced pending messages, messages removed by the expired cleanup). Dropping the last selected id ends the selection.
  • (c) loadSelected skips a message that fails to load (and logs it) instead of failing the whole selection. Cancellation is rethrown.
  • Test: MessageSelectionTest covers (a), (b), (c) and the cancellation. It failed before the change and passes after it. The unit tests also compile and pass on a merge with current master.

Non-blocking points:

  1. Mentions are forwarded as {mention-user1}: true, and it is the same as the single-message forward does today (message?.message). Fixing it needs the message parameters to travel with the forwarded text, which changes the single forward as well. I would do it in a separate PR, so this one stays about selection.
  2. "Select" entry in the message menu: done, commit dac44703f.
  3. Forwarding limits: done in dac44703f. Selection allows at most 100 messages and 32,000 forwarded text characters in total. The 100-message cap follows Telegram's published per-request forward limit; Talk's server caps one message at 32,000 characters. A selection that would exceed either cap is rejected with a specific notice. Removing a selection always works. The combined text cap keeps the string list passed through an Intent well below Android's roughly 1 MB Binder transaction limit. The cap is a client usability guard, not a server rate limit.

The PR is updated with two commits: 6c811ccf4 (the crash) and dac44703f ("Select" and the limits).

About "Select": you are right, selection starts only from the empty part of a row. A long press on the bubble opens the message menu (openMenuOnClick, MessageRowFrame.kt), and a full-width bubble leaves around 20 % of the row, less in one-to-one chats. The menu now has a "Select" entry (dac44703f, one commit on top of 6c811ccf4). It starts the same selection (MessageSelection.toggle) and is shown under the same condition as isSelectable(): not for system, deleted or pending messages. Both places use one function, isSelectableMessage, so they cannot drift apart. Three new strings (menu entry and two limit notices; translation via Transifex). The test for the condition (regular: shown; system, deleted, pending: hidden) failed before the change and passes after it; the unit tests also compile and pass on a merge with current master. Not checked on a device.

…reachable

The sheet used the default state and opened at the half-height peek;
with the longer menu the Delete entry stayed below the screen edge.

Assisted-by: Claude-Code:claude-sonnet-5-5
Signed-off-by: Krainov Gleb <krajnov.g@kontentplus.ru>
@ToteMeiSter

Copy link
Copy Markdown
Contributor Author

One more commit, f66dc9e9e: the new "Select" entry made the message menu longer, and on a phone the sheet opened at half height, so "Delete" stayed below the screen edge. The message menu now opens fully expanded (skipPartiallyExpanded = true in MessageActionsBottomSheet). The partialExpand() call on the way back from the emoji picker is removed, because it throws on such a sheet. A unit test checks the sheet state. Checked on a device (Android 14, 720×1610): all entries are reachable, emoji picker → back works.

@github-actions

Copy link
Copy Markdown
Contributor

Codacy

Lint

TypemasterPR
Warnings138138
Errors1515

SpotBugs

CategoryBaseNew
Bad practice77
Correctness88
Dodgy code4040
Internationalization33
Malicious code vulnerability33
Performance88
Security1111
Total8080

ToteMeiSter added a commit to ToteMeiSter/talk-android that referenced this pull request Oct 24, 2026
…elop

fix(chat): open the message menu fully expanded (port f66dc9e from nextcloud#6926)
@mahibi

mahibi commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator

Thank you for you contributions. We will try to review this soon!
If there are merge conflicts or other open points from review, please try to solve it or let us know which other PRs from you should be reviewed beforehand.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Select several chat messages with a long press beside the bubble (forward / copy)

3 participants