Skip to content

fix: keep paging chat history until a visible message is found - #6714

Merged
AndyScherzinger merged 4 commits into
masterfrom
fixMessageLoadingByTooManyReactions
Sep 21, 2026
Merged

AndyScherzinger merged 4 commits into
masterfrom
fixMessageLoadingByTooManyReactions

Conversation

@mahibi

@mahibi mahibi commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator
  • Fix Messages not shown in chat with many users #5775 -> Chat history could stop loading — a chat could look empty, or scrolling up for older messages could get permanently stuck.

    Root cause

    The server returns chat history as a flat sequence of entries, and not every
    entry is something the app actually draws as its own message bubble: system
    messages (reactions, thread-created markers, unpin notices, ...) and, outside
    of a thread view, other threads' replies are fetched and persisted like any
    other message, but the chat UI filters them back out before rendering. A
    single fetched page (default limit: 100 entries) has no guaranteed floor on
    how much of it is "real" content versus entries the UI will filter away —
    it's entirely possible for a page to be 100% filtered-out entries, with the
    next actually-visible message sitting just beyond it. The app treated such a
    page as "there is nothing (more) to show" and stopped, even though real
    messages existed right past the edge of that page.

    Concretely, this can happen several ways:

    • Reactions (the originally reported case): every add/remove of a
      reaction generates its own REACTION/REACTION_REVOKED/REACTION_DELETED
      entry. A message that collects many reactions — or has reactions
      toggled on/off repeatedly — can push 100+ non-visible entries in a row
      into the history, right after it.
    • Thread activity: a very active thread generates a burst of replies
      that are only shown while viewing that thread; from the main channel's
      perspective, a page can be 100% replies belonging to some other thread.
    • Other filtered system messages: a burst of thread-created or
      unpin/pin notices can have the same effect, even though none of them
      are reactions.

    Confirmed on a real device for the reactions case: the same anchor
    (161577) and range (161459..161577) were re-fetched on every scroll
    gesture with no progress, because the sync had no way to tell "this page has
    nothing to show" apart from "there's nothing left". This mirrors a problem
    iOS already solved (see the fix's inline reference to
    NCChatController.m):
    keep paging until a page actually contains something visible, instead of
    stopping at a fixed message count.

    Fix

    • ChatMessageSyncer.pullUntilVisibleMessage(): repeats the fetch, advancing
      the anchor deeper into history each round, until a round persists a message
      that will actually render, history is exhausted, or a bounded number of
      rounds (5) is spent. Wired into the initial "newest messages" fetch and into
      loadMoreMessages (scroll-up-for-older-history).
    • The first round re-fetches the anchor message itself (includeLastKnown),
      so a page is only accepted once it contains a newly visible message —
      otherwise a heavily-reacted anchor message satisfies "found something
      visible" immediately and the reaction spam right below it never gets paged
      through. Found via device testing, not just by inspection.
    • CHAT_HIDDEN_SYSTEM_MESSAGE_TYPES: a set dedicated to "does this render as
      its own chat bubble", separate from the pre-existing
      ConversationListUpdater.LAST_MESSAGE_HIDDEN_SYSTEM_TYPES (which answers a
      different question — "is this a valid conversation-preview text"). Reusing
      the preview set left a gap: THREAD_CREATED and MESSAGE_UNPINNED are
      valid preview texts but never get their own bubble, so a page made up
      entirely of those could reproduce the same stuck behavior.
    • isChatVisibleMessage(): also treats a real message as hidden when it's a
      reply belonging to some other thread than the one being synced (mirrors
      ChatViewModel.handleThreadMessages()'s main-channel filtering).

    Known follow-up (not addressed here)

    ChatViewModel also hides all system messages when a room is an
    announcement-preset "channel" (ConversationUtils.isChannel), not just the
    types in CHAT_HIDDEN_SYSTEM_MESSAGE_TYPES. ChatMessageSyncer has no way to
    evaluate that today — it only carries a bare SyncTarget
    (user/roomToken/threadId/credentials/url), not the ConversationModel or
    SpreedCapability that decision depends on, and it's intentionally usable
    without an open chat (background sync, catch-up jobs). Closing this would mean
    looking up the conversation + capabilities per visibility check, which is a
    real plumbing change for a narrow slice of rooms — left for a follow-up PR
    rather than folded in here.

    Testing

    • New unit tests in ChatMessageSyncerTest: paging through reaction-only
      pages, non-reaction hidden types, thread-reply-only pages (main channel vs.
      the thread itself), the anchor re-fetch edge case, exhausted history, and
      the round-budget give-up path.
    • Verified live on device: before the fix, scrolling repeatedly re-fetched the
      same range with no new messages; after the fix, a single scroll paged
      through the reaction wall and
      revealed the older messages.

🖼️ Screenshots

🏚️ Before 🏡 After
grafik grafik

🏁 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

@mahibi mahibi added this to the 25.1.0 milestone Sep 17, 2026
@mahibi mahibi self-assigned this Sep 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

APK file: https://github.com/nextcloud/talk-android/actions/runs/35254688511/artifacts/10511429758
To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud app.
qrcode (please click on link to get QR code displayed)

A page fetched from the server (or a single loadMore round) can be made
up entirely of messages that are never rendered as their own chat
bubble, most notably a burst of REACTION/REACTION_REVOKED/
REACTION_DELETED system messages from repeatedly adding and removing a
reaction. Accepting such an all-hidden page as "there is nothing (more)
to show" left the chat looking empty or stuck on older history, even
though real messages existed just beyond that page.

ChatMessageSyncer.pullUntilVisibleMessage() now repeats the fetch,
advancing the anchor deeper into history each round, until a round
persists a message that will actually be shown, history is exhausted,
or a bounded number of rounds is spent. Since the first round re-fetches
the anchor message itself (includeLastKnown), a page is only accepted
once it contains a visible message other than that already-known
anchor - otherwise a heavily-reacted anchor message would immediately
satisfy "found a visible message" and the wall of reaction messages
right below it would never be paged through.

This mirrors how the iOS client handles the same scenario (see
https://github.com/nextcloud/talk-ios/blob/2131b55b0b79247121f4ca1eaed8d09bb246bdbc/NextcloudTalk/Chat/NCChatController.m#L148-L168).

Fixes #5775

Assisted-by: Claude Code:claude-sonnet-5

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
ConversationListUpdater.LAST_MESSAGE_HIDDEN_SYSTEM_TYPES answers "can
this be a conversation's last-message preview text", which is not the
same question as "does this render as its own bubble in an open chat".
It is missing THREAD_CREATED and MESSAGE_UNPINNED: both are valid
preview texts but ChatViewModel.shouldRemoveMessage() still hides them
from the chat itself. Reusing the preview set in
pullUntilVisibleMessage() meant a page made up entirely of e.g.
THREAD_CREATED messages could fool the "found a visible message" check
the same way a page of reaction messages did.

Add CHAT_HIDDEN_SYSTEM_MESSAGE_TYPES, a set dedicated to that question
and mirroring ChatViewModel.shouldRemoveMessage()'s per-type checks
(kept unconditional, so it stays a safe superset of what is truly
hidden rather than risking a subset). SyncOutcome now derives
newestPersistedMessage (conversation preview, unchanged) and
visibleMessageIds (chat visibility) from two separate filters instead
of one.

Assisted-by: Claude Code:claude-sonnet-5

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
ChatViewModel.handleThreadMessages() hides thread replies (isThread &&
threadId != its own id) from the main channel view when not viewing
that specific thread. That filter needs isThread/threadId, not
systemMessageType, so it could not live in CHAT_HIDDEN_SYSTEM_MESSAGE_TYPES
- a very active thread could crowd a fetched page with replies and fool
pullUntilVisibleMessage the same way reaction or THREAD_CREATED spam did.

Add isChatVisibleMessage(), combining the existing hidden-type check
with a thread-child check gated on the synced SyncTarget.threadId being
null (main channel) vs. set (that thread's own replies are the visible
content there, matching ChatViewModel's conversationThreadId == null
guard).

Assisted-by: Claude Code:claude-sonnet-5

Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
@mahibi
mahibi force-pushed the fixMessageLoadingByTooManyReactions branch from 4b661ab to 99a999b Compare September 18, 2026 14:26
@github-actions

Copy link
Copy Markdown
Contributor

APK file: https://github.com/nextcloud/talk-android/actions/runs/35356290357/artifacts/10551779899
To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud app.
qrcode (please click on link to get QR code displayed)

@mahibi
mahibi marked this pull request as ready for review September 18, 2026 14:34
@mahibi mahibi added the 3. to review Waiting for reviews label Sep 18, 2026
5 rounds (500 messages at the default limit) was not enough to
reproduce a real-world reactions-spam scenario during testing. Raising
it to 25 is safe: pullUntilVisibleMessage() exits after round 1 for
virtually every page, so a higher cap costs nothing in the common
case - the extra budget is only ever spent in the exact pathological
case it exists to cover, where the previous alternative was getting
stuck forever (#5775).

Documented the reasoning with example calculations directly on the
constant: worst case is MAX_VISIBLE_MESSAGE_ROUNDS * limit messages
fetched and persisted before giving up (2,500 at the default limit of
100), roughly a few seconds up to ~10-12s of loading at a rough
300-500ms per round, paid once by the one chat that actually needs it.

Also noted, as a known-gap comment in ChatViewModel.loadMoreMessages(),
that this is a bounded guess and not an unbounded guarantee: if some
room's gap ever exceeds the cap, what happens next is governed by how
the caller advances its anchor for the next attempt (currently derived
from the UI's filtered message list, not the raw persisted range that
loadMoreMessages() already returns and discards) rather than by this
constant.

Assisted-by: Claude Code:claude-sonnet-5
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
@github-actions

Copy link
Copy Markdown
Contributor

APK file: https://github.com/nextcloud/talk-android/actions/runs/35360083926/artifacts/10553744462
To test this change/fix you can simply download above APK file and install and test it in parallel to your existing Nextcloud app.
qrcode (please click on link to get QR code displayed)

@AndyScherzinger
AndyScherzinger merged commit d08e9db into master Sep 21, 2026
16 of 19 checks passed
@AndyScherzinger
AndyScherzinger deleted the fixMessageLoadingByTooManyReactions branch September 21, 2026 09:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3. to review Waiting for reviews AI assisted

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Messages not shown in chat with many users

2 participants