Skip to content

fix(ui): auto-scroll on optimistic local messages#2683

Merged
xsahil03x merged 1 commit into
masterfrom
fix/auto-scroll-on-local-message
May 27, 2026
Merged

fix(ui): auto-scroll on optimistic local messages#2683
xsahil03x merged 1 commit into
masterfrom
fix/auto-scroll-on-local-message

Conversation

@xsahil03x
Copy link
Copy Markdown
Member

@xsahil03x xsahil03x commented May 25, 2026

Closes FLU-499 (QA item #97 from Flutter QA — New Design).

Summary

  • StreamMessageListView's auto-scroll-to-bottom listener now subscribes to channel.state.newMessageStream (or newThreadMessageStream(parentId) in thread mode), exposed via a new NewMessageStreamX extension on ChannelClientState (in mlv_utils.dart, not exported). Diffs messagesStream / threadsStream for "a new message landed at the bottom" — covering both server-confirmed message.new events AND optimistic local sends. The previous channel.on(EventType.messageNew) path missed local sends because no WS event fires for them.
  • Gated on isUpToDate: no auto-scroll while the channel is loaded around a historic message. The false → true flip is a wholesale replacement and is also suppressed.
  • Detection uses (newLast.id != lastSeen.id) && newLast.createdAt.isAfter(lastSeen.createdAt) — handles tail deletion, optimistic→server confirm (same id, no double-scroll), and pruning + append in the same emission.

Test plan

  • flutter test test/src/message_list_view/auto_scroll_test.dart — 7/7 pass (new file).
  • flutter test test/src/message_list_view/ — full MLV suite passes.
  • flutter analyze lib/src/message_list_view/ — clean.
  • Manual on Pixel 4 (profile mode): send while at bottom, send while scrolled up, receive while at bottom, receive while scrolled up.
  • Manual: send → server confirm doesn't double-scroll.
  • Manual: send → fails → tap retry — no jump, message stays in place.
  • Manual: open a thread, send in thread — thread auto-scrolls.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Fixed auto-scroll behavior when sending outgoing messages before server confirmation is received, ensuring the message list stays positioned at the latest message.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 25, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • ✅ Review completed - (🔄 Check again to review again)
📝 Walkthrough

Walkthrough

StreamMessageListView now subscribes to derived ChannelClientState streams that emit only when the message list bottom grows (count increases + bottom ID changes). didChangeDependencies wires the appropriate stream, preserving the in-progress-scroll guard and own-message vs other-message auto-scroll rules. Tests and a changelog entry are added.

Changes

Auto-scroll stream-based detection

Layer / File(s) Summary
Stream-based auto-scroll mechanism
packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart
Adds a typed StreamSubscription<Message>?, selects newThreadMessageStream(parentId) or newMessageStream in didChangeDependencies, and installs existing scroll gating. Consolidates many UI formatting/refactor changes and adds a private ChannelClientState extension that yields only when the message list length increases and the bottom message ID changes (channel stream also gates on isUpToDate).
Auto-scroll test suite
packages/stream_chat_flutter/test/src/message_list_view/auto_scroll_test.dart
Adds a widget test harness with mock StreamChatClient/Channel/ChannelClientState, helpers to seed and append messages, and five tests covering other-user at bottom, other-user while scrolled-up, own message while scrolled-up, optimistic local send, and rapid burst while at bottom.
Changelog entry
packages/stream_chat_flutter/CHANGELOG.md
Adds an "Upcoming Changes" section documenting the auto-scroll bug fix for the user's own outgoing message prior to server confirmation.

Sequence Diagram

sequenceDiagram
    participant View as StreamMessageListView
    participant ChannelState as ChannelClientState
    participant Stream as messagesStream/threadsStream
    participant Scroll as ScrollController
    
    View->>ChannelState: subscribe to newMessageStream/newThreadMessageStream
    ChannelState->>Stream: new messages list arrives
    Stream->>View: onData(updatedMessageList)
    View->>View: compare list length & last message ID
    alt List length increased AND last ID changed
        alt Own message
            View->>Scroll: auto-scroll to bottom
        else Other message AND at bottom
            View->>Scroll: auto-scroll to bottom
        else Other message AND scrolled up
            View->>View: skip scroll (FAB visible)
        end
    else No length change or ID unchanged
        View->>View: skip scroll (edit/reaction/delete)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • VelikovPetar
  • renefloor

Poem

🐰 I hopped along the message trail,

Saw bottoms grow where new texts sail.
My own quick hop needn't wait,
The list finds end — it lands my state.
Tests clap softly — scrolls elate.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(ui): auto-scroll on optimistic local messages' directly and concisely describes the primary change: fixing auto-scroll behavior for optimistic local messages before server confirmation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/auto-scroll-on-local-message

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/stream_chat_flutter/test/src/message_list_view/auto_scroll_test.dart (1)

118-118: 💤 Low value

Minor inconsistency: .reversed.toList() differs from other test files.

Other tests like mark_read_test.dart use generateConversation(...) without reversing. While this doesn't break the tests (new message detection works regardless of initial ordering), consistency with the existing test patterns would improve maintainability.

♻️ Suggested change for consistency
-        final messages = generateConversation(20, users: [other]).reversed.toList();
+        final messages = generateConversation(20, users: [other]);

Apply similar changes to lines 140, 165, 194, and 217.

Also applies to: 140-140, 165-165, 194-194, 217-217

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/stream_chat_flutter/test/src/message_list_view/auto_scroll_test.dart`
at line 118, The test creates the messages list by calling
generateConversation(...).reversed.toList(), which is inconsistent with other
tests; remove the .reversed.toList() so messages is assigned directly from
generateConversation(20, users: [other]) (and do the same for the other
occurrences where messages is built from generateConversation(...)). Locate the
assignments to the messages variable that use generateConversation and strip the
.reversed.toList() call (occurrences near the current ones using
generateConversation(..., users: [other])) to match the pattern used in
mark_read_test.dart.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@packages/stream_chat_flutter/test/src/message_list_view/auto_scroll_test.dart`:
- Line 118: The test creates the messages list by calling
generateConversation(...).reversed.toList(), which is inconsistent with other
tests; remove the .reversed.toList() so messages is assigned directly from
generateConversation(20, users: [other]) (and do the same for the other
occurrences where messages is built from generateConversation(...)). Locate the
assignments to the messages variable that use generateConversation and strip the
.reversed.toList() call (occurrences near the current ones using
generateConversation(..., users: [other])) to match the pattern used in
mark_read_test.dart.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5fa72ed1-9719-48d3-87f8-9d653e24a440

📥 Commits

Reviewing files that changed from the base of the PR and between 508c019 and f27b413.

📒 Files selected for processing (3)
  • packages/stream_chat_flutter/CHANGELOG.md
  • packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart
  • packages/stream_chat_flutter/test/src/message_list_view/auto_scroll_test.dart

@codecov
Copy link
Copy Markdown

codecov Bot commented May 25, 2026

Codecov Report

❌ Patch coverage is 68.00000% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.38%. Comparing base (8b7b890) to head (2aba070).

Files with missing lines Patch % Lines
...t_flutter/lib/src/message_list_view/mlv_utils.dart 58.82% 7 Missing ⚠️
...r/lib/src/message_list_view/message_list_view.dart 87.50% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2683      +/-   ##
==========================================
+ Coverage   65.33%   65.38%   +0.05%     
==========================================
  Files         423      423              
  Lines       26648    26665      +17     
==========================================
+ Hits        17410    17436      +26     
+ Misses       9238     9229       -9     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@xsahil03x xsahil03x force-pushed the fix/auto-scroll-on-local-message branch from f27b413 to 0fd46bd Compare May 26, 2026 15:25
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart (1)

441-462: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Rebind the listener when parentMessage changes.

Lines 441-462 choose the source stream from widget.parentMessage?.id, but the subscription is only recreated when StreamChannel.of(context) changes. If the same StreamMessageListView state is rebuilt with a different thread parent on the same channel, auto-scroll will stay wired to the previous stream.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart`
around lines 441 - 462, When rebinding the new-message subscription, also detect
changes to widget.parentMessage (compare previousWidget.parentMessage?.id to
widget.parentMessage?.id) and, if different, cancel the existing
_messageNewListener and recreate it exactly like you do when streamChannel
changes: compute newMessageStream from widget.parentMessage?.id (using
streamChannel!.channel.state?.newThreadMessageStream(parentId) or
.newMessageStream) and call newMessageStream?.listen(...) to assign
_messageNewListener; ensure debouncedMarkRead/MarkThreadRead and
_userReadListener handling remains unchanged.
🧹 Nitpick comments (1)
packages/stream_chat_flutter/test/src/message_list_view/auto_scroll_test.dart (1)

63-84: ⚡ Quick win

Add coverage for the new thread and re-seed branches.

This harness only renders const StreamMessageListView() with isUpToDate = true, so the new newThreadMessageStream(parentId) path and the isUpToDate == false -> true re-seed logic never run. A small test for thread mode and one for the historic-load transition would catch the easiest regressions in this PR.

Also applies to: 113-239

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/stream_chat_flutter/test/src/message_list_view/auto_scroll_test.dart`
around lines 63 - 84, The test helper pumpMessageList currently always renders
const StreamMessageListView with isUpToDate=true so
newThreadMessageStream(parentId) and the re-seed path when
channelClientState.isUpToDate transitions false->true are never exercised; add
two tests that call pumpMessageList with (1) a thread mode setup: mock
channelClientState.newThreadMessageStream(parentId) to return a stream of thread
messages and render StreamMessageListView in thread mode (e.g., pass parentId or
the API that enables thread view) to assert thread messages are shown, and (2) a
historic-load transition: initially pumpMessageList with isUpToDate=false and
some historic messages, then update the mock to isUpToDate=true and inject new
messages (or call the widget update path) to verify the re-seed logic runs
(e.g., by asserting the message list updates); use the existing pumpMessageList,
channelClientState, and StreamMessageListView symbols to locate where to extend
tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart`:
- Around line 441-462: When rebinding the new-message subscription, also detect
changes to widget.parentMessage (compare previousWidget.parentMessage?.id to
widget.parentMessage?.id) and, if different, cancel the existing
_messageNewListener and recreate it exactly like you do when streamChannel
changes: compute newMessageStream from widget.parentMessage?.id (using
streamChannel!.channel.state?.newThreadMessageStream(parentId) or
.newMessageStream) and call newMessageStream?.listen(...) to assign
_messageNewListener; ensure debouncedMarkRead/MarkThreadRead and
_userReadListener handling remains unchanged.

---

Nitpick comments:
In
`@packages/stream_chat_flutter/test/src/message_list_view/auto_scroll_test.dart`:
- Around line 63-84: The test helper pumpMessageList currently always renders
const StreamMessageListView with isUpToDate=true so
newThreadMessageStream(parentId) and the re-seed path when
channelClientState.isUpToDate transitions false->true are never exercised; add
two tests that call pumpMessageList with (1) a thread mode setup: mock
channelClientState.newThreadMessageStream(parentId) to return a stream of thread
messages and render StreamMessageListView in thread mode (e.g., pass parentId or
the API that enables thread view) to assert thread messages are shown, and (2) a
historic-load transition: initially pumpMessageList with isUpToDate=false and
some historic messages, then update the mock to isUpToDate=true and inject new
messages (or call the widget update path) to verify the re-seed logic runs
(e.g., by asserting the message list updates); use the existing pumpMessageList,
channelClientState, and StreamMessageListView symbols to locate where to extend
tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b1437629-0015-4f11-9f23-36fd9e66055b

📥 Commits

Reviewing files that changed from the base of the PR and between f27b413 and 0fd46bd.

📒 Files selected for processing (3)
  • packages/stream_chat_flutter/CHANGELOG.md
  • packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart
  • packages/stream_chat_flutter/test/src/message_list_view/auto_scroll_test.dart
✅ Files skipped from review due to trivial changes (1)
  • packages/stream_chat_flutter/CHANGELOG.md

@xsahil03x xsahil03x force-pushed the fix/auto-scroll-on-local-message branch from 0fd46bd to 66b53cc Compare May 26, 2026 15:38
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/stream_chat_flutter/test/src/message_list_view/auto_scroll_test.dart (1)

113-239: ⚡ Quick win

Add one thread-mode auto-scroll test.

These tests only exercise the channel path via messagesStream, but Line 457 switches to newThreadMessageStream(parentId) when parentMessage is set. A small case that mounts StreamMessageListView(parentMessage: ...) and drives threadsStream would cover the new branch directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/stream_chat_flutter/test/src/message_list_view/auto_scroll_test.dart`
around lines 113 - 239, Add a new widget test that mounts StreamMessageListView
with parentMessage set to exercise the thread branch: create a parent Message,
call pumpMessageList(parentMessage: parent) (or the test helper that mounts
StreamMessageListView with parentMessage), then drive the threads stream by
delivering a new thread message (use deliverNewMessage with newMessage.user and
existing list and ensure the helper routes to
threadsStream/newThreadMessageStream), and assert auto-scroll behavior (e.g.,
find.text(newThread.text!) and FloatingActionButton presence/absence) so the
code path that calls newThreadMessageStream(parentId) is covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart`:
- Around line 1530-1559: The newMessageStream currently ignores emissions when
the list length stays the same, but trimming old head items can remove earlier
messages while a new bottom message is appended so length doesn't grow; update
the logic in Stream<Message> get newMessageStream to treat a changed last
message as a new message even when emitted.length == lastLength by checking
newLast.id != lastSeen?.id and yielding newLast in that case (i.e. don't require
lengthGrew to be true to yield if lastChanged is true), and ensure lastSeen and
lastLength are updated after handling this condition; refer to the
newMessageStream getter, variables wasUpToDate, lastSeen, lastLength, emitted,
newLast, lengthGrew and lastChanged to locate and implement the change.

---

Nitpick comments:
In
`@packages/stream_chat_flutter/test/src/message_list_view/auto_scroll_test.dart`:
- Around line 113-239: Add a new widget test that mounts StreamMessageListView
with parentMessage set to exercise the thread branch: create a parent Message,
call pumpMessageList(parentMessage: parent) (or the test helper that mounts
StreamMessageListView with parentMessage), then drive the threads stream by
delivering a new thread message (use deliverNewMessage with newMessage.user and
existing list and ensure the helper routes to
threadsStream/newThreadMessageStream), and assert auto-scroll behavior (e.g.,
find.text(newThread.text!) and FloatingActionButton presence/absence) so the
code path that calls newThreadMessageStream(parentId) is covered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 84c7790f-db4b-4a74-b923-d6c772596250

📥 Commits

Reviewing files that changed from the base of the PR and between 0fd46bd and 66b53cc.

📒 Files selected for processing (3)
  • packages/stream_chat_flutter/CHANGELOG.md
  • packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart
  • packages/stream_chat_flutter/test/src/message_list_view/auto_scroll_test.dart
✅ Files skipped from review due to trivial changes (1)
  • packages/stream_chat_flutter/CHANGELOG.md

Comment thread packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart Outdated
@xsahil03x xsahil03x force-pushed the fix/auto-scroll-on-local-message branch 2 times, most recently from 7b1dbbc to 68b0611 Compare May 26, 2026 15:52
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart (1)

437-457: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Resubscribe when parentMessage changes.

This listener is only rebuilt when the inherited StreamChannel changes. Rebuilding the same StreamMessageListView with a different parentMessage keeps _messageNewListener attached to the previous stream, so thread auto-scroll follows the wrong conversation until the widget is remounted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart`
around lines 437 - 457, The code only rebuilds _messageNewListener when the
inherited StreamChannel changes, so changing widget.parentMessage leaves the
listener on the old stream; update the logic that recreates the listener (where
streamChannel, debouncedMarkRead/ThreadRead, _messageNewListener,
_userReadListener, and _unreadState are reset) to also run when
widget.parentMessage?.id changes (compare previous parent id to current), and
then re-subscribe by creating the correct newMessageStream using the current
widget.parentMessage (as done with switch on widget.parentMessage?.id) and
assigning _messageNewListener to its listener so thread auto-scroll follows the
current parent thread.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart`:
- Around line 437-457: The code only rebuilds _messageNewListener when the
inherited StreamChannel changes, so changing widget.parentMessage leaves the
listener on the old stream; update the logic that recreates the listener (where
streamChannel, debouncedMarkRead/ThreadRead, _messageNewListener,
_userReadListener, and _unreadState are reset) to also run when
widget.parentMessage?.id changes (compare previous parent id to current), and
then re-subscribe by creating the correct newMessageStream using the current
widget.parentMessage (as done with switch on widget.parentMessage?.id) and
assigning _messageNewListener to its listener so thread auto-scroll follows the
current parent thread.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 18805226-f41e-4856-9373-c04dee582a62

📥 Commits

Reviewing files that changed from the base of the PR and between 7b1dbbc and 68b0611.

📒 Files selected for processing (3)
  • packages/stream_chat_flutter/CHANGELOG.md
  • packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart
  • packages/stream_chat_flutter/test/src/message_list_view/auto_scroll_test.dart
✅ Files skipped from review due to trivial changes (1)
  • packages/stream_chat_flutter/CHANGELOG.md

@xsahil03x xsahil03x force-pushed the fix/auto-scroll-on-local-message branch 4 times, most recently from 181c0b6 to d442be8 Compare May 26, 2026 16:21
@xsahil03x
Copy link
Copy Markdown
Member Author

Triage of CodeRabbit findings:

  1. lengthGrew && lastChanged drops events on append-with-trim (inline) — ✅ fixed. The gate is now lastChanged && newLast.createdAt.isAfter(lastSeen.createdAt), so retention pruning + append in the same emission still fires (length stays flat but the tail id changes to a newer createdAt). CodeRabbit already auto-marked this as addressed.

  2. Rebind listener when parentMessage changes (flagged across two reviews) — ✅ fixed. Extracted _subscribeMessageNewListener() and added a didUpdateWidget that re-runs it when oldWidget.parentMessage?.id != widget.parentMessage?.id. The previous code only rebound on StreamChannel change, so a thread parent switch on the same channel would leak the old subscription.

  3. .reversed.toList() inconsistent with mark_read_test.dart — declining. The reverse is load-bearing here, not stylistic. generateConversation(...) returns newest-first, but production state.messages is sorted oldest-first (channel.dart:3619_sortByCreatedAt: a.createdAt.compareTo(b.createdAt) is ascending). The auto-scroll listener uses messages.last as "newest"; without the reverse it would be "oldest" and the tests would fire on the wrong message. mark_read_test.dart doesn't reverse because its assertions don't depend on the bottom-most-message identity — it only checks that channel.markRead() was called. Different test contracts.

@xsahil03x xsahil03x force-pushed the fix/auto-scroll-on-local-message branch 14 times, most recently from a483cc4 to 6239d71 Compare May 27, 2026 11:29
Subscribe to `channel.state.messagesStream` (or `threadsStream[parentId]`
in thread mode) instead of `channel.on(EventType.messageNew)` so the list
follows to the new bottom message the moment it lands in state. The event
path only fires on server-confirmed messages, which meant the user's own
send wasn't auto-scrolled until the server round-trip completed.

The data-source-driven pattern matches what the Android, iOS, and React
Native SDKs do. New-message detection uses a `lengthGrew && lastChanged`
check between emissions; the bottom-most snapshot is seeded from current
state on subscribe so we don't auto-scroll on the BehaviorSubject replay.
The synchronous `controller.scrollTo(index: 0)` call still clears SPL's
anchor key before `didUpdateWidget` (no race).

Adds an `auto_scroll_test.dart` covering: other-user-at-bottom,
other-user-scrolled-up, own-message-scrolled-up, optimistic local send,
and rapid burst.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@xsahil03x xsahil03x force-pushed the fix/auto-scroll-on-local-message branch from 6239d71 to 2aba070 Compare May 27, 2026 11:30
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart (1)

441-483: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Rebind message listener when thread parent changes on the same channel.

At Line 441, listener setup only runs when newStreamChannel != streamChannel. If only parentMessage?.id changes, the subscription is left on the old stream.

Suggested fix
+  `@override`
+  void didUpdateWidget(covariant StreamMessageListView oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (oldWidget.parentMessage?.id != widget.parentMessage?.id) {
+      _subscribeMessageNewListener();
+    }
+  }

   `@override`
   void didChangeDependencies() {
     super.didChangeDependencies();
     final newStreamChannel = StreamChannel.of(context);
     _streamTheme = StreamChatTheme.of(context);

     if (newStreamChannel != streamChannel) {
       streamChannel = newStreamChannel;
@@
-      final state = streamChannel?.channel.state;
-      final newMessageStream = switch (widget.parentMessage?.id) {
-        final parentId? => state?.newThreadMessageStream(parentId),
-        _ => state?.newMessageStream,
-      };
-
-      _messageNewListener?.cancel();
-      _messageNewListener = newMessageStream?.listen((message) {
+      _subscribeMessageNewListener();
+
+      final state = streamChannel?.channel.state;
+      _userReadListener?.cancel();
+      _userReadListener = state?.currentUserReadStream.listen((_) {
+        _unreadState.value = _readUnreadSnapshot();
+      });
+    }
+  }
+
+  void _subscribeMessageNewListener() {
+    final state = streamChannel?.channel.state;
+    final newMessageStream = switch (widget.parentMessage?.id) {
+      final parentId? => state?.newThreadMessageStream(parentId),
+      _ => state?.newMessageStream,
+    };
+
+    _messageNewListener?.cancel();
+    _messageNewListener = newMessageStream?.listen((message) {
         // existing body unchanged
-      });
-
-      _userReadListener?.cancel();
-      _userReadListener = state?.currentUserReadStream.listen((_) {
-        _unreadState.value = _readUnreadSnapshot();
-      });
-    }
+    });
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart`
around lines 441 - 483, The listener for new messages is only reinitialized when
newStreamChannel != streamChannel, so changes to widget.parentMessage?.id on the
same streamChannel leave _messageNewListener subscribed to the wrong stream;
update the logic in the block that currently checks `if (newStreamChannel !=
streamChannel)` to also detect parent-thread changes (compare previous
widget.parentMessage?.id to current widget.parentMessage?.id or always recreate
the newMessageStream subscription) and then cancel and recreate
_messageNewListener with the appropriate newMessageStream; ensure you still
cancel debouncedMarkRead/debouncedMarkThreadRead and update _unreadState via
_readUnreadSnapshot, and reuse the existing variables (_messageNewListener,
newMessageStream, streamChannel) so other cleanup (e.g., _userReadListener)
remains correct.
🧹 Nitpick comments (1)
packages/stream_chat_flutter/test/src/message_list_view/auto_scroll_test.dart (1)

122-350: ⚡ Quick win

Add thread-mode cases to pin the new thread listener path.

Current suite validates main-list behavior well, but it doesn’t exercise parentMessage mode (newThreadMessageStream) or parent-id switching on the same channel. Add at least one thread send case and one parent-switch rebind case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/stream_chat_flutter/test/src/message_list_view/auto_scroll_test.dart`
around lines 122 - 350, Add tests that exercise the thread-mode listener path:
create a test that pumps the message list in thread mode (using parentMessage /
parent id) and then deliver a new threaded message via the
newThreadMessageStream so the widget reacts the same as the main-list case (use
pumpMessageList, deliverNewMessage, messagesController and stub
channelClientState.messages as in other tests, and assert auto-scroll / FAB
behavior). Also add a test that switches the parent id on the same channel
(rebind case): pump the widget with one parentMessage, then change the
parentMessage to a different id, update
channelClientState/newThreadMessageStream accordingly, push messagesController
events and assert the listener was torn down/rebound correctly (no exceptions
and correct rendering), referencing StreamMessageListView, messagesController,
channelClientState.messages, pumpMessageList, and deliverNewMessage to locate
where to add these cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In
`@packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart`:
- Around line 441-483: The listener for new messages is only reinitialized when
newStreamChannel != streamChannel, so changes to widget.parentMessage?.id on the
same streamChannel leave _messageNewListener subscribed to the wrong stream;
update the logic in the block that currently checks `if (newStreamChannel !=
streamChannel)` to also detect parent-thread changes (compare previous
widget.parentMessage?.id to current widget.parentMessage?.id or always recreate
the newMessageStream subscription) and then cancel and recreate
_messageNewListener with the appropriate newMessageStream; ensure you still
cancel debouncedMarkRead/debouncedMarkThreadRead and update _unreadState via
_readUnreadSnapshot, and reuse the existing variables (_messageNewListener,
newMessageStream, streamChannel) so other cleanup (e.g., _userReadListener)
remains correct.

---

Nitpick comments:
In
`@packages/stream_chat_flutter/test/src/message_list_view/auto_scroll_test.dart`:
- Around line 122-350: Add tests that exercise the thread-mode listener path:
create a test that pumps the message list in thread mode (using parentMessage /
parent id) and then deliver a new threaded message via the
newThreadMessageStream so the widget reacts the same as the main-list case (use
pumpMessageList, deliverNewMessage, messagesController and stub
channelClientState.messages as in other tests, and assert auto-scroll / FAB
behavior). Also add a test that switches the parent id on the same channel
(rebind case): pump the widget with one parentMessage, then change the
parentMessage to a different id, update
channelClientState/newThreadMessageStream accordingly, push messagesController
events and assert the listener was torn down/rebound correctly (no exceptions
and correct rendering), referencing StreamMessageListView, messagesController,
channelClientState.messages, pumpMessageList, and deliverNewMessage to locate
where to add these cases.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3c623648-a62f-4ab4-8823-4085c61458aa

📥 Commits

Reviewing files that changed from the base of the PR and between 7b1dbbc and 2aba070.

📒 Files selected for processing (4)
  • packages/stream_chat_flutter/CHANGELOG.md
  • packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart
  • packages/stream_chat_flutter/lib/src/message_list_view/mlv_utils.dart
  • packages/stream_chat_flutter/test/src/message_list_view/auto_scroll_test.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/stream_chat_flutter/CHANGELOG.md

@xsahil03x xsahil03x merged commit 386807d into master May 27, 2026
24 checks passed
@xsahil03x xsahil03x deleted the fix/auto-scroll-on-local-message branch May 27, 2026 11:55
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