Skip to content

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

Open
xsahil03x wants to merge 1 commit into
masterfrom
fix/auto-scroll-on-local-message
Open

fix(ui): auto-scroll on optimistic local messages#2683
xsahil03x wants to merge 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), a private extension that diffs channel.state.messagesStream 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.
  • Matches the data-source-driven pattern in the Android, iOS, and React Native SDKs.
  • Gated on isUpToDate (matching Android's areNewestMessagesLoaded and iOS's isFirstPageLoaded): 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 retention pruning + append in the same emission, deletion of the tail, optimistic→server confirm (same id, no double-scroll), and failed→retry.
  • Re-binds the listener when widget.parentMessage changes via didUpdateWidget (thread parent switch on the same channel).

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

    • Auto-scroll now reliably keeps the view pinned to the bottom when you send messages (including optimistic/local sends) and avoids unwanted jumps when new messages arrive or message state changes.
  • Tests

    • Added comprehensive tests covering auto-scroll behavior for incoming, outgoing, optimistic, and burst-message scenarios.
  • Documentation

    • Added an "Upcoming Changes" entry noting the auto-scroll fix.

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
  • 🔍 Trigger review
📝 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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 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 63.63636% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.37%. Comparing base (8b7b890) to head (76ad959).

Files with missing lines Patch % Lines
...r/lib/src/message_list_view/message_list_view.dart 63.63% 12 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2683      +/-   ##
==========================================
+ Coverage   65.33%   65.37%   +0.04%     
==========================================
  Files         423      423              
  Lines       26648    26669      +21     
==========================================
+ Hits        17410    17436      +26     
+ Misses       9238     9233       -5     

☔ 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

@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 from d442be8 to 77ea726 Compare May 26, 2026 16:26
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 77ea726 to 76ad959 Compare May 26, 2026 16:48
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.

1 participant