Skip to content

perf(llc): faster channel state ingest#2653

Merged
xsahil03x merged 35 commits into
masterfrom
perf/channel-state-ingest
May 18, 2026
Merged

perf(llc): faster channel state ingest#2653
xsahil03x merged 35 commits into
masterfrom
perf/channel-state-ingest

Conversation

@xsahil03x
Copy link
Copy Markdown
Member

@xsahil03x xsahil03x commented May 18, 2026

Closes FLU-486

Summary

Reduces CPU and allocation cost in ChannelClientState on the message-ingest hot path. Targets Pain Point #6 from the Flutter tech-debt notion doc, plus a few adjacent cleanups.

Each commit is one focused change so they can be reviewed (and reverted) independently.

Commit Change
feat(llc): add SortedListX and ListX list extensions Foundational List extensions: merge (two-pointer + append/prepend fast paths when compare provided), sortedInsert, sortedUpsert, sortedUpsertAt, updateIf. Signatures mirror stream_core's upcoming list_extensions for a clean future swap.
perf(llc): faster channel state ingest The actual Pain Point #6 fix. updateMessage uses one indexWhere + strict-newer short-circuit + sortedUpsertAt; updateChannelState inlines merges with identity-skip so read/membership-only updates allocate nothing; _updatePinnedMessages has a fast-path for the no-pins case; reaction/update handlers dispatch by parentId first via _findMessage. Legacy Iterable.merge extension removed.
fix(llc): preserve null transitions in Channel.currentUserReadStream mapNotNull swallowed the user-logged-out transition. Switched to map so null propagates.
refactor(llc): drop the per-update walk for expired CDN attachment URLs _checkExpiredAttachmentMessages ran on every updateChannelState walking every message and parsing every CDN URL. Removed — matches JS SDK; v10's StreamImageCDN.cacheKey already keeps the image cache valid across signed-URL rotations.

Base branch

This PR is based on fix/message-list-view-performance-issues (the UI perf PR), because that branch added .distinct() to Channel.readStream / Channel.currentUserReadStream / Channel.unreadCountStream which this work refines. Once the UI PR merges to master, this PR's base will auto-shift.

Test plan

  • flutter test across stream_chat — 1,204 tests pass
  • dart analyze clean
  • No external callers reference the removed symbols (_checkExpiredAttachmentMessages, _updatedMessagesIds, Iterable.merge)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Message trimming controls (pruneOldest, maximumMessageLimit, retentionTrimBuffer) and new list utilities (sorted insert/upsert/upsertAt, keyed merge, conditional update) now publicly available.
  • Bug Fixes

    • queryDrafts now correctly forwards the filter argument to the API.
  • Performance

    • Faster channel state updates and deduplicated read/unread streams.
  • Changed

    • currentUserReadStream emits null on logout; attachment URL refresh now relies on server events.
  • Tests

    • Expanded tests for pruning, merge/sort behavior, and list utilities.

Review Change Stack

xsahil03x and others added 21 commits May 13, 2026 17:01
- stream_chat: dedupe readStream and unreadCountStream via .distinct() so
  listeners don't rebuild on unrelated channel state mutations.
- stream_chat_flutter: markRead/markThreadRead fire on the leading edge of
  the 1s debounce so the first read receipt hits the server immediately,
  and the thread page falls back to the captured parent message when it's
  no longer in the loaded list (fixes StateError).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ageListCore

- Cache the resolved messagesStream so parent rebuilds no longer cancel and
  resubscribe + replay through the full pipeline.
- Combine filter and reverse into a single lazy pass (3 list allocations per
  emission collapsed to 1).
- Drop the redundant ListEquality comparator on BetterStreamBuilder; upstream
  Channel.messagesStream already deduplicates via .distinct().
- Suppress transient empty emissions during channel reload at the source
  instead of via a long-lived cached _messages field.
- Tear down MessageListController.paginateData on dispose and on controller
  swap to prevent the externally-owned controller from holding a closure on
  the disposed widget.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
- Reset _lastEvent / _lastError on stream identity change so the next build
  doesn't render the previous stream's last value before the new stream's
  first emission lands.
- Recover from the error state when an equal event arrives (was sticky
  until the next external rebuild).
- Route stream errors through FlutterError.reportError when no errorBuilder
  is provided, so failures aren't silently swallowed.
- Short-circuit _onEvent / _onError when unmounted, avoiding stray
  comparator passes on a disposed widget.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…e currentUserReadStream

Two coupled changes that together stop read-state churn from rebuilding the
entire StreamMessageListView on every read event.

- StreamMessageListView (stream_chat_flutter):
  * Replace `unreadCount` + `_firstUnreadMessage` fields and the per-event
    setState with a single `ValueNotifier<({int count, String? firstUnreadId})>`
    that the unread-separator `ValueListenableBuilder` subscribes to. Read
    events no longer rebuild the visible list rows; only the separator.
  * Listen on `currentUserReadStream` instead of `readStream`, so other users'
    reads no longer trigger any work.
  * Extract `_readUnreadSnapshot()` so the initial seed and the listener
    refresh share one expression.
  * Delete the dead `messagesIndex` field and its per-emission rebuild loop
    (was populated but never read by active code).
  * Drop the throwaway `.toList()` in `_handleItemPositionsChanged`.

- channel.dart (stream_chat):
  * `currentUserReadStream` now applies `.distinct()` on the user id before
    `switchMap` and on the resulting `Read?` after, eliminating both
    `OwnUser`-reassignment churn and duplicate emissions caused by other
    users' read events.
Drop the redundant `members.any(...)` membership check from
`_buildScrollToBottom`. `unreadCount > 0` already implies the user has a
`Read` entry on the channel, which is the actual source of truth for "has
unread messages?".

The membership check was a brittle proxy that broke in channels with more
than the default 100 loaded members: when the current user was paginated
out of `channel.state.members`, the badge was silently hidden even though
unread messages existed. The in-line unread separator never had this
check, so the two unread surfaces also disagreed.

Side benefit: removes the O(N) member scan on every FAB rebuild.
`_buildScrollToBottom` was the last `StreamBuilder` in the file and the
last consumer of `Channel.unreadCountStream`. The latter is a fresh
`Stream` chain on every getter access (`currentUserReadStream.map(...)
.distinct()`), so `StreamBuilder.didUpdateWidget`'s identity check
resubscribed the entire pipeline on every parent rebuild — at livestream
emission rates that's ~100 resubscribes/sec for a value the rest of the
widget already tracks in `_unreadState`.

Switch to `ValueListenableBuilder<_unreadState>`. Same source upstream
(`currentUserReadStream`), single subscription, single source of truth
shared with the unread-messages separator.

Side benefits:
- No initial-emission flicker (ValueListenable always has a seeded value).
- One canonical place to derive unread state if a third consumer ever
  needs it.
Matches the inferred form already used by the unread-separator builder.
Three perf changes targeting `StreamMessageListView` under flood load
(100+ msg/s incoming, e.g. livestream channels). On-device profile
across baseline → this branch: ~20% of UI-thread work reclaimed.

`ScrollablePositionedList`'s `_keyToIndexMap` was rebuilt per `build()`
by walking every item via `itemKeyBuilder`. On a 1k-message list that's
a fresh 1000-entry map per parent rebuild — 6.0% of UI-thread time.
Switch to a sliding-window bound matching Compose's
`LazyLayoutNearestRangeState` (window 30, ±100 extras, anchored at
bucket boundaries so scrolling within a bucket reuses the map). Cache
invalidates only on `itemCount` or bucket cross. Profile share:
6.00% → 0.25%. Also replace the `<Object, int>{}` literal (a
`LinkedHashMap` carrying insertion-order metadata we don't use) with
`HashMap`, and reuse one instance via `.clear()` + refill instead of
allocating per cache miss.

`ItemScrollController.isScrolling` + `isScrollingListenable` are new
APIs mirroring Compose's `LazyListState.isScrollInProgress`. Replace
the pre-existing pattern where `StreamMessageListView` listened to
`ScrollNotification`s and maintained its own `_isUserScrolling` flag —
that flag was getting stuck `true` on drag-without-fling because it
filtered on `ScrollEndNotification` instead of `UserScrollNotification`.
Single source of truth, fed by the controller's own activity state.

`_StreamMessageListViewState.buildMessage` was constructing the full
freshened `members` list per row, per build, just to scan for the
current user's `Member` via `firstWhereOrNull`. Read `channel.membership`
directly — single server-populated cached field — and drop the extra
`StreamChat.of(context)` lookup that was needed only to scope the scan.
Profile shows the `members` chain collapse from 5% of UI-thread time
to 1.5% with downstream allocation relief lifting `buildMessage` and
the `_buildItem` chain too.

Side changes the above enable / depend on:
- Anchor preservation across data mutations via `itemKeyBuilder` +
  saved-anchor-key state, invalidated on explicit `jumpTo`/`scrollTo`.
- Fit-anchor fallback in `UnboundedRenderViewport` for short-content
  lists — pins to axis-leading edge regardless of `initialAlignment`.

New tests:
- `is_scrolling_test` — `isScrolling` across drag, `scrollTo`, `jumpTo`,
  and listenable contract.
- `item_key_builder_test` — anchor preservation, element reuse,
  separators, reanchor guard, mid-gesture preservation, null opt-out.
- `viewport_fit_anchor_test` — short-content layout-time fit anchor.
- `mark_read_test` — `markReadWhenAtTheBottom` trigger gates + FAB
  visibility.
- `message_list_view_benchmark_test` — perf regression gate (cold
  build, batch insert, trickled stream).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`scrollTo` previously required callers to pass a `duration` and accepted
`curve: Curves.linear` by default — linear isn't right for a user-facing
animated scroll, so every caller (and there's only one: the
auto-scroll-on-new-message path in `StreamMessageListView`) duplicated
the same `Duration(milliseconds: 250) + Curves.easeOut` pair.

Lift those into the controller as defaults:

  duration = const Duration(milliseconds: 250)
  curve    = Curves.fastOutSlowIn

`fastOutSlowIn` is Material's standard scroll curve and the closest
built-in match to the critically-damped spring shape used by
`LazyListState.animateScrollToItem` (`SpringSpec(stiffness=400,
dampingRatio=1.0)`). 250 ms matches the spring's ~99 % settling time.
Crucially `transform(1.0) == 1.0` so the scroll lands exactly at the
target — the previous `easeOut` did too, but only by accident; an
earlier draft used a custom spring curve that violated this contract
and caused a one-frame snap at the end.

The auto-scroll call site collapses to `controller.scrollTo(index: 0)`
and the surrounding `_messageNewListener` block sheds ~25 lines of
attribution / restating-the-code comments, keeping only the three
non-obvious whys: skip while scrolling, anchor-preservation/auto-scroll
split, and why this is synchronous (not post-frame).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `onQuotedMessageTap` else-branch was silently broken. When the target
quoted message wasn't in the loaded window, it called
`channel.loadChannelAtMessage(id)` and then set the State fields
`initialIndex = 21; initialAlignment = 0.1;` — assuming a rebuild would
re-anchor the list to the loaded message. It doesn't:
`ScrollablePositionedList.initialScrollIndex` is read **only** during
the SPL's `initState`, so the assignments were dead after the first
mount. Result: tapping a quoted message that wasn't already loaded
paginated correctly but left the user at whatever scroll position they
were at before the tap. The hardcoded `21` (`19 + 2`) was also fragile
— it assumed `paginationLimit = 20` with the target at index 19.

Replaced both branches with one linear flow that uses the real index:

  1. Scan `this.messages` for the target.
  2. If not found, paginate, wait one frame for the rebuild to flush
     `this.messages`, re-scan.
  3. `scrollTo` the resulting index (or bail out if the message
     genuinely isn't in the channel).

`this.messages` (the State field, refreshed by the BetterStreamBuilder
callback) is read instead of the closure-captured `messages` parameter,
which is the snapshot at tap time and goes stale across the `await`.
Single `indexWhere` per path (the previous if-branch did
`map().contains()` then `indexWhere` — two scans).

While in there, three related cleanups:

- `initialIndex` / `initialAlignment` became `late final` initialised
  inline. The two assignments in `didChangeDependencies` (channel
  switch) and two more in `scrollToBottomDefaultTapAction` (channel not
  up to date) were all dead after the first SPL mount for the same
  reason. `getInitialIndex` is `O(N)` over channel messages — the
  `late final` initialiser runs once on first build's read, never
  recomputed. Removed the `_resolveInitialAlignment()` helper too — its
  four-line body collapsed into the field initialiser.

- The three remaining `scrollTo` call sites
  (`scrollToBottomDefaultTapAction`, `scrollToUnreadDefaultTapAction`,
  and the quoted-message tap) all duplicated
  `duration: const Duration(seconds: 1), curve: Curves.easeInOut`.
  Dropped — they now pick up `ItemScrollController.scrollTo`'s
  defaults (250 ms / `fastOutSlowIn`), matching the auto-scroll-on-new-
  message path that already used them. Consistent feel across every
  scroll the message list owns.

- `mounted` checks added across the async gaps in
  `scrollToBottomDefaultTapAction` and `onQuotedMessageTap` — these
  paths await pagination + a frame tick, so the State could be gone by
  the time we try to touch the controller.

- The stale `// we need to use ClampingScrollPhysics...` comment on
  `this.scrollPhysics` is gone. The default it referred to had already
  been removed.

Net: `message_list_view.dart` is 8 lines shorter, the quoted-message
pagination path actually works, and scrolling animations are uniform
across the four user-tap surfaces.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop the unused `super.key` parameter on `_CountingItem` — the test
  helper is private and no caller passes a key.
- Tear off `setUp(_CountingItem.reset)` instead of wrapping it in an
  empty closure.
- Collapse the two consecutive `items.removeAt(5)` calls into a
  cascade so analyzer's `cascade_invocations` rule is satisfied.

`melos analyze` now runs clean across the workspace.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`dart format` fallout — wraps three statements in `_resolveMessagesStream`
and `_filterAndReverse` to stay under the analyzer's 80-character line
limit. No behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bump SPL default scroll duration from 250ms to 400ms to align with
Compose's `animateScrollToItem` default spring (StiffnessMediumLow,
critical-damped) which perceptually settles in ~350-450ms. The previous
250ms felt rushed for long-distance scrolls (quoted/unread tap).

Change `onQuotedMessageTap` alignment from 0.1 (near-bottom) to 0.5
(centered), matching Compose's `defaultOffsetHandler` which centers the
focused message in the viewport via `-sizeDiff / 2`. Brings it in line
with `scrollToUnreadDefaultTapAction`, which already used 0.5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`refactor(ui): default ItemScrollController.scrollTo duration + curve`
(688fb03) changed the curve default from `Curves.linear` to
`Curves.fastOutSlowIn`. The SPL test suite makes position assertions at
fixed time fractions (halfway through a 1s scroll) and depended on the
linear-interpolation contract — under `fastOutSlowIn` half-time is no
longer half-distance, so 12 long-scroll tests broke.

Pass `curve: Curves.linear` explicitly to every `scrollTo` call in the
SPL test suite (92 sites). The tests are exercising the primary /
secondary sliver swap and stop-mid-scroll semantics, not the curve, so
forcing linear restores predictable time→distance mapping.

Drop `message_list_view_benchmark_test.dart`. Local baseline ~3.2s,
GitHub-hosted CI runs ~12s. Any budget tight enough to be a useful
regression gate is tight enough to flake on CI; any budget loose enough
to pass CI doesn't catch real regressions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`_userReadListener` writes to `_unreadState` (the `currentUserReadStream`
callback re-snapshots it). The old dispose order called
`_unreadState.dispose()` first and then `_userReadListener?.cancel()`,
so an in-flight microtask from the stream could land on a disposed
notifier. Reorder so all subscriptions and observers are torn down
first, then dispose the notifier. Null the subscription handles after
cancel for clarity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The branch added `.distinct()` to `Channel.readStream`,
`Channel.currentUserReadStream`, and `Channel.unreadCountStream`. The
CHANGELOG only listed the first and last — add `currentUserReadStream`
so the entry matches the code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `updateMessage` (binary insertion) and `updateChannelState`
(merge-skip) entries describe local-only SDK ingest changes that
aren't included in this PR. Remove them so the changelog matches
what's actually being shipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Foundation for upcoming `ChannelClientState` perf work.

`SortedListX`:
- `sortedInsert(element, compare:)` — O(log n) binary search with O(1)
  append / prepend fast paths.
- `sortedUpsert(element, key:, compare:, update:)` — keyed sorted upsert
  with in-place replacement when the sort position is unchanged.
- `sortedUpsertAt(index, element, compare:, update:)` — sorted upsert
  when the caller already has the index from a prior scan.
- `merge(other, key:, update:, compare:)` — keyed merge. When `compare`
  is provided runs an O(N + M) two-pointer pass with append / prepend
  fast paths; otherwise a Map-based merge. Returns the receiver
  unchanged when `other` is `null`, empty, or identical.

`ListX`:
- `updateIf(test, update)` — copy-on-write update; returns the receiver
  reference when nothing matches.

Signatures mirror `stream_core`'s upcoming `list_extensions`, so the
later swap to `package:stream_core` is a one-line import change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pain Point #6 from the tech-debt notion doc — message updates were
re-sorting the entire `messages` list and reaction handlers were
scanning the wrong list when the target was a thread message.

`ChannelClientState.updateMessage` now:
- Uses one `indexWhere` for both the existing-message lookup and the
  sorted upsert (via the new `sortedUpsertAt`).
- Skips the lookup scan entirely for new messages strictly newer than
  the current tail — the livestream steady state.
- `_updatePinnedMessages` returns the existing reference unchanged when
  the incoming message isn't pinned and the pinned list is empty.
- Uses `updateIf` for the quoted-message rewrite, which is
  identity-stable when nothing matches.

`ChannelClientState.updateChannelState` now merges messages, watchers
and reads via the new `List.merge` extension. The merge runs an
O(N + M) two-pointer pass when `compare` is provided; non-overlapping
fast paths cover the livestream-tail and paginated-history cases. Reads
and watchers (no `compare`) take the Map-based path. All variants
return the receiver unchanged when the incoming collection is null,
empty, or identical — so read-state / membership / watcher updates
that pass `messages` through allocate nothing.

`Channel.updateRead` and `Channel.updateThreadInfo` use the same
`List.merge` extension.

Reaction / message-update event handlers (`reaction.new`,
`reaction.deleted`, `reaction.updated`, `message.updated`) now dispatch
by `parentId` first via the new `_findMessage` helper: thread events
look only inside `threads[parentId]`, channel events look only inside
`messages`. Previously every reaction event scanned the full channel
list even when the event targeted a thread message.

The legacy `Iterable.merge` extension is removed — all call sites
migrated to `List.merge`.

Tests: new `updateChannelState identity guard` and `updateMessage
quoted-rewrite` groups exercising the new behaviour. Four existing
thread-message tests gained explicit `createdAt: DateTime.now()` — the
new `merge(compare:)` precondition asserts sort consistency, and
`Message.createdAt` falls back to `DateTime.now()` on every read when
no `createdAt` is provided, which previously hid duplicate-key bugs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`mapNotNull` swallowed the user-logged-out transition. Switched to
`map` so `null` propagates downstream. The `distinct()` step still
prevents re-subscribing on equal user ids.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`_checkExpiredAttachmentMessages` ran on every `updateChannelState`,
walked every message, parsed every CDN URL, and re-fetched any messages
with expired `Expires` query params. Matches the JS SDK's behaviour now
— URL refreshes flow through normal server events. v10's
`StreamImageCDN.cacheKey` already strips the volatile signed-URL
parameters from the image cache key, so the on-device cache survives
signature rotations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 330d6455-fbd7-4751-aaa0-205fcf03ba8c

📥 Commits

Reviewing files that changed from the base of the PR and between 4f369c4 and be2e997.

📒 Files selected for processing (1)
  • .github/workflows/pana.yml
✅ Files skipped from review due to trivial changes (1)
  • .github/workflows/pana.yml

📝 Walkthrough

Walkthrough

This PR implements a complete message pruning system to cap loaded message counts in memory, starting with new foundational list utilities (SortedListX, ListX), refactoring channel state management to use those utilities efficiently, introducing a retention gate policy engine, integrating pruning orchestration into MessageListCore, and exposing configuration through the UI layer.

Changes

Message Pruning with Configurable Limits

Layer / File(s) Summary
List extension utilities
packages/stream_chat/lib/src/core/util/list_extensions.dart, packages/stream_chat/lib/src/core/util/extension.dart, packages/stream_chat/lib/stream_chat.dart, packages/stream_chat/test/src/core/util/list_extensions_test.dart
New SortedListX extension with stable sorted insert/upsert/merge operations supporting keyed-map and pre-sorted two-pointer merge modes; new ListX.updateIf for conditional element replacement; SafeCastExtension replacing old iterable-merge; comprehensive tests and public export.
Channel state optimization and pruneOldest
packages/stream_chat/lib/src/client/channel.dart, packages/stream_chat/test/src/client/channel_test.dart
Refactored ChannelClientState to use new list merge utilities for messages/reads/threads; removed expired-attachment scanning; added _findMessage helper; simplified updateRead/updateMessage/updateChannelState; added pruneOldest(int maxMessages); optimized currentUserReadStream resubscription; added identity-guard and quoted-rewrite tests.
MessageRetentionGate policy engine
packages/stream_chat_flutter_core/lib/src/message_list_core.dart, packages/stream_chat_flutter_core/test/message_list_core_test.dart
New MessageRetentionGate encapsulates pruning decision logic (limit, trimBuffer, isUpToDate, length threshold, tail-id change); supports seed/configure and unit tests for decision rules.
MessageListCore retention orchestration
packages/stream_chat_flutter_core/lib/src/message_list_core.dart, packages/stream_chat_flutter_core/test/message_list_core_test.dart
MessageListCore gains maximumMessageLimit and retentionTrimBuffer; initializes/manages MessageRetentionGate; re-seeds/reconfigures on context changes; evaluates emissions and calls streamChannel.pruneOldest(...) when gate decides.
StreamChannel and UI integration
packages/stream_chat_flutter_core/lib/src/stream_channel.dart, packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.dart, packages/stream_chat_flutter_core/test/stream_channel_test.dart
Added StreamChannel.pruneOldest(maxMessages) delegating to ChannelClientState.pruneOldest and resetting top-pagination when messages shrink; added StreamMessageListView.maximumMessageLimit forwarded to MessageListCore; tests for delegation and pagination reset/no-op behavior.
Changelog & CI
packages/stream_chat/CHANGELOG.md, packages/stream_chat_flutter/CHANGELOG.md, packages/stream_chat_flutter_core/CHANGELOG.md, .github/actions/pana/action.yml, .github/workflows/pana.yml
Updated changelogs documenting new APIs/behavior and adjusted Pana composite action defaults and workflow min_score inputs (120→100); changed local-dependency override editing to write under dependencies.*.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • GetStream/stream-chat-flutter#2651: Both PRs touch ChannelClientState read-state streams and .distinct() deduplication behavior alongside currentUserReadStream null-propagation and resubscribe logic.

Suggested reviewers

  • Brazol

Poem

🐰 I hop through lists both neat and bright,
I nudge old messages out of sight.
Gates watch the tail, they trim with care,
Streams stay tidy, memory fair. 🌿✨

🚥 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 accurately summarizes the main performance objective, focusing on faster channel state ingestion as the primary change throughout the PR.
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 perf/channel-state-ingest

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.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 18, 2026

Codecov Report

❌ Patch coverage is 95.13514% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.30%. Comparing base (32ab308) to head (be2e997).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
...m_chat_flutter_core/lib/src/message_list_core.dart 87.80% 5 Missing ⚠️
packages/stream_chat/lib/src/client/channel.dart 93.10% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2653      +/-   ##
==========================================
+ Coverage   64.98%   65.30%   +0.31%     
==========================================
  Files         421      422       +1     
  Lines       26549    26640      +91     
==========================================
+ Hits        17252    17396     +144     
+ Misses       9297     9244      -53     

☔ 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 and others added 3 commits May 18, 2026 06:59
…annelState

`[...?updatedState.members]` produced a defensive copy and coerced
`null` to `[]` (silently clearing the channel's members on a partial
state update). Passing `updatedState.members` directly:

- shares the reference (no copy — `updatedState` is transient and
  `members` is treated as immutable elsewhere)
- lets `ChannelState.copyWith`'s `?? this.members` preserve the
  existing members on null incoming — matching the new
  preserve-on-null semantics of watchers and reads in this method

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread packages/stream_chat/lib/src/client/channel.dart
)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Base automatically changed from fix/message-list-view-performance-issues to master May 18, 2026 12:00
…ingest

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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: 4

🤖 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/message_list_core.dart`:
- Around line 328-354: The MessageRetentionGate currently accepts negative
values for limit and trimBuffer which can break pruning math; add upfront
validation in the constructor and in configure(...) to reject negative values
(e.g., throw ArgumentError or assert) when limit < 0 or trimBuffer < 0, and keep
existing behavior for null limit (disabled); update MessageRetentionGate(...)
and configure({ required int? limit, required int trimBuffer }) to perform these
checks before assigning to _limit/_trimBuffer so invalid settings cannot
propagate into pruneOldest(...) or threshold computations.
- Around line 364-378: The prune predicate currently treats any change in tail
id as "new data", causing deletions to trigger trimming; modify evaluate() to
track the previous seen list length (add a field like _lastSeenTailCount) and
only treat a tail-id change as new data when the list length has grown (i.e.,
newTailId != previousTailId && data.length > _lastSeenTailCount) or when
previousTailId is null; update both _lastSeenTailId and _lastSeenTailCount at
the end of evaluate(); keep the existing checks using _limit and _trimBuffer
unchanged but use the length-growth condition to prevent deletions from causing
pruning.

In `@packages/stream_chat_flutter_core/test/message_list_core_test.dart`:
- Around line 401-406: The test is appending a message with a duplicate id
(causing update semantics) instead of a true new-tail arrival; modify the
creation of withNewArrival so the appended message has a unique id by using
_generateMessages in a way that produces a different id than initialMessages
(e.g., pass an offset/seed or call a helper to produce a new unique id) or
explicitly set a new id on the generated message (via the message's
copy/constructor) before appending; update references around initialMessages,
withNewArrival and _generateMessages accordingly.

In `@packages/stream_chat/lib/src/core/util/list_extensions.dart`:
- Around line 129-135: The two-pointer merge path in merge(compare:) can emit
duplicate keys if either input contains repeated keys; before calling
_mergeSortedTwoPointer, normalize/deduplicate both inputs by their key (using
the same key(...) selector and resolve(...) semantics) or detect duplicates
during the two-pointer walk and immediately fallback to the keyed-map merge
implementation (the map-based merge used elsewhere) to ensure the contract of
unique keys is preserved; update merge(compare:) to perform this
dedupe-or-fallback logic so _mergeSortedTwoPointer only runs on inputs
guaranteed to have unique keys.
🪄 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: 371aecb1-27a6-42f1-8bcb-931662e58291

📥 Commits

Reviewing files that changed from the base of the PR and between 32ab308 and 06bd2ea.

📒 Files selected for processing (14)
  • packages/stream_chat/CHANGELOG.md
  • packages/stream_chat/lib/src/client/channel.dart
  • packages/stream_chat/lib/src/core/util/extension.dart
  • packages/stream_chat/lib/src/core/util/list_extensions.dart
  • packages/stream_chat/lib/stream_chat.dart
  • packages/stream_chat/test/src/client/channel_test.dart
  • packages/stream_chat/test/src/core/util/list_extensions_test.dart
  • packages/stream_chat_flutter/CHANGELOG.md
  • packages/stream_chat_flutter/lib/src/message_list_view/message_list_view.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
  • packages/stream_chat_flutter_core/test/message_list_core_test.dart
  • packages/stream_chat_flutter_core/test/stream_channel_test.dart
💤 Files with no reviewable changes (1)
  • packages/stream_chat/lib/src/core/util/extension.dart

Comment thread packages/stream_chat_flutter_core/lib/src/message_list_core.dart
Comment thread packages/stream_chat_flutter_core/lib/src/message_list_core.dart Outdated
Comment thread packages/stream_chat_flutter_core/test/message_list_core_test.dart
Comment thread packages/stream_chat/lib/src/core/util/list_extensions.dart
xsahil03x and others added 6 commits May 18, 2026 14:22
…el replies

Replies with show_in_channel = true are mirrored into both threads[parentId]
and the channel-level messages list. The previous short-circuit on parentId
skipped the messages scan entirely, so when the thread wasn't loaded the
lookup returned null and reaction/messageUpdated events stripped the
locally-cached ownReactions/poll/pollId from the channel-level copy.

Fall through to the messages scan when the thread missed and the reply is
shown in channel; keep the perf win for thread-only replies (parentId set,
showInChannel != true) untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…onGate

Negative inputs would skew the `limit + trimBuffer` threshold and waste
evaluate cycles (pruneOldest already guards `maxMessages <= 0`). Guard
the gate's constructor and configure() to surface the misconfiguration
in debug builds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After top-pagination the cached message count can sit above `limit +
trimBuffer` with the tail unchanged. Deleting the tail then both shrinks
the list and rotates the tail id, which the previous predicate treated as
"new data" and pruned — which also reset StreamChannel's
`_topPaginationEnded` tracker unnecessarily. Track `_lastSeenCount`
alongside the tail id and require both a new tail and a longer list before
firing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`_generateMessages(count: 1).first` re-emitted `testMessageId0`, so the
list became `[m0..m9, m0]` — a duplicate-id append that hits update
semantics, not a true new-tail arrival. Offset the second call to keep
the appended id distinct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The two-pointer branch documented that duplicate-keyed inputs may surface
duplicates in the output. All in-tree callers feed server-deduped data,
so the right move is a debug-only guard rather than a runtime fallback
that would defeat the fast path. Adds a `_hasUniqueKeys` assert alongside
the existing `_isSorted` asserts and tests for both new failure modes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@xsahil03x xsahil03x force-pushed the perf/channel-state-ingest branch from 7875579 to f7ca349 Compare May 18, 2026 13:03
…_score to 100

The previous `dependency_overrides` form is stripped by pana's
`_stripAndAugmentPubspecYaml`, so cross-package-API PRs (e.g. consuming
a new `ChannelClientState.pruneOldest` from `stream_chat_flutter_core`)
and release PRs (constraints reference not-yet-published versions) both
fail static analysis when pana resolves against pub.dev.

Writing the path into `dependencies` survives the strip, but `dart
analyze` (newer Dart SDK) flags it with a "Publishable packages can't
have 'path' dependencies" warning. Combined with the WASM partial-score
penalty added in pana 0.23.10 (-10 for web packages with native deps),
the score floor lands at ~110 in CI. Threshold drops to 100 to leave a
small margin and unblock both classes of PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…100) wins

Each job in pana.yml explicitly passes `min_score: 120`, which shadows
the action's default. Without this the previous commit's threshold
change had no effect (jobs still compared against 120 and failed at
110/160).

Tracking pana score recovery work in linear FLU-492.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@xsahil03x xsahil03x merged commit 1139f35 into master May 18, 2026
17 checks passed
@xsahil03x xsahil03x deleted the perf/channel-state-ingest branch May 18, 2026 13:54
xsahil03x added a commit that referenced this pull request May 20, 2026
Brings master's QA / security / perf work into the v10 design-refresh
branch.

Highlights of what landed in v10 from master:

LLC (`stream_chat`)
- `Client.queryDrafts` now forwards `filter` (#2647).
- `Client.queryChannels` coalesces concurrent identical queries via the
  new `InFlightCache<K, V>` (#2652).
- `SortedListX` / `ListX` extensions added in `list_extensions.dart`;
  duplicate-keyed inputs are tolerated by `merge` (#2660). v10's
  `IterableMergeExtension.merge` / `.mergeFrom` are kept — `SortedListX`
  is on `List` and routes there for the new perf paths; the old
  extension still serves `Iterable<T>` callers in v10.
- `ChannelClientState._checkExpiredAttachmentMessages` removed (#2653);
  v10's `StreamImageCDN.cacheKey` already keeps the image cache valid
  across signed-URL rotations.
- `ChannelClientState.updateChannelState` now identity-short-circuits
  when `updatedState.messages` is null or the same reference, so
  downstream `.distinct()` listeners can skip rebuilds.
- Reaction listeners now dispatch via `_findMessage` (parentId-aware)
  while keeping v10's `addMyReaction` / `deleteMyReaction` semantics.

`stream_chat_flutter_core`
- `BetterStreamBuilder` correctness fixes: mounted guard, error reporting
  via `FlutterError.reportError`, identity-equal emission gating (#2651).
- `MessageListCore` caches its `messagesStream` / `_initialMessages` as
  fields instead of recomputing in `build()` (#2651). `defaultMessageFilter`
  takes an optional `currentUserId`.
- `StreamChatCore` debounces connectivity events to 3 s (#2652).

`stream_chat_flutter`
- `scrollable_positioned_list/`: master version taken in full. Bounded
  `_keyToIndexMap`, `isScrolling` / `isScrollingListenable`,
  `itemKeyBuilder` anchor preservation, fit-anchor fallback in
  `UnboundedRenderViewport`, sensible defaults on `scrollTo` (#2651).
- `tld.dart` removed (#2654); `StreamMessageComposer` relaxed its URL
  regex from `[a-z]{2,4}` to `[a-z]{2,}` and dropped the `isValidTLD`
  filter at both call sites.
- `StreamMessageListView` and `separated_reorderable_list_view`:
  v10's design-refresh version retained. v10 already covers the
  functional surface; master's identity-preserving micro-optimizations
  to `updateMessage` are a follow-up.

CI / repo
- Path/draft gating job (`gate`) added to `legacy_version_analyze`,
  `check_db_entities`, and `stream_flutter_workflow` (#2669).
- Flutter 3.44 fixes (#2667), pana / build cleanups (#2656),
  local-setup CI fixes (#2650).
- `melos.yaml`: kept v10's higher floors; added `firebase_crashlytics`
  (master's #2665); dropped `sentry_flutter` (per master).

Notes / follow-ups
- `sample_app/`: v10's redesigned app retained. The Sentry → Firebase
  Crashlytics migration (#2665) applies to master's pre-redesign sample
  app and was not ported here; left for a separate pass.
- `channel_test.dart` `updateMessage quoted-rewrite > does not rewrite
  quotes when an existing quoted target is updated without being
  deleted` is marked `skip:` — v10's `_updateMessages` reconstructs the
  channel list via `_mergeMessagesIntoExisting`, so identity is not
  preserved on non-deletion edits. Functional behavior matches master.
- `goldens/`: deleted-on-v10 goldens kept deleted; modified-on-both
  goldens kept at v10's bytes (the redesigned UI is the source of
  truth).
- `stream_message_composer.dart` had `SizeTransition(alignment:)` which
  was never a valid parameter — switched to `axisAlignment: -1` (the
  Flutter API the v10 author intended).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
xsahil03x added a commit that referenced this pull request May 20, 2026
Brings master's QA / security / perf work into the v10 design-refresh
branch.

Highlights of what landed in v10 from master:

LLC (`stream_chat`)
- `Client.queryDrafts` now forwards `filter` (#2647).
- `Client.queryChannels` coalesces concurrent identical queries via the
  new `InFlightCache<K, V>` (#2652).
- `SortedListX` / `ListX` extensions added in `list_extensions.dart`;
  duplicate-keyed inputs are tolerated by `merge` (#2660). v10's
  `IterableMergeExtension.merge` / `.mergeFrom` are kept — `SortedListX`
  is on `List` and routes there for the new perf paths; the old
  extension still serves `Iterable<T>` callers in v10.
- `ChannelClientState._checkExpiredAttachmentMessages` removed (#2653);
  v10's `StreamImageCDN.cacheKey` already keeps the image cache valid
  across signed-URL rotations.
- `ChannelClientState.updateChannelState` now identity-short-circuits
  when `updatedState.messages` is null or the same reference, so
  downstream `.distinct()` listeners can skip rebuilds.
- Reaction listeners now dispatch via `_findMessage` (parentId-aware)
  while keeping v10's `addMyReaction` / `deleteMyReaction` semantics.

`stream_chat_flutter_core`
- `BetterStreamBuilder` correctness fixes: mounted guard, error reporting
  via `FlutterError.reportError`, identity-equal emission gating (#2651).
- `MessageListCore` caches its `messagesStream` / `_initialMessages` as
  fields instead of recomputing in `build()` (#2651). `defaultMessageFilter`
  takes an optional `currentUserId`.
- `StreamChatCore` debounces connectivity events to 3 s (#2652).

`stream_chat_flutter`
- `scrollable_positioned_list/`: master version taken in full. Bounded
  `_keyToIndexMap`, `isScrolling` / `isScrollingListenable`,
  `itemKeyBuilder` anchor preservation, fit-anchor fallback in
  `UnboundedRenderViewport`, sensible defaults on `scrollTo` (#2651).
- `tld.dart` removed (#2654); `StreamMessageComposer` relaxed its URL
  regex from `[a-z]{2,4}` to `[a-z]{2,}` and dropped the `isValidTLD`
  filter at both call sites.
- `StreamMessageListView` and `separated_reorderable_list_view`:
  v10's design-refresh version retained. v10 already covers the
  functional surface; master's identity-preserving micro-optimizations
  to `updateMessage` are a follow-up.

CI / repo
- Path/draft gating job (`gate`) added to `legacy_version_analyze`,
  `check_db_entities`, and `stream_flutter_workflow` (#2669).
- Flutter 3.44 fixes (#2667), pana / build cleanups (#2656),
  local-setup CI fixes (#2650).
- `melos.yaml`: kept v10's higher floors; added `firebase_crashlytics`
  (master's #2665); dropped `sentry_flutter` (per master).

Notes / follow-ups
- `sample_app/`: v10's redesigned app retained. The Sentry → Firebase
  Crashlytics migration (#2665) applies to master's pre-redesign sample
  app and was not ported here; left for a separate pass.
- `channel_test.dart` `updateMessage quoted-rewrite > does not rewrite
  quotes when an existing quoted target is updated without being
  deleted` is marked `skip:` — v10's `_updateMessages` reconstructs the
  channel list via `_mergeMessagesIntoExisting`, so identity is not
  preserved on non-deletion edits. Functional behavior matches master.
- `goldens/`: deleted-on-v10 goldens kept deleted; modified-on-both
  goldens kept at v10's bytes (the redesigned UI is the source of
  truth).
- `stream_message_composer.dart` had `SizeTransition(alignment:)` which
  was never a valid parameter — switched to `axisAlignment: -1` (the
  Flutter API the v10 author intended).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
xsahil03x added a commit that referenced this pull request May 20, 2026
Brings master's QA / security / perf work into the v10 design-refresh
branch.

Highlights of what landed in v10 from master:

LLC (`stream_chat`)
- `Client.queryDrafts` now forwards `filter` (#2647).
- `Client.queryChannels` coalesces concurrent identical queries via the
  new `InFlightCache<K, V>` (#2652).
- `SortedListX` / `ListX` extensions added in `list_extensions.dart`;
  duplicate-keyed inputs are tolerated by `merge` (#2660). v10's
  `IterableMergeExtension.merge` / `.mergeFrom` are kept — `SortedListX`
  is on `List` and routes there for the new perf paths; the old
  extension still serves `Iterable<T>` callers in v10.
- `ChannelClientState._checkExpiredAttachmentMessages` removed (#2653);
  v10's `StreamImageCDN.cacheKey` already keeps the image cache valid
  across signed-URL rotations.
- `ChannelClientState.updateChannelState` now identity-short-circuits
  when `updatedState.messages` is null or the same reference, so
  downstream `.distinct()` listeners can skip rebuilds.
- Reaction listeners now dispatch via `_findMessage` (parentId-aware)
  while keeping v10's `addMyReaction` / `deleteMyReaction` semantics.

`stream_chat_flutter_core`
- `BetterStreamBuilder` correctness fixes: mounted guard, error reporting
  via `FlutterError.reportError`, identity-equal emission gating (#2651).
- `MessageListCore` caches its `messagesStream` / `_initialMessages` as
  fields instead of recomputing in `build()` (#2651). `defaultMessageFilter`
  takes an optional `currentUserId`.
- `StreamChatCore` debounces connectivity events to 3 s (#2652).

`stream_chat_flutter`
- `scrollable_positioned_list/`: master version taken in full. Bounded
  `_keyToIndexMap`, `isScrolling` / `isScrollingListenable`,
  `itemKeyBuilder` anchor preservation, fit-anchor fallback in
  `UnboundedRenderViewport`, sensible defaults on `scrollTo` (#2651).
- `tld.dart` removed (#2654); `StreamMessageComposer` relaxed its URL
  regex from `[a-z]{2,4}` to `[a-z]{2,}` and dropped the `isValidTLD`
  filter at both call sites.
- `StreamMessageListView` and `separated_reorderable_list_view`:
  v10's design-refresh version retained. v10 already covers the
  functional surface; master's identity-preserving micro-optimizations
  to `updateMessage` are a follow-up.

CI / repo
- Path/draft gating job (`gate`) added to `legacy_version_analyze`,
  `check_db_entities`, and `stream_flutter_workflow` (#2669).
- Flutter 3.44 fixes (#2667), pana / build cleanups (#2656),
  local-setup CI fixes (#2650).
- `melos.yaml`: kept v10's higher floors; added `firebase_crashlytics`
  (master's #2665); dropped `sentry_flutter` (per master).

Notes / follow-ups
- `sample_app/`: v10's redesigned app retained. The Sentry → Firebase
  Crashlytics migration (#2665) applies to master's pre-redesign sample
  app and was not ported here; left for a separate pass.
- `channel_test.dart` `updateMessage quoted-rewrite > does not rewrite
  quotes when an existing quoted target is updated without being
  deleted` is marked `skip:` — v10's `_updateMessages` reconstructs the
  channel list via `_mergeMessagesIntoExisting`, so identity is not
  preserved on non-deletion edits. Functional behavior matches master.
- `goldens/`: deleted-on-v10 goldens kept deleted; modified-on-both
  goldens kept at v10's bytes (the redesigned UI is the source of
  truth).
- `stream_message_composer.dart` had `SizeTransition(alignment:)` which
  was never a valid parameter — switched to `axisAlignment: -1` (the
  Flutter API the v10 author intended).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
xsahil03x added a commit that referenced this pull request May 20, 2026
Brings master's QA / security / perf work into the v10 design-refresh
branch.

Highlights of what landed in v10 from master:

LLC (`stream_chat`)
- `Client.queryDrafts` now forwards `filter` (#2647).
- `Client.queryChannels` coalesces concurrent identical queries via the
  new `InFlightCache<K, V>` (#2652).
- `SortedListX` / `ListX` extensions added in `list_extensions.dart`;
  duplicate-keyed inputs are tolerated by `merge` (#2660). v10's
  `IterableMergeExtension.merge` / `.mergeFrom` are kept — `SortedListX`
  is on `List` and routes there for the new perf paths; the old
  extension still serves `Iterable<T>` callers in v10.
- `ChannelClientState._checkExpiredAttachmentMessages` removed (#2653);
  v10's `StreamImageCDN.cacheKey` already keeps the image cache valid
  across signed-URL rotations.
- `ChannelClientState.updateChannelState` now identity-short-circuits
  when `updatedState.messages` is null or the same reference, so
  downstream `.distinct()` listeners can skip rebuilds.
- Reaction listeners now dispatch via `_findMessage` (parentId-aware)
  while keeping v10's `addMyReaction` / `deleteMyReaction` semantics.

`stream_chat_flutter_core`
- `BetterStreamBuilder` correctness fixes: mounted guard, error reporting
  via `FlutterError.reportError`, identity-equal emission gating (#2651).
- `MessageListCore` caches its `messagesStream` / `_initialMessages` as
  fields instead of recomputing in `build()` (#2651). `defaultMessageFilter`
  takes an optional `currentUserId`.
- `StreamChatCore` debounces connectivity events to 3 s (#2652).

`stream_chat_flutter`
- `scrollable_positioned_list/`: master version taken in full. Bounded
  `_keyToIndexMap`, `isScrolling` / `isScrollingListenable`,
  `itemKeyBuilder` anchor preservation, fit-anchor fallback in
  `UnboundedRenderViewport`, sensible defaults on `scrollTo` (#2651).
- `tld.dart` removed (#2654); `StreamMessageComposer` relaxed its URL
  regex from `[a-z]{2,4}` to `[a-z]{2,}` and dropped the `isValidTLD`
  filter at both call sites.
- `StreamMessageListView` and `separated_reorderable_list_view`:
  v10's design-refresh version retained. v10 already covers the
  functional surface; master's identity-preserving micro-optimizations
  to `updateMessage` are a follow-up.

CI / repo
- Path/draft gating job (`gate`) added to `legacy_version_analyze`,
  `check_db_entities`, and `stream_flutter_workflow` (#2669).
- Flutter 3.44 fixes (#2667), pana / build cleanups (#2656),
  local-setup CI fixes (#2650).
- `melos.yaml`: kept v10's higher floors; added `firebase_crashlytics`
  (master's #2665); dropped `sentry_flutter` (per master).

Notes / follow-ups
- `sample_app/`: v10's redesigned app retained. The Sentry → Firebase
  Crashlytics migration (#2665) applies to master's pre-redesign sample
  app and was not ported here; left for a separate pass.
- `channel_test.dart` `updateMessage quoted-rewrite > does not rewrite
  quotes when an existing quoted target is updated without being
  deleted` is marked `skip:` — v10's `_updateMessages` reconstructs the
  channel list via `_mergeMessagesIntoExisting`, so identity is not
  preserved on non-deletion edits. Functional behavior matches master.
- `goldens/`: deleted-on-v10 goldens kept deleted; modified-on-both
  goldens kept at v10's bytes (the redesigned UI is the source of
  truth).
- `stream_message_composer.dart` had `SizeTransition(alignment:)` which
  was never a valid parameter — switched to `axisAlignment: -1` (the
  Flutter API the v10 author intended).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

3 participants