perf(chat): render reactions, deletions, edits and pinned messages without waiting for the server - #6694
Conversation
3c90e33 to
591b1a8
Compare
|
/backport to stable-25.0.x |
|
APK file: https://github.com/nextcloud/talk-android/actions/runs/34527290870/artifacts/10172455176 |
|
APK file: https://github.com/nextcloud/talk-android/actions/runs/34536421911/artifacts/10175823129 |
a17638a to
0ad7b87
Compare
|
APK file: https://github.com/nextcloud/talk-android/actions/runs/34566909089/artifacts/10186511874 |
8d847ec to
e16c78a
Compare
|
APK file: https://github.com/nextcloud/talk-android/actions/runs/34678988533/artifacts/10293456862 |
e16c78a to
b9621fd
Compare
|
APK file: https://github.com/nextcloud/talk-android/actions/runs/34940435207/artifacts/10385038818 |
e517620 to
35f88e8
Compare
A reaction was written to the local database only after the server had acknowledged it, so tapping an emoji rendered a full round trip later. The reaction is now applied locally before the request is sent. A request that fails for a transient reason is retried once, and only a request that finally fails reverts the local change. The revert is applied only while the local message still carries the optimistic change, so a server payload that arrived meanwhile through signaling or a chat sync stays authoritative. Adding a reaction now accepts HTTP 200 next to 201 and removing one accepts 404, so a reaction that is already in the desired state no longer counts as a failure. Operations on the same message and emoji are serialized, and a reaction that could not be applied is now reported to the user instead of only being logged. The message list rebuild waited behind a 200 ms debounce, which held back every isolated change - such as a reaction of one's own - for that whole window. It now uses a leading edge throttle that passes the first change on immediately and still collapses bursts into one rebuild per window. The reaction chips animate their count, their colors and their place in the row, so a change that lands instantly still reads as intentional. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Deleting a message asked the server and wrote nothing locally, so the bubble only turned into a deleted message once the corresponding system message came back through the chat sync - the round trip plus the delivery of that message. The message is now marked as deleted in the local database before the request goes out, and the deletion is reverted from a snapshot if the request finally fails. A transient failure is retried once, and HTTP 404 counts as success: the server no longer knows the message, so it is gone either way. The call moves from RxJava to coroutines on the way, which is what lets it reuse withRetry. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Editing a message sent the request and wrote nothing locally, and the server's answer was used for status codes only. The new text therefore appeared when the corresponding system message came back through the chat sync. The new text and the edit metadata are now written to the local database before the request goes out. A transient failure is retried once; a failing request or a server that refuses the edit, such as for a message that is too old, restores the previous version from a snapshot. The version the server confirms is persisted as the authoritative one - taken from the system message's parent, which is where the edit endpoint reports the edited message, the same way the chat sync reads it. Editing a temporary message already worked this way, through editTempChatMessage. Leaving edit mode also clears the input and lets the input buttons follow the input state again instead of turning all of them visible, so the save and send buttons no longer overlap, the scheduled messages button no longer appears unasked, and the edited text no longer stays behind in the input. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Pinning a message called the API, discarded the message it returned and then refreshed the whole room, so the pinned banner only appeared after two round trips. The pinned message is now written into the local conversation entry before the request goes out, guarded in the room list merge the way the pending favorite flags are, so a response computed before the change reached the server cannot revert it. A transient failure is retried once, and a request that finally fails restores the previous state. The room refresh stays in place: it re-asserts the server's state and reveals another pinned message after unpinning. The shared items screen pins and unpins without ever opening the chat, so the local write is skipped when the repository has no conversation data. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Dismissing the pinned banner called the API and then refetched the whole room, so the banner stayed on screen for two round trips - a tap that looks ignored. The conversation entry now hides the message first, guarded like a pin so a room list response computed before the request cannot bring the banner back, and the dismissal is undone if the request finally fails. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
a9d9f50 to
763e963
Compare
The backport of #6694 brought the IOException and HttpException catch clauses into ChatViewModel but not their imports, so stable-25.0.x has not compiled since. Add them back in the same place master keeps them. Assisted-by: Claude Code:claude-opus-5 Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Reactions, deletions, edits, pinning and dismissing the pinned banner only rendered once the server had acknowledged them, so every one of those taps cost a full round trip - for deletions and edits even the round trip plus the delivery of the system message that reports the change, and for the pinned banner two round trips - before anything moved on screen.
Each of them is now applied to the local database before the request goes out, and the chat renders from there. A request that fails for a transient reason (connection problem, rate limit, server error) is retried once; only a request that finally fails reverts the local change, and the revert is applied only while the local state still carries it, so a server payload that arrived meanwhile through signaling or a chat sync stays authoritative. A cancelled request - closing the chat mid-flight - reverts as well, instead of leaving the change behind forever. This follows the pattern the conversation list already uses for mark as read, favorites and archiving.
One commit per action:
Reaction*ViewStateLiveData had no observers left since the Compose chat rewrite) and now reach the user as a snackbar. Operations on the same message are serialized, so two quick taps cannot overwrite each other's local write. The chips animate their count, their colors and their place in the row, so the immediate change reads as intentional rather than as a jump.deleteChatMessageswrote nothing locally; the bubble only turned into a deleted message when theMESSAGE_DELETEDsystem message came back through the sync. The message is now marked deleted locally, including dropping its attachment parameters so a deleted image stops rendering as an image, and restored from a snapshot if the request fails. HTTP 404 counts as success. The call moves from RxJava to coroutines on the way, which is what lets it reusewithRetry.One change is not per-action: the message list rebuild ran behind a 200 ms
debounce, which held back every isolated change - each of the above included - for the full window. It now uses a leading edge throttle (Flow.throttleLatest): the first change is passed on immediately, a burst still collapses into one rebuild per window, so the reason the debounce existed is kept. Every other local edit and incoming message benefits from this too.Behaviour is meant to be exactly as before, only mimicked locally instead of awaited.
🖼️ Screenshots
None - the UI itself does not change, only when it updates.
🚧 TODO
🏁 Checklist
/backport to stable-xx.x🤖 AI (if applicable)