Skip to content

fix: bug bashing ChannelList + Channel (#2474, #2441, #2393)#3227

Open
oliverlaz wants to merge 8 commits into
masterfrom
fix/channellist-bugbash-tier1
Open

fix: bug bashing ChannelList + Channel (#2474, #2441, #2393)#3227
oliverlaz wants to merge 8 commits into
masterfrom
fix/channellist-bugbash-tier1

Conversation

@oliverlaz

@oliverlaz oliverlaz commented Jun 29, 2026

Copy link
Copy Markdown
Member

🎯 Goal

Fixes three Tier‑1 issues from the bug‑bash triage. Each is an independent fix kept as its own self‑contained commit (with tests):

#2599 — root cause fixed upstream; this PR carries the regression test. An earlier stopWatching-on-removal attempt here was implemented and then reverted: it did not make removal durable. channel.stopWatching() does not evict the channel from client.activeChannels, so a reconnect (recoverState re-watches every active channel) or a queued/in-flight message.new re-added the removed channel — the exact reported symptom. The real fix evicts activeChannels[cid] on notification.removed_from_channel (mirroring the existing channel.deleted handling) and is now merged in GetStream/stream-chat-js#1788. This PR includes the regression test for it (ChannelList.test.tsx), but that test is red until a stream-chat release containing #1788 is published and the dependency here is bumped to it. That bump is the only remaining step to close #2599, and the failing check on that single test is expected until then.

🛠 Implementation details

#2474fix(ChannelListItem): guard muteStatus against an uninitialized/disconnected channel

  • useIsChannelMuted reads the mute status through a helper that calls channel.muteStatus() only when the channel is initialized and not disconnected, falling back to a not‑muted status otherwise. muteStatus() throws on an uninitialized channel (#2474) and also after the client disconnects — via channel.getClient() — even though the channel stays initialized (the bug: ChannelInner can call methods on a disconnected channel, causing uncaught errors #2393 failure class on the channel‑list path).
  • The mute‑update subscription now depends on [channel, client] and cleans up via the unsubscribe handle returned by client.on(). It previously depended on [muted], so it re‑subscribed on its own output and could capture a stale channel.

#2441fix(ChannelList): do not query unfiltered channels on notification.message_new

  • Moved the allowNewMessagesFromUnfilteredChannels guard before the getChannel() call in handleNotificationMessageNew, so the flag actually suppresses the per‑event watch() instead of querying first and throwing the result away. Behaviour with the flag enabled is unchanged.

#2393fix(Channel): guard channel methods against a disconnected client

  • Guarded channel.lastRead() in useCreateChannelStateContext with !channel.disconnected so a disconnect while the channel is mounted does not crash the render.
  • loadMore now early‑returns when channel.disconnected, mirroring the existing markRead guard, to avoid a doomed pagination query.
  • Note: the shared‑client race cannot be fully eliminated (the existing try/catch backstops on the async paths remain); this removes the uncaught render‑time crash, which is the part with no safety net.

Tests — added test‑first (each test was confirmed to fail before the fix):

  • useIsChannelMuted does not throw on an uninitialized channel, nor on an initialized‑but‑disconnected channel.
  • notification.message_new does not call watch() when allowNewMessagesFromUnfilteredChannels={false}.
  • rendering does not crash when the client disconnects while the channel is mounted.
  • loadMore does not query when the client is disconnected.
  • bug:channelList doesn't stop watching channels that we are removed from #2599 (gated): notification.removed_from_channel evicts the channel from client.activeChannels (so reconnect cannot re‑watch it). This test is red until the stream-chat bump described above — it was verified red→green during development by portaling stream-chat to the stream-chat-js#1788 branch.

Verification: yarn test (ChannelList, ChannelListItem, Channel, MessageList, Thread), yarn types, and ESLint (--max-warnings 0) clean for #2474, #2441 and #2393, which were additionally confirmed in a real browser against the example app. The single #2599 test is expected to fail CI until the stream-chat dependency is bumped to the release containing GetStream/stream-chat-js#1788.

🎨 UI Changes

None — these are crash / correctness / rate‑limit fixes in ChannelList and Channel event handling, with no visual changes.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed message-new handling when unfiltered channels aren’t allowed so unlisted/unqueried channels aren’t watched or added to the list.
    • Made mute status lookups safe when a channel isn’t initialized or is disconnected.
    • Prevented pagination and last-read handling from running while the client/channel is disconnected to avoid crashes.
  • Tests

    • Added coverage for message-new filtering with unfiltered channels disabled.
    • Expanded mute hook tests for uninitialized and disconnected cases.
    • Added disconnected-client scenarios for rendering and pagination.

useIsChannelMuted called channel.muteStatus() during render, which throws
"hasn't been initialized yet" when the channel has not been watched. The
ChannelList can briefly hold such a channel (e.g. a message.new for a channel
that has not been watched yet), and the throw crashed the whole app.

Guard the call behind channel.initialized and fall back to a not-muted status.

Closes #2474
…ssage_new

handleNotificationMessageNew called getChannel() (channel.watch()) before
checking allowNewMessagesFromUnfilteredChannels, so a queryChannel was issued
for every notification.message_new even when the result was discarded. When many
such events arrive at once (e.g. a bulk campaign) this exhausted the rate limit.

Move the guard ahead of the query so the flag actually suppresses the watch().

Closes #2441
Removing a channel from the list did not stop watching it, so the client kept
receiving its events and a later message.new / notification.message_new re-added
it to the list until a hard refresh.

notification.removed_from_channel now calls channel.stopWatching(), and a new
member.removed handler removes the channel and stops watching it when the current
user is the removed member.

Closes #2599
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

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

Adds a ChannelList early return for unfiltered new-message events, guards mute-status reads before channel initialization, and prevents Channel pagination and state reads after disconnect. Adds tests for the new ChannelList and Channel behaviors.

Changes

ChannelList message-new guard

Layer / File(s) Summary
Message-new early return
src/components/ChannelList/hooks/useChannelListShape.ts, src/components/ChannelList/__tests__/ChannelList.test.tsx
handleNotificationMessageNew returns early when unfiltered new messages are disabled, and a ChannelList test asserts the cached channel is not watched or rendered for an unlisted message event.

Channel mute and disconnect guards

Layer / File(s) Summary
Mute-status guard
src/components/ChannelListItem/hooks/useIsChannelMuted.ts, src/components/ChannelListItem/hooks/__tests__/useIsChannelMuted.test.tsx
useIsChannelMuted now derives a safe default mute status before initialization and uses that guarded value for state setup and event updates, with tests covering uninitialized and initialized channels.
Disconnected-channel guards
src/components/Channel/Channel.tsx, src/components/Channel/hooks/useCreateChannelStateContext.ts, src/components/Channel/__tests__/Channel.test.tsx
Channel.loadMore bails out when the channel is disconnected, useCreateChannelStateContext skips lastRead() in that state, and Channel tests cover disconnected-client rendering and pagination.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • arnautov-anton

Poem

🐇 I hopped through lists with ears held high,
No unready channels slipping by.
If muted roots or disconnects appear,
The rabbit code stays calm and clear.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is clear and relevant, summarizing the ChannelList/Channel bug-fix set without being misleading.
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 The PR description follows the required template and includes complete goal, implementation, and UI sections.
✨ 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/channellist-bugbash-tier1

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.

@github-actions

github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown

Size Change: +598 B (+0.08%)

Total Size: 798 kB

📦 View Changed
Filename Size Change
dist/cjs/channel-detail.js 22.9 kB -3 B (-0.01%)
dist/cjs/index.js 259 kB -15 B (-0.01%)
dist/cjs/ReactPlayerWrapper.js 544 B -3 B (-0.55%)
dist/cjs/useChannelHeaderOnlineStatus.js 38 kB +297 B (+0.79%)
dist/cjs/useNotificationApi.js 52.8 kB +11 B (+0.02%)
dist/es/channel-detail.mjs 22.6 kB +2 B (+0.01%)
dist/es/index.mjs 256 kB -8 B (0%)
dist/es/useChannelHeaderOnlineStatus.mjs 37.4 kB +294 B (+0.79%)
dist/es/useNotificationApi.mjs 51.5 kB +23 B (+0.04%)
ℹ️ View Unchanged
Filename Size
dist/cjs/audioProcessing.js 1.74 kB
dist/cjs/emojis.js 2.56 kB
dist/cjs/mp3-encoder.js 814 B
dist/cjs/useMessageComposerController.js 1.01 kB
dist/css/channel-detail.css 2.84 kB
dist/css/emoji-picker.css 178 B
dist/css/emoji-replacement.css 456 B
dist/css/index.css 41.3 kB
dist/es/audioProcessing.mjs 1.65 kB
dist/es/emojis.mjs 2.48 kB
dist/es/mp3-encoder.mjs 768 B
dist/es/ReactPlayerWrapper.mjs 485 B
dist/es/useMessageComposerController.mjs 937 B

compressed-size-action

Comment thread src/components/ChannelListItem/hooks/useIsChannelMuted.ts
@codecov

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.20%. Comparing base (a768e30) to head (10b96a3).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3227      +/-   ##
==========================================
- Coverage   84.20%   84.20%   -0.01%     
==========================================
  Files         485      485              
  Lines       14867    14872       +5     
  Branches     4704     4707       +3     
==========================================
+ Hits        12519    12523       +4     
- Misses       2348     2349       +1     

☔ View full report in Codecov by Harness.
📢 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

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 `@src/components/ChannelListItem/hooks/useIsChannelMuted.ts`:
- Around line 21-24: The local muted state in useIsChannelMuted is only
initialized once and then updated on notification.channel_mutes_updated, so it
can stay stale if the channel mounts before watch()/query() finishes. Add a
resync path in useIsChannelMuted that re-reads getMuteStatus(channel) when the
channel initializes or becomes available, and keep the existing event-driven
update intact. Use the useEffect and getMuteStatus symbols in this hook to
locate the fix, and add a regression test for the mount-before-watch
initialization case.
🪄 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: 38a4028a-0f91-4f43-98cc-e2cc77805bbe

📥 Commits

Reviewing files that changed from the base of the PR and between a768e30 and 3bef4e1.

📒 Files selected for processing (4)
  • src/components/ChannelList/__tests__/ChannelList.test.tsx
  • src/components/ChannelList/hooks/useChannelListShape.ts
  • src/components/ChannelListItem/hooks/__tests__/useIsChannelMuted.test.tsx
  • src/components/ChannelListItem/hooks/useIsChannelMuted.ts

Comment thread src/components/ChannelListItem/hooks/useIsChannelMuted.ts Outdated
channel.lastRead() runs during render (useCreateChannelStateContext) and throws
"You can't use a channel after client.disconnect() was called" once the shared
client is disconnected - crashing the render, with no error boundary to catch
it. Guard the call with channel.disconnected. Also skip loadMore's pagination
query while the client is disconnected, mirroring the existing markRead guard.

Closes #2393
@oliverlaz oliverlaz changed the title fix(ChannelList): tier-1 bug-bash fixes (#2474, #2441, #2599) fix: tier-1 bug-bash fixes — ChannelList + Channel (#2474, #2441, #2599, #2393) Jun 29, 2026
@oliverlaz oliverlaz changed the title fix: tier-1 bug-bash fixes — ChannelList + Channel (#2474, #2441, #2599, #2393) fix: ChannelList + Channel bug fixes (#2474, #2441, #2599, #2393) Jun 29, 2026
Browser verification showed the stopWatching-based fix does not make removal
durable: channel.stopWatching() does not evict the channel from
client.activeChannels, so a reconnect (recoverState re-watches every active
channel) or a queued/in-flight message.new re-adds the removed channel to the
list - the exact reported symptom.

The correct fix belongs in stream-chat core: evict activeChannels[cid] on
notification.removed_from_channel / member.removed, mirroring the existing
channel.deleted handling. Dropping #2599 from this PR until that core change
lands. #2474, #2441 and #2393 are unaffected.
@oliverlaz oliverlaz changed the title fix: ChannelList + Channel bug fixes (#2474, #2441, #2599, #2393) fix: tier-1 bug-bash fixes — ChannelList + Channel (#2474, #2441, #2393) Jun 30, 2026
@oliverlaz oliverlaz changed the title fix: tier-1 bug-bash fixes — ChannelList + Channel (#2474, #2441, #2393) fix: bug bashing ChannelList + Channel (#2474, #2441, #2393) Jun 30, 2026
The notification.channel_mutes_updated effect depended on [muted], so it
re-subscribed on its own output and could capture a stale channel when the
channel prop changed without the muted value changing. Depend on
[channel, client] instead and clean up via the unsubscribe handle returned
by client.on(), dropping the exhaustive-deps override.
An initialized channel whose client has disconnected still routes
channel.muteStatus() through channel.getClient(), which throws "You can't use
a channel after client.disconnect() was called". Guard channel.disconnected
alongside channel.initialized in getMuteStatus so rendering a ChannelListItem
with a disconnected channel no longer crashes the app (same failure class as
the #2393 Channel-state guard).
…nnels (#2599)

Regression test for #2599: removing the current user from a channel must evict
it from client.activeChannels, otherwise recoverState() re-watches it on the
next reconnect (recoverStateOnReconnect defaults to true) and the list
resurrects it.

The eviction itself lives in stream-chat core (GetStream/stream-chat-js#1788).
This test is red until that fix ships in a stream-chat release and the
dependency here is bumped to it; the bump + green run will accompany closing
#2599.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment