Skip to content

Fix adding and removing channels from channel list query when channel updates#3983

Merged
laevandus merged 2 commits intodevelopfrom
fix/channel-list-update-on-channel-updated-event
Feb 25, 2026
Merged

Fix adding and removing channels from channel list query when channel updates#3983
laevandus merged 2 commits intodevelopfrom
fix/channel-list-update-on-channel-updated-event

Conversation

@laevandus
Copy link
Contributor

@laevandus laevandus commented Feb 25, 2026

🔗 Issue Links

Fixes: IOS-1449

🎯 Goal

Fix adding and removing channels from channel list query when channel updated web-socket event is received

📝 Summary

  • Link channels on channel updated event so that they will appear on channel list

🛠 Implementation

🎨 Showcase

output_after.mp4

🧪 Manual Testing Notes

  1. Open 2 simulators with different users
  2. User A selects "Premium Tagged Channels" from the debug menu
  3. User B toggles premium state of different channels and A observers that channels are added and removed appropriately

☑️ Contributor Checklist

  • I have signed the Stream CLA (required)
  • This change should be manually QAed
  • Changelog is updated with client-facing changes
  • Changelog is updated with new localization keys
  • New code is covered by unit tests
  • Documentation has been updated in the docs-content repo

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed channel list updates to properly reflect additions and removals when channel modifications are received over the connection.
  • New Features

    • Channel filter tags can now be toggled between premium and non-premium states in the demo app.
  • Tests

    • Added tests verifying correct channel linking behavior when update events occur and channels match current filters.

@laevandus laevandus requested a review from a team as a code owner February 25, 2026 09:00
@laevandus laevandus added 🌐 SDK: StreamChat (LLC) Tasks related to the StreamChat LLC SDK 🤞 Ready For QA A PR that is Ready for QA labels Feb 25, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

Updates the channel list query handling to link channels that pass the current filter after receiving a channel update event. When a ChannelUpdatedEvent occurs, the system now chains an unlink operation followed by a link operation via completion callback. Tests verify this behavior, and the demo app adds a toggle action for premium tag management.

Changes

Cohort / File(s) Summary
Core Channel List Logic
Sources/StreamChat/Workers/ChannelListLinker.swift
Modified ChannelUpdatedEvent observer to chain unlinkChannelIfNeeded with a completion block that invokes linkChannelIfNeeded. Updated unlinkChannelIfNeeded method signature to accept an optional completion closure that executes after unlinking completes.
Test Coverage
Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift
Added tests verifying linking behavior when ChannelUpdatedEvent occurs for channels matching the current filter. Tests assert that link completion is invoked and channel watcher handler is engaged.
Demo Application
DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift
Replaced fixed "Add Premium Tag" action with "Toggle Premium Tag" logic that checks for existing "premium" tag and updates channel to either ["premium"] or ["non-premium"] accordingly.
Documentation
CHANGELOG.md
Added entry documenting that channel additions/removals in ChannelListQuery are now updated when channel update events are received over WebSocket.

Sequence Diagram(s)

sequenceDiagram
    participant WebSocket as WebSocket Event
    participant Linker as ChannelListLinker
    participant Query as ChannelListQuery
    participant Channel as ChatChannel

    WebSocket->>Linker: ChannelUpdatedEvent received
    Linker->>Linker: unlinkChannelIfNeeded(channel)
    rect rgba(100, 150, 200, 0.5)
        Note over Linker,Query: Unlink operation
        Linker->>Query: Remove channel from query
        Query-->>Linker: Unlink complete (completion)
    end
    Linker->>Linker: linkChannelIfNeeded(channel)<br/>(via completion callback)
    rect rgba(100, 200, 150, 0.5)
        Note over Linker,Query: Link operation if filter matches
        Linker->>Channel: Check if passes filter
        Channel-->>Linker: Filter result
        alt Filter matches
            Linker->>Query: Add channel to query
            Query-->>Linker: Link complete
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A hop through the channels, a dance most divine,
When updates arrive, the links now align,
First unlink with grace, then link anew,
Completion callbacks see the whole journey through! 🌟

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing the addition and removal of channels from channel list queries when channel updates occur, which is the primary objective of this pull request.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/channel-list-update-on-channel-updated-event

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.

@github-actions
Copy link

1 Warning
⚠️ The changes should be manually QAed before the Pull Request will be merged

Generated by 🚫 Danger

@github-actions
Copy link

Public Interface

🚀 No changes affecting the public interface.

Copy link

@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: 2

🧹 Nitpick comments (2)
Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift (1)

571-573: Add an explicit no-unlink assertion for this matching-filter path.

You already validate linking on Line 571-Line 572; adding XCTAssertEqual(env.channelListUpdater?.unlink_callCount, 0) makes this regression test stricter.

Based on learnings: Prioritize backwards compatibility, API stability, and high test coverage when changing code in the Stream iOS Chat SDK.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift`
around lines 571 - 573, Add an explicit no-unlink assertion to the
matching-filter test by asserting env.channelListUpdater?.unlink_callCount is 0
immediately after the existing link and watch assertions; specifically, in
ChannelListController_Tests where you call
XCTAssertEqual(env.channelListUpdater?.link_callCount, 1) and
XCTAssertEqual(env.channelWatcherHandler?.attemptToWatch_callCount, 1), add
XCTAssertEqual(env.channelListUpdater?.unlink_callCount, 0) to ensure the
matching-filter path does not trigger an unlink.
Sources/StreamChat/Workers/ChannelListLinker.swift (1)

59-63: Update the header docs to match the new ChannelUpdatedEvent behavior.

Line 12-Line 13 still says updated channels are only removed, but Line 61 now links matching channels after update handling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/StreamChat/Workers/ChannelListLinker.swift` around lines 59 - 63,
Update the header documentation for ChannelUpdatedEvent in
ChannelListLinker.swift to reflect the new behavior: it no longer only removes
updated channels but may also re-link them after handling; specifically change
the lines that state "updated channels are only removed" (around the
ChannelUpdatedEvent description) to describe that the callback now calls
unlinkChannelIfNeeded(event.channel) and then
linkChannelIfNeeded(event.channel), so updated channels can be removed and
conditionally re-linked based on matching logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift`:
- Around line 508-510: The toggle currently overwrites all filterTags via
channelController.partialChannelUpdate(filterTags: ["premium"/"non-premium"])
which drops unrelated tags; instead read the current tags from
channelController.channel?.filterTags, create a new set/array that preserves all
existing tags but adds "premium" when toggling on or removes "premium" when
toggling off (do not replace other tags), then call
channelController.partialChannelUpdate(filterTags: newTags) { error in ... } so
only the premium-related tag is changed while other filterTags remain intact.

In `@Sources/StreamChat/Workers/ChannelListLinker.swift`:
- Around line 122-134: The spy ChannelListUpdater_Spy currently increments
unlink_callCount but does not invoke the unlink completion, causing callers of
ChannelListUpdater.unlink(channel:with:completion:) (used by
ChannelListLinker.unlinkChannelIfNeeded) to never continue the post-unlink flow;
update the spy's unlink(channel:with:completion:) implementation to call the
provided completion (e.g., completion?(nil)) or store the completion for
test-controlled invocation so tests observe the same behavior as the production
unlink implementation and allow the chained completion in
ChannelListLinker.unlinkChannelIfNeeded to run.

---

Nitpick comments:
In `@Sources/StreamChat/Workers/ChannelListLinker.swift`:
- Around line 59-63: Update the header documentation for ChannelUpdatedEvent in
ChannelListLinker.swift to reflect the new behavior: it no longer only removes
updated channels but may also re-link them after handling; specifically change
the lines that state "updated channels are only removed" (around the
ChannelUpdatedEvent description) to describe that the callback now calls
unlinkChannelIfNeeded(event.channel) and then
linkChannelIfNeeded(event.channel), so updated channels can be removed and
conditionally re-linked based on matching logic.

In
`@Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift`:
- Around line 571-573: Add an explicit no-unlink assertion to the
matching-filter test by asserting env.channelListUpdater?.unlink_callCount is 0
immediately after the existing link and watch assertions; specifically, in
ChannelListController_Tests where you call
XCTAssertEqual(env.channelListUpdater?.link_callCount, 1) and
XCTAssertEqual(env.channelWatcherHandler?.attemptToWatch_callCount, 1), add
XCTAssertEqual(env.channelListUpdater?.unlink_callCount, 0) to ensure the
matching-filter path does not trigger an unlink.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5fd4430 and 4def182.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift
  • Sources/StreamChat/Workers/ChannelListLinker.swift
  • Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift

Comment on lines +508 to +510
.init(title: "Toggle Premium Tag", isEnabled: canUpdateChannel, handler: { [unowned self] _ in
let hasPremium = channelController.channel?.filterTags.contains("premium") ?? false
channelController.partialChannelUpdate(filterTags: hasPremium ? ["non-premium"] : ["premium"]) { error in
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid overwriting unrelated filterTags while toggling premium state.

On Line 510, replacing tags with ["premium"] / ["non-premium"] drops any existing non-premium tags. Toggle only the premium-related tags and preserve the rest.

💡 Suggested fix
-                let hasPremium = channelController.channel?.filterTags.contains("premium") ?? false
-                channelController.partialChannelUpdate(filterTags: hasPremium ? ["non-premium"] : ["premium"]) { error in
+                var updatedTags = Set(channelController.channel?.filterTags ?? [])
+                if updatedTags.contains("premium") {
+                    updatedTags.remove("premium")
+                    updatedTags.insert("non-premium")
+                } else {
+                    updatedTags.remove("non-premium")
+                    updatedTags.insert("premium")
+                }
+                channelController.partialChannelUpdate(filterTags: updatedTags) { error in
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@DemoApp/StreamChat/Components/DemoChatChannelListRouter.swift` around lines
508 - 510, The toggle currently overwrites all filterTags via
channelController.partialChannelUpdate(filterTags: ["premium"/"non-premium"])
which drops unrelated tags; instead read the current tags from
channelController.channel?.filterTags, create a new set/array that preserves all
existing tags but adds "premium" when toggling on or removes "premium" when
toggling off (do not replace other tags), then call
channelController.partialChannelUpdate(filterTags: newTags) { error in ... } so
only the premium-related tag is changed while other filterTags remain intact.

Comment on lines +122 to +134
private func unlinkChannelIfNeeded(_ channel: ChatChannel, completion: (() -> Void)? = nil) {
guard !shouldChannelBelongToCurrentQuery(channel) else {
completion?()
return
}
isInChannelList(channel) { [worker, query] exists, _ in
guard exists else { return }
worker.unlink(channel: channel, with: query)
guard exists else {
completion?()
return
}
worker.unlink(channel: channel, with: query) { _ in
completion?()
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== 1. Locate ChannelListUpdater_Spy.swift ==="
find . -name "ChannelListUpdater_Spy.swift" -type f 2>/dev/null | head -5

echo
echo "=== 2. Contents of ChannelListUpdater_Spy.swift ==="
fd "ChannelListUpdater_Spy.swift" --type f -x cat -n {}

echo
echo "=== 3. Search for unlink method in spy implementations ==="
rg -n "func unlink" --type swift -B2 -A10

echo
echo "=== 4. Check ChannelListUpdater production implementation ==="
fd "ChannelListUpdater.swift" --type f --not-path "*/Test*" --not-path "*Spy*" -x cat -n {}

Repository: GetStream/stream-chat-swift

Length of output: 8394


Spy unlink method must invoke completion callback to align with production contract.

The spy implementation (lines 87-93 in ChannelListUpdater_Spy.swift) only increments unlink_callCount without invoking the completion callback, while the production implementation (lines 159-168 in ChannelListUpdater.swift) invokes completion?(error) on line 166. The new sequencing in ChannelListLinker.unlinkChannelIfNeeded at lines 132-134 chains a follow-up completion handler that depends on this callback being invoked. Without the spy invoking it, tests will skip the post-unlink completion flow and diverge from production behavior.

Update the spy to invoke completion?(nil) or capture it for manual control, consistent with test expectations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/StreamChat/Workers/ChannelListLinker.swift` around lines 122 - 134,
The spy ChannelListUpdater_Spy currently increments unlink_callCount but does
not invoke the unlink completion, causing callers of
ChannelListUpdater.unlink(channel:with:completion:) (used by
ChannelListLinker.unlinkChannelIfNeeded) to never continue the post-unlink flow;
update the spy's unlink(channel:with:completion:) implementation to call the
provided completion (e.g., completion?(nil)) or store the completion for
test-controlled invocation so tests observe the same behavior as the production
unlink implementation and allow the chained completion in
ChannelListLinker.unlinkChannelIfNeeded to run.

@Stream-SDK-Bot
Copy link
Collaborator

SDK Performance

target metric benchmark branch performance status
MessageList Hitches total duration 10 ms 21.69 ms -116.9% 🔽 🔴
Duration 2.6 s 2.55 s 1.92% 🔼 🟢
Hitch time ratio 4 ms per s 8.52 ms per s -113.0% 🔽 🔴
Frame rate 75 fps 79.31 fps 5.75% 🔼 🟢
Number of hitches 1 1.8 -80.0% 🔽 🔴

@Stream-SDK-Bot
Copy link
Collaborator

SDK Size

title develop branch diff status
StreamChat 8.58 MB 8.58 MB 0 KB 🟢
StreamChatUI 4.91 MB 4.91 MB 0 KB 🟢

@Stream-SDK-Bot
Copy link
Collaborator

StreamChat XCSize

Object Diff (bytes)
ChannelListLinker.o +1260

@sonarqubecloud
Copy link

@testableapple testableapple added 🧪 QAing 🟢 QAed A PR that was QAed and removed 🤞 Ready For QA A PR that is Ready for QA 🧪 QAing labels Feb 25, 2026
Copy link
Member

@nuno-vieira nuno-vieira left a comment

Choose a reason for hiding this comment

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

LGTM! ✅

@laevandus laevandus merged commit 6b36961 into develop Feb 25, 2026
14 checks passed
@laevandus laevandus deleted the fix/channel-list-update-on-channel-updated-event branch February 25, 2026 17:45
@Stream-SDK-Bot Stream-SDK-Bot mentioned this pull request Feb 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🟢 QAed A PR that was QAed 🌐 SDK: StreamChat (LLC) Tasks related to the StreamChat LLC SDK

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants