Skip to content

fix(llc, core, ui): isolate channel state from sub-route wraps and smooth thread-open scroll#2704

Merged
xsahil03x merged 3 commits into
masterfrom
fix/thread-state-portable
Jun 2, 2026
Merged

fix(llc, core, ui): isolate channel state from sub-route wraps and smooth thread-open scroll#2704
xsahil03x merged 3 commits into
masterfrom
fix/thread-state-portable

Conversation

@xsahil03x
Copy link
Copy Markdown
Member

@xsahil03x xsahil03x commented Jun 2, 2026

Summary

Small cluster of related fixes around channel-state isolation and thread-page UX.

  • Channel.getMessagesById is now a pure fetch — no longer merges the response into ChannelState.messages. Resolving a thread parent for an in-channel reply previously injected the parent into the loaded channel window (typically at index 0, since parents predate the loaded window).
  • StreamChannel.getMessage cache lookup now also scans state.threads.values and state.pinnedMessages before falling back to the network.
  • Added StreamChannel.value constructor. Wraps a channel for context access without running channel-page positioning. Use for sub-route or overlay wraps (thread page, info screens, long-press modal, attachment viewer).
  • MessageListCore no longer reloads the parent channel from its dispose path when the disposing instance is in thread mode.
  • ScrollablePositionedList._startScroll waits one frame for layout when itemPositions is empty. Previously a scrollTo invoked right after mount took the dual-controller teleport path even for nearby targets, producing a multi-screen fake-scroll where a short animation was expected. Matches Compose's LazyListState.animateScrollToItem (which suspends until first composition).

Test plan

  • In a channel with a thread that has an in-channel reply, tap "View Thread" on the reply — thread opens at the target without an animated scroll-up, channel window unchanged on pop.
  • Tap a quoted (old) message in the channel, then long-press any message — channel doesn't silently reload to latest behind the modal.
  • Open a thread, pop back — channel's loaded window unchanged.
  • streamChannel.getMessage(threadReplyId) for a reply already loaded in state.threads resolves from cache (no network call).

Summary by CodeRabbit

  • New Features

    • Added an option to wrap pre-initialized channels while skipping automatic positioning/loading.
  • Bug Fixes

    • Fetching messages by ID no longer mutates the loaded message window.
    • Fixed premature long-distance scroll behavior immediately after widget mount.
    • Improved message lookup to check multiple in-memory caches before a network fetch.
    • Channel reload now replaces previously loaded messages instead of merging.
    • Prevented channel reloads during disposal for thread views.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 640c1a69-7755-453f-89e7-ad87811a6abe

📥 Commits

Reviewing files that changed from the base of the PR and between b7cbab4 and 04e9c10.

📒 Files selected for processing (1)
  • packages/stream_chat_flutter_core/lib/src/message_list_core.dart

📝 Walkthrough

Walkthrough

Removes Channel.getMessagesById side effects, adds StreamChannel.value for pre-initialized channels (skips positioning), makes getMessage search caches before fetching, prevents parent-channel reload when disposing thread views, and defers ScrollablePositionedList scroll decisions until item positions are available.

Changes

Channel State & Message Initialization

Layer / File(s) Summary
Channel.getMessagesById side-effect removal
packages/stream_chat/lib/src/client/channel.dart, packages/stream_chat/CHANGELOG.md
Channel.getMessagesById no longer updates the local channel window; it returns the network response directly and changelog notes the removal of the side effect.
StreamChannel.value constructor for pre-initialized channels
packages/stream_chat_flutter_core/lib/src/stream_channel.dart, packages/stream_chat_flutter_core/CHANGELOG.md
Adds const StreamChannel.value({required Widget child, required Channel channel}) that renders an already-initialized channel and sets a private _shouldPosition flag to skip mount-time positioning/loading; _maybeInitChannel returns early when used.
Message cache-first lookup and thread handling
packages/stream_chat_flutter_core/lib/src/stream_channel.dart, packages/stream_chat_flutter_core/lib/src/message_list_core.dart, packages/stream_chat_flutter_core/CHANGELOG.md
StreamChannelState.getMessage now checks loaded messages, thread replies, then pinned messages before calling channel.getMessagesById; MessageListCore skips reloading the parent channel when disposing in a thread.

Scroll Positioning Fix

Layer / File(s) Summary
ScrollablePositionedList frame deferral for item positions
packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart, packages/stream_chat_flutter/CHANGELOG.md
_startScroll defers to SchedulerBinding.instance.endOfFrame when itemPositions is empty, re-checks mounted, and then decides direct vs. dual-controller scrolling using populated position data; changelog updated.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • renefloor
  • Brazol
  • martinmitrevski

Poem

🐇 I hopped the channel without a fuss,
No hidden mutations followed us.
.value kept the window still,
Cache looked first — no network spill,
And scrolls now wait for layout's trust!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly addresses the main objective of isolating channel state from sub-route wraps and fixing thread-open scroll behavior, which are the core changes reflected across the codebase modifications.
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.

✏️ 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/thread-state-portable

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.

@xsahil03x xsahil03x force-pushed the fix/thread-state-portable branch 2 times, most recently from e53b58a to e84c8e9 Compare June 2, 2026 05:24
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

🤖 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_core/lib/src/stream_channel.dart`:
- Around line 66-74: Add a fail-fast assert in the StreamChannel.value
constructor to prevent passing an uninitialized channel: assert(channel.state !=
null, 'StreamChannel.value requires a channel with initialized state'). This
ensures callers can't set showLoading = false while giving a channel whose state
is null, avoiding silent returns of child and downstream null-state crashes;
update the StreamChannel.value constructor (referenced symbol:
StreamChannel.value and channel.state) to include this assert and keep current
default assignments.
🪄 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: 447f7bad-61d7-4a25-ae7e-96afb50a150b

📥 Commits

Reviewing files that changed from the base of the PR and between 58331ec and a07937a.

📒 Files selected for processing (7)
  • packages/stream_chat/CHANGELOG.md
  • packages/stream_chat/lib/src/client/channel.dart
  • packages/stream_chat_flutter/CHANGELOG.md
  • packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart
  • packages/stream_chat_flutter_core/CHANGELOG.md
  • packages/stream_chat_flutter_core/lib/src/message_list_core.dart
  • packages/stream_chat_flutter_core/lib/src/stream_channel.dart

Comment thread packages/stream_chat_flutter_core/lib/src/stream_channel.dart Outdated
@xsahil03x xsahil03x force-pushed the fix/thread-state-portable branch from e84c8e9 to 628f661 Compare June 2, 2026 05:27
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_core/lib/src/stream_channel.dart (1)

66-74: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Consider a fail-fast assert for an uninitialized channel.

Because showLoading is forced to false, passing a channel whose state is still null lets the first build return child before _maybeInitChannel's watch() completes, so descendants can hit a much less actionable null-state crash. The doc on Line 59-60 already states the channel must be initialized; an assert makes that contract enforceable at the callsite.

Suggested guard
   const StreamChannel.value({
     super.key,
     required this.child,
     required this.channel,
-  }) : showLoading = false,
+  }) : assert(
+         channel.state != null,
+         'StreamChannel.value requires an already-initialized channel.',
+       ),
+       showLoading = false,
         initialMessageId = null,
         errorBuilder = _defaultErrorBuilder,
         loadingBuilder = _defaultLoadingBuilder,
         _shouldPosition = false;
🤖 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_core/lib/src/stream_channel.dart` around lines
66 - 74, Add a fail-fast assert to the StreamChannel.value constructor to ensure
the provided channel is already initialized (channel.state != null) since
showLoading is forced false; update the constructor (StreamChannel.value) to
assert channel.state != null with a clear message so callers get an immediate
error instead of downstream null crashes from _maybeInitChannel.watch().
🤖 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_core/lib/src/stream_channel.dart`:
- Around line 66-74: Add a fail-fast assert to the StreamChannel.value
constructor to ensure the provided channel is already initialized (channel.state
!= null) since showLoading is forced false; update the constructor
(StreamChannel.value) to assert channel.state != null with a clear message so
callers get an immediate error instead of downstream null crashes from
_maybeInitChannel.watch().

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6e12a87e-415d-4a36-aa26-ecdade2a14ab

📥 Commits

Reviewing files that changed from the base of the PR and between a07937a and e84c8e9.

📒 Files selected for processing (7)
  • packages/stream_chat/CHANGELOG.md
  • packages/stream_chat/lib/src/client/channel.dart
  • packages/stream_chat_flutter/CHANGELOG.md
  • packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart
  • packages/stream_chat_flutter_core/CHANGELOG.md
  • packages/stream_chat_flutter_core/lib/src/message_list_core.dart
  • packages/stream_chat_flutter_core/lib/src/stream_channel.dart
✅ Files skipped from review due to trivial changes (3)
  • packages/stream_chat/CHANGELOG.md
  • packages/stream_chat_flutter/CHANGELOG.md
  • packages/stream_chat_flutter_core/CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/stream_chat/lib/src/client/channel.dart
  • packages/stream_chat_flutter_core/lib/src/message_list_core.dart
  • packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart

@xsahil03x xsahil03x force-pushed the fix/thread-state-portable branch from 628f661 to 3a9b8d5 Compare June 2, 2026 05:29
…ooth thread-open scroll

A small cluster of related issues that surfaced together while
investigating thread-page UX. All four are independent bugs touching
the LLC, core, and the vendored SPL.

- `Channel.getMessagesById` was merging fetched messages into
  `ChannelState.messages` as a side effect. Resolving a thread parent
  for an in-channel reply would silently inject the parent into the
  loaded channel window, typically at index 0. The fetch is now pure
  — callers that want to refresh the loaded window can pipe the
  response through `ChannelClientState.updateMessage`.
- `StreamChannel.getMessage` previously hit the network for any
  message not in `channel.state.messages`, ignoring thread replies
  and pinned messages. Cache lookup now also scans
  `channel.state.threads.values` and `channel.state.pinnedMessages`.
- Added `StreamChannel.value` constructor for wrapping an
  already-initialized channel purely to expose it via
  `StreamChannel.of` to a sub-route or overlay (thread page, channel
  info screen, long-press modal, attachment viewer). Skips the
  channel-page positioning the default constructor runs, which would
  otherwise overwrite the parent route's loaded window.
- `MessageListCore` no longer reloads the parent channel from its
  dispose path when the disposing instance is in thread mode — a
  thread's lifecycle shouldn't be touching the channel's loaded
  window.
- `MessageListCore` no longer refetches thread replies on first
  attach when `state.threads[parentId]` is already populated.
  WebSocket events keep that cache live; the redundant fetch caused
  a merge-rebuild flicker on warm cache.
- `ScrollablePositionedList._startScroll` waits one frame for
  `itemPositions` to be published when it's empty. Previously a
  `scrollTo` requested from a post-frame callback right after mount
  would fall through to the dual-controller teleport path even for
  nearby targets, fake-scrolling two screens for what should have
  been a short animation. Matches the Compose `LazyListState`
  pattern of suspending until first composition before scrolling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@xsahil03x xsahil03x force-pushed the fix/thread-state-portable branch from 3a9b8d5 to 7946a52 Compare June 2, 2026 05:30
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 2, 2026

Codecov Report

❌ Patch coverage is 19.04762% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.64%. Comparing base (8f77232) to head (04e9c10).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
...ream_chat_flutter_core/lib/src/stream_channel.dart 6.25% 15 Missing ⚠️
...ositioned_list/src/scrollable_positioned_list.dart 33.33% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2704      +/-   ##
==========================================
- Coverage   65.67%   65.64%   -0.04%     
==========================================
  Files         423      423              
  Lines       26694    26708      +14     
==========================================
+ Hits        17532    17533       +1     
- Misses       9162     9175      +13     

☔ 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.

Copy link
Copy Markdown
Contributor

@VelikovPetar VelikovPetar left a comment

Choose a reason for hiding this comment

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

Left a very small question, otherwise looks good!

Comment thread packages/stream_chat_flutter_core/lib/src/message_list_core.dart Outdated
xsahil03x and others added 2 commits June 2, 2026 13:26
…eeded

Encapsulates the early-return conditions in one place so callers don't
need to know which contexts skip the reload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@xsahil03x xsahil03x enabled auto-merge (squash) June 2, 2026 11:29
@xsahil03x xsahil03x merged commit 093743a into master Jun 2, 2026
20 checks passed
@xsahil03x xsahil03x deleted the fix/thread-state-portable branch June 2, 2026 11:36
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