Skip to content

Support predefined filters in ChannelListQuery#4113

Draft
laevandus wants to merge 3 commits into
developfrom
feature/predefined-filters
Draft

Support predefined filters in ChannelListQuery#4113
laevandus wants to merge 3 commits into
developfrom
feature/predefined-filters

Conversation

@laevandus
Copy link
Copy Markdown
Contributor

@laevandus laevandus commented May 26, 2026

🔗 Issue Links

https://linear.app/stream/issue/IOS-1706

🎯 Goal

Let apps reference a server-side predefined channel-list filter by name and have the server resolve its filter/sort templates, instead of constructing the filter client-side.

📝 Summary

  • New ChannelListQuery(predefinedFilter:filterValues:sortValues:) initializer.
  • Server-resolved filter/sort returned on the predefined_filter response are decoded and re-applied locally for Core Data caching.
  • ChatChannelListController.query and ChannelListState.query become internal(set) so the SDK can swap to the resolved query after the first response.
  • Filter decoder now supports multi-key objects as an implicit $and, matching the Stream filter JSON shape.

🛠 Implementation

  • ChannelListQuery+PredefinedFilter.swift decodes persisted filter/sort JSON and re-attaches Core Data wiring (keyPathString, valueMapper, predicateMapper) via ChannelListFilterScope.predefinedFilterKeyMapping. Unknown keys are logged and dropped.
  • ChannelListPayload decodes the new predefined_filter response key. ChannelListUpdater persists the resolved filter+sort onto ChannelListQueryDTO and returns an updated ChannelListQuery to the controller/state layer.
  • ChannelListSortingKey.predefinedSortingKeyMapping provides the same key registry for sort fields.
  • On controller/state init, loadPredefinedFilter(for:) rehydrates the locally cached resolved query so Core Data fetch predicates work before the first server response lands.

🧪 Manual Testing Notes

  • The demo app's channel list picker exposes a "Predefined filter" entry — pick one and verify that the channel list loads, paginates, and updates on websocket events.
  • For a plain (non-predefined) query, behaviour should be unchanged.

New pre-defined filters can be added through the dashboard.

☑️ 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

  • New Features

    • Demo app now includes quick-access filters for messaging and livestream channels
    • SDK now supports server-provided predefined filter configurations for channel lists
  • Documentation

    • Updated CHANGELOG to reflect new predefined filter support

Review Change Stack

laevandus added 2 commits May 26, 2026 15:14
# Conflicts:
#	CHANGELOG.md
#	DemoApp/StreamChat/Components/DemoChatChannelListVC.swift
@laevandus laevandus requested a review from a team as a code owner May 26, 2026 12:24
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

📝 Walkthrough

Walkthrough

This PR adds server-side predefined channel list filter support to the Stream Chat SDK. It introduces mutable queries that resolve server-provided filter/sort JSON at initialization, persist resolved values in Core Data, integrate query mutation through state layers and controllers, and apply cached resolutions during pagination.

Changes

Predefined Channel List Filters

Layer / File(s) Summary
Query & Payload Data Models
Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift, Sources/StreamChat/Query/ChannelListQuery.swift, Sources/StreamChat/Query/Sorting/ChannelListSortingKey.swift, Sources/StreamChat/Utils/Dictionary+Extensions.swift
ChannelListQuery adds predefinedFilter, filterValues, and sortValues properties plus queryHash for deterministic persistence; PredefinedFilterPayload decodes server-provided filter/sort metadata; ChannelListSortingKey registry maps known sort fields.
Filter & Sort JSON Decoding
Sources/StreamChat/Query/ChannelListQuery+PredefinedFilter.swift, Sources/StreamChat/Query/Filter.swift
New utilities decode predefined filter/sort JSON from server, enrich leaf filter nodes with Core Data wiring (keyPath, mapper metadata), and support implicit $eq for short-form leaf filters.
Database Persistence & Core Data
Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/..., Sources/StreamChat/Database/DTOs/ChannelListQueryDTO.swift, Sources/StreamChat/Database/DatabaseSession.swift, Sources/StreamChat/Database/DTOs/ChannelDTO.swift
Core Data model adds sortJSONData; database session API transitions from filter-hash to query-based lookups; ChannelListQueryDTO persists resolved filter/sort with error handling; loadPredefinedFilter() reconstructs queries from cache.
Worker Update Flow & Result Type
Sources/StreamChat/Workers/ChannelListUpdater.swift
ChannelListUpdateResult bundles channels with optional updatedQuery; updater resolves predefined filters during persistence, detects filter/sort changes, and propagates resolved query to all callers; pagination accumulation uses resolved channels.
State Observer Reload & Query Mutation
Sources/StreamChat/StateLayer/ChannelListState+Observer.swift, Sources/StreamChat/StateLayer/ChannelListState.swift
ChannelListState.Observer stores mutable query and handler; new reload(with:) rebuilds database observer when query changes; ChannelListState normalizes input query via predefined filter resolution and adds setQuery() for runtime mutation.
ChannelList Pagination & Loading
Sources/StreamChat/StateLayer/ChannelList.swift
ChannelList derives pagination from state.query rather than storing query separately; get(), loadChannels(), loadMoreChannels(), and refreshLoadedChannels() integrate with predefined filter resolution via updated completion types.
Controller Query Mutation & Observer Rebuild
Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift
ChatChannelListController.query becomes mutable; synchronize and loadNextChannels accept Result<ChannelListUpdateResult, Error> completion; controller resolves predefined filters on init, rebuilds observer when updatedQuery differs, and marks delivered channels.
Demo App UI & Logging
DemoApp/StreamChat/Components/DemoChatChannelListVC.swift, Sources/StreamChat/Repositories/SyncOperations.swift, CHANGELOG.md
Demo adds predefined messaging and livestream channel filters with action sheet UI; sync operation logging updated to generic text; changelog documents new API.
Test Mocks, Spies & Configuration
TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift, TestTools/StreamChatTestTools/SpyPattern/Spy/ChannelListUpdater_Spy.swift, StreamChat.xcodeproj/project.pbxproj
Mocks support new loadPredefinedFilter() and query-based lookups; spy maps predefined resolution into result type; Xcode project updated with test file paths.
Test Coverage
Tests/StreamChatTests/**/*
Comprehensive tests validate payload decoding, database persistence, Core Data reconstruction, controller sync behavior, pagination with resolved filters, filter/sort JSON decoding, query hash stability, and implicit operator decoding.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • martinmitrevski

Poem

🐰 Filters bundled from the cloud,
Queries mutable and proud,
Core Data caches dreams resolved,
Predefined lists evolution—solved!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Support predefined filters in ChannelListQuery' directly describes the main feature addition—predefined filter support for ChannelListQuery—which is the primary objective evident across all file changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/predefined-filters

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.

@laevandus laevandus added the ✅ Feature An issue or PR related to a feature label May 26, 2026
Copy link
Copy Markdown

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

🧹 Nitpick comments (1)
Tests/StreamChatTests/Workers/ChannelListUpdater_Tests.swift (1)

452-452: 💤 Low value

Consider renaming the Core Data attribute for clarity.

The predicate uses attribute name filterHash but queries against query.queryHash. This naming mismatch could cause confusion for future maintainers. If the attribute now stores query hashes rather than filter hashes, consider renaming it to queryHash in the Core Data model for semantic clarity.

🤖 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 `@Tests/StreamChatTests/Workers/ChannelListUpdater_Tests.swift` at line 452,
The test is using request.predicate = NSPredicate(format: "filterHash == %@",
query.queryHash) while the attribute name is filterHash but it actually stores
query hashes; update the Core Data model attribute name from filterHash to
queryHash for semantic clarity, then update all references (e.g.,
request.predicate, fetch requests, NSManagedObject properties) to use
"queryHash" (or, if renaming the model is not possible, add a migration mapping
and clear comments explaining the mismatch) so code and model consistently use
queryHash instead of filterHash.
🤖 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 `@CHANGELOG.md`:
- Around line 6-9: Update the Upcoming changelog section to follow "Keep a
Changelog" structure: replace emoji-prefixed subsection headings like "✅ Added"
with plain "### Added" (and use "### Fixed"/"### Changed" where applicable),
ensure the existing entry mentioning
ChannelListQuery(predefinedFilter:filterValues:sortValues:) is under "##
StreamChat" and move or add empty subsections for "## StreamChatUI" and "##
StreamChatCommonUI" under the same "# Upcoming" parent so the file contains
separate "StreamChat", "StreamChatUI", and "StreamChatCommonUI" subsections with
standardized "### Added/Fixed/Changed" headings.

In
`@Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift`:
- Around line 189-192: When replacing self.query with updatedQuery you must also
update the channelListLinker so websocket/event filtering uses the new query;
after assigning self.query = updatedQuery call the code path that reconfigures
the linker (e.g. update or recreate channelListLinker with updatedQuery — for
example invoke the existing linker configuration helper or a new
configureChannelListLinker(with: updatedQuery) method) instead of only calling
updateChannelListObserver(); apply the same change to the other occurrence
around lines 225–228 so channelListLinker always reflects the current query.

In `@Sources/StreamChat/Database/DTOs/ChannelListQueryDTO.swift`:
- Around line 56-63: The current code silently swallows decode failures for
persisted query payloads (dto.filterJSONData and dto.sortJSONData) by using
try?; update the ChannelListQueryDTO -> mapping logic that sets updated.filter
and updated.sort so that decoding errors from
Filter<ChannelListFilterScope>.predefinedFilter(fromJSONData:) and
[Sorting<ChannelListSortingKey>].predefinedFilterSort(fromJSONData:) are caught
and logged (include the error and the offending data) instead of being
ignored—use a do/catch around those calls, log the error with a clear message
referencing dto.filterJSONData/dto.sortJSONData and the associated types
(Filter<ChannelListFilterScope>, Sorting<ChannelListSortingKey>), and only set
updated.filter/updated.sort on success.

In `@Sources/StreamChat/Query/ChannelListQuery`+PredefinedFilter.swift:
- Around line 34-37: The guard in ChannelListQuery+PredefinedFilter that checks
ChannelListFilterScope.predefinedFilterKeyMapping[key] logs unknown keys but
returns self, leaving the unknown leaf in the filter tree; change the
early-return behavior so the unknown key is actually removed from the returned
filter (e.g., return a new filter with that child/leaf dropped) instead of
returning self. Locate the guard (the lookup of predefinedFilterKeyMapping[key])
and replace the return self with logic that returns the filter with the
offending leaf removed (use the existing filter-tree manipulation helpers or
implement a removal of the child node for the given key) so unresolved keys
cannot leak into predicate resolution.

In `@Sources/StreamChat/StateLayer/ChannelListState`+Observer.swift:
- Around line 63-69: reload(with:) currently replaces channelListObserver but
leaves channelListLinker tied to the old query; update reload(with:) to also
rebuild or reinitialize channelListLinker using the new query (e.g., call
Self.makeChannelListLinker(for: newQuery, database: database, clientConfig:
clientConfig) or the existing factory used at init), so both channelListObserver
and channelListLinker reflect the newQuery and subsequent event-driven updates
evaluate against the refreshed criteria.

In `@TestTools/StreamChatTestTools/SpyPattern/Spy/ChannelListUpdater_Spy.swift`:
- Around line 53-60: The spy currently only treats a server-resolved change as
updatedQuery when filters differ (guard uses resolvedQuery.isFilterEqual), so
sort-only resolutions are ignored; update the logic inside update_completion
(where resolvedQuery, changedQuery and ChannelListQuery are used) to detect any
mutation of the query (not just filter changes) — for example, replace the
isFilterEqual check with a full query equality check or add a sort-comparison
(e.g., use an existing isEqual/isQueryEqual method or compare resolvedQuery.sort
to channelListQuery.sort) so that sort-only server resolutions also assign
changedQuery and propagate it into ChannelListUpdateResult.

---

Nitpick comments:
In `@Tests/StreamChatTests/Workers/ChannelListUpdater_Tests.swift`:
- Line 452: The test is using request.predicate = NSPredicate(format:
"filterHash == %@", query.queryHash) while the attribute name is filterHash but
it actually stores query hashes; update the Core Data model attribute name from
filterHash to queryHash for semantic clarity, then update all references (e.g.,
request.predicate, fetch requests, NSManagedObject properties) to use
"queryHash" (or, if renaming the model is not possible, add a migration mapping
and clear comments explaining the mismatch) so code and model consistently use
queryHash instead of filterHash.
🪄 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: ff35987c-ce52-43f5-afbf-f502fe762565

📥 Commits

Reviewing files that changed from the base of the PR and between b2eb7ac and 160479f.

📒 Files selected for processing (30)
  • CHANGELOG.md
  • DemoApp/StreamChat/Components/DemoChatChannelListVC.swift
  • Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift
  • Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift
  • Sources/StreamChat/Database/DTOs/ChannelDTO.swift
  • Sources/StreamChat/Database/DTOs/ChannelListQueryDTO.swift
  • Sources/StreamChat/Database/DatabaseSession.swift
  • Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contents
  • Sources/StreamChat/Query/ChannelListQuery+PredefinedFilter.swift
  • Sources/StreamChat/Query/ChannelListQuery.swift
  • Sources/StreamChat/Query/Filter.swift
  • Sources/StreamChat/Query/Sorting/ChannelListSortingKey.swift
  • Sources/StreamChat/Repositories/SyncOperations.swift
  • Sources/StreamChat/StateLayer/ChannelList.swift
  • Sources/StreamChat/StateLayer/ChannelListState+Observer.swift
  • Sources/StreamChat/StateLayer/ChannelListState.swift
  • Sources/StreamChat/Utils/Dictionary+Extensions.swift
  • Sources/StreamChat/Workers/ChannelListUpdater.swift
  • StreamChat.xcodeproj/project.pbxproj
  • TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift
  • TestTools/StreamChatTestTools/SpyPattern/Spy/ChannelListUpdater_Spy.swift
  • Tests/StreamChatTests/APIClient/Endpoints/Payloads/ChannelListPayload_Tests.swift
  • Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift
  • Tests/StreamChatTests/Database/DTOs/ChannelListQueryDTO_Tests.swift
  • Tests/StreamChatTests/Query/ChannelListQuery_PredefinedFilter_Tests.swift
  • Tests/StreamChatTests/Query/ChannelListQuery_Tests.swift
  • Tests/StreamChatTests/Query/Filter_Tests.swift
  • Tests/StreamChatTests/Query/Sorting/ListDatabaseObserver+Sorting_Tests.swift
  • Tests/StreamChatTests/StateLayer/ChannelList_Tests.swift
  • Tests/StreamChatTests/Workers/ChannelListUpdater_Tests.swift

Comment thread CHANGELOG.md
Comment on lines +6 to +9
## StreamChat
### ✅ Added
- Add `ChannelListQuery(predefinedFilter:filterValues:sortValues:)` for creating channel list queries with predefined filters [#4113](https://github.com/GetStream/stream-chat-swift/pull/4113)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align Upcoming changelog structure with the required format.

Use ### Added / ### Fixed / ### Changed (without emoji), and include the ## StreamChatCommonUI subsection under # Upcoming as required.

As per coding guidelines, "Follow Keep a Changelog format with ### Added, ### Fixed, ### Changed subsections in CHANGELOG.md" and "Include separate subsections in CHANGELOG.md for StreamChat, StreamChatUI, and StreamChatCommonUI".

🤖 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 `@CHANGELOG.md` around lines 6 - 9, Update the Upcoming changelog section to
follow "Keep a Changelog" structure: replace emoji-prefixed subsection headings
like "✅ Added" with plain "### Added" (and use "### Fixed"/"### Changed" where
applicable), ensure the existing entry mentioning
ChannelListQuery(predefinedFilter:filterValues:sortValues:) is under "##
StreamChat" and move or add empty subsections for "## StreamChatUI" and "##
StreamChatCommonUI" under the same "# Upcoming" parent so the file contains
separate "StreamChat", "StreamChatUI", and "StreamChatCommonUI" subsections with
standardized "### Added/Fixed/Changed" headings.

Comment on lines +189 to +192
if let updatedQuery = updateResult.updatedQuery {
self.query = updatedQuery
self.updateChannelListObserver()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Update the linker when query is replaced.

When updatedQuery is applied, only the database observer is rebuilt. channelListLinker remains configured with the old query, so websocket/event filtering can diverge from the current controller query.

Also applies to: 225-228

🤖 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
`@Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift`
around lines 189 - 192, When replacing self.query with updatedQuery you must
also update the channelListLinker so websocket/event filtering uses the new
query; after assigning self.query = updatedQuery call the code path that
reconfigures the linker (e.g. update or recreate channelListLinker with
updatedQuery — for example invoke the existing linker configuration helper or a
new configureChannelListLinker(with: updatedQuery) method) instead of only
calling updateChannelListObserver(); apply the same change to the other
occurrence around lines 225–228 so channelListLinker always reflects the current
query.

Comment on lines +56 to +63
if !dto.filterJSONData.isEmpty,
let filter = try? Filter<ChannelListFilterScope>.predefinedFilter(fromJSONData: dto.filterJSONData) {
updated.filter = filter
}
if let sortJSONData = dto.sortJSONData,
let sort = try? [Sorting<ChannelListSortingKey>].predefinedFilterSort(fromJSONData: sortJSONData) {
updated.sort = sort
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Don’t silently swallow predefined filter/sort decode failures.

Using try? here hides corrupted persisted payloads and makes local query-state issues hard to debug. Please log decode failures in this path.

Suggested adjustment
-        if !dto.filterJSONData.isEmpty,
-           let filter = try? Filter<ChannelListFilterScope>.predefinedFilter(fromJSONData: dto.filterJSONData) {
-            updated.filter = filter
-        }
-        if let sortJSONData = dto.sortJSONData,
-           let sort = try? [Sorting<ChannelListSortingKey>].predefinedFilterSort(fromJSONData: sortJSONData) {
-            updated.sort = sort
-        }
+        if !dto.filterJSONData.isEmpty {
+            do {
+                updated.filter = try Filter<ChannelListFilterScope>.predefinedFilter(fromJSONData: dto.filterJSONData)
+            } catch {
+                log.error("Failed decoding predefined filter from cache with error: \(error).")
+            }
+        }
+        if let sortJSONData = dto.sortJSONData {
+            do {
+                updated.sort = try [Sorting<ChannelListSortingKey>].predefinedFilterSort(fromJSONData: sortJSONData)
+            } catch {
+                log.error("Failed decoding predefined sort from cache with error: \(error).")
+            }
+        }
🤖 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 `@Sources/StreamChat/Database/DTOs/ChannelListQueryDTO.swift` around lines 56 -
63, The current code silently swallows decode failures for persisted query
payloads (dto.filterJSONData and dto.sortJSONData) by using try?; update the
ChannelListQueryDTO -> mapping logic that sets updated.filter and updated.sort
so that decoding errors from
Filter<ChannelListFilterScope>.predefinedFilter(fromJSONData:) and
[Sorting<ChannelListSortingKey>].predefinedFilterSort(fromJSONData:) are caught
and logged (include the error and the offending data) instead of being
ignored—use a do/catch around those calls, log the error with a clear message
referencing dto.filterJSONData/dto.sortJSONData and the associated types
(Filter<ChannelListFilterScope>, Sorting<ChannelListSortingKey>), and only set
updated.filter/updated.sort on success.

Comment on lines +34 to +37
guard let coreDataMetadata = ChannelListFilterScope.predefinedFilterKeyMapping[key] else {
StreamCore.log.error("Unknown channel list filtering key '\(key)' - dropping from local predefined filter.")
return self
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Unknown filter keys are not actually dropped.

Line 35 says unknown keys are dropped, but Line 36 returns self, so the unknown leaf remains in the filter tree. This can leak unresolved keys into local predicate resolution and cause incorrect cached/local behavior.

Proposed direction
-private func applyCoreDataFilteringKeys() -> Filter {
+private func applyCoreDataFilteringKeys() -> Filter? {
     if `operator`.isGroupOperator {
         guard let children = value as? [Filter] else {
-            return self
+            return self
         }
+        let mappedChildren = children.compactMap { $0.applyCoreDataFilteringKeys() }
         return Filter(
             operator: `operator`,
             key: nil,
-            value: children.map { $0.applyCoreDataFilteringKeys() },
+            value: mappedChildren,
             isCollectionFilter: false
         )
     }
     guard let key else { return self }
     guard let coreDataMetadata = ChannelListFilterScope.predefinedFilterKeyMapping[key] else {
         StreamCore.log.error("Unknown channel list filtering key '\(key)' - dropping from local predefined filter.")
-        return self
+        return nil
     }
     return Filter(
         operator: `operator`,
         key: key,
         value: value,
         valueMapper: coreDataMetadata.valueMapper,
         keyPathString: coreDataMetadata.keyPathString,
         isCollectionFilter: coreDataMetadata.isCollectionFilter,
         predicateMapper: coreDataMetadata.predicateMapper
     )
 }
🤖 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 `@Sources/StreamChat/Query/ChannelListQuery`+PredefinedFilter.swift around
lines 34 - 37, The guard in ChannelListQuery+PredefinedFilter that checks
ChannelListFilterScope.predefinedFilterKeyMapping[key] logs unknown keys but
returns self, leaving the unknown leaf in the filter tree; change the
early-return behavior so the unknown key is actually removed from the returned
filter (e.g., return a new filter with that child/leaf dropped) instead of
returning self. Locate the guard (the lookup of predefinedFilterKeyMapping[key])
and replace the return self with logic that returns the filter with the
offending leaf removed (use the existing filter-tree manipulation helpers or
implement a removal of the child node for the given key) so unresolved keys
cannot leak into predicate resolution.

Comment on lines +63 to +69
func reload(with newQuery: ChannelListQuery) -> [ChatChannel] {
query = newQuery
channelListObserver = Self.makeChannelListObserver(
for: newQuery,
database: database,
clientConfig: clientConfig
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Rebuild the linker when the query is reloaded.

reload(with:) only rebuilds channelListObserver, but channelListLinker still keeps the original query from initialization. After predefined-filter resolution, event-driven updates can be evaluated against stale query criteria.

🤖 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 `@Sources/StreamChat/StateLayer/ChannelListState`+Observer.swift around lines
63 - 69, reload(with:) currently replaces channelListObserver but leaves
channelListLinker tied to the old query; update reload(with:) to also rebuild or
reinitialize channelListLinker using the new query (e.g., call
Self.makeChannelListLinker(for: newQuery, database: database, clientConfig:
clientConfig) or the existing factory used at init), so both channelListObserver
and channelListLinker reflect the newQuery and subsequent event-driven updates
evaluate against the refreshed criteria.

Comment on lines +53 to +60
let resolvedQuery = loadPredefinedFilter(for: channelListQuery)
update_completion = { result in
let changedQuery: ChannelListQuery? = {
guard let resolvedQuery, !resolvedQuery.isFilterEqual(to: channelListQuery) else { return nil }
return resolvedQuery
}()
completion?(result.map { ChannelListUpdateResult(channels: $0, updatedQuery: changedQuery) })
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Propagate sort-only query changes in spy updatedQuery.

Line [56] checks only isFilterEqual, so sort-only server resolutions won’t set updatedQuery. This can hide regressions in tests that rely on query mutation.

🛠️ Proposed fix
         let changedQuery: ChannelListQuery? = {
-            guard let resolvedQuery, !resolvedQuery.isFilterEqual(to: channelListQuery) else { return nil }
+            guard let resolvedQuery, resolvedQuery.queryHash != channelListQuery.queryHash else { return nil }
             return resolvedQuery
         }()
🤖 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 `@TestTools/StreamChatTestTools/SpyPattern/Spy/ChannelListUpdater_Spy.swift`
around lines 53 - 60, The spy currently only treats a server-resolved change as
updatedQuery when filters differ (guard uses resolvedQuery.isFilterEqual), so
sort-only resolutions are ignored; update the logic inside update_completion
(where resolvedQuery, changedQuery and ChannelListQuery are used) to detect any
mutation of the query (not just filter changes) — for example, replace the
isFilterEqual check with a full query equality check or add a sort-comparison
(e.g., use an existing isEqual/isQueryEqual method or compare resolvedQuery.sort
to channelListQuery.sort) so that sort-only server resolutions also assign
changedQuery and propagate it into ChannelListUpdateResult.

Copy link
Copy Markdown
Contributor

@martinmitrevski martinmitrevski left a comment

Choose a reason for hiding this comment

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

looks good, but some changes at tricky places - we should do extensive testing here

completion(error)
}
updateChannelList { result in
completion?(result.error)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think I've mentioned this in the other PR as well, why not self?.callback?

/// - Important: Always add new sorting keys to the map.
static let predefinedSortingKeyMapping: [String: Self] = Dictionary(
uniqueKeysWithValues: [
.cid,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

are these documented/checked by the backend?

/// The metadata is what `Filter+predicate.swift` consumes when building `NSPredicate`s.
struct ChannelListFilterKeyCoreDataMetadata: Sendable {
let keyPathString: String?
let valueMapper: (@Sendable (Any) -> FilterValue?)?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why Any? Shouldn't we be more specific here?

}
return Dictionary(
uniqueKeysWithValues: [
map(.archived),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

not pretty - wonder how we can make sure to update this list when introducing new keys

return
}

let keys = container.allKeys
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why are all these changes needed?

private let channelListLinker: ChannelListLinker
private let channelListUpdater: ChannelListUpdater
private let database: DatabaseContainer
private let dynamicFilter: ((ChatChannel) -> Bool)?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

no more dynamic filter?


final class PredefinedFilterPayload: Decodable, Sendable {
let name: String
let filter: [String: RawJSON]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

we won't use the typesafe Filter we use for the other cases? (Same for sorting)

@github-actions
Copy link
Copy Markdown

1 Warning
⚠️ Big PR

Generated by 🚫 Danger

@github-actions
Copy link
Copy Markdown

Public Interface

 @MainActor public final class ChannelListState: ObservableObject  
-   public let query: ChannelListQuery
+   public internal var query: ChannelListQuery

 public class ChatChannelListController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable  
-   public let query: ChannelListQuery
+   public internal var query: ChannelListQuery

 public struct ChannelListQuery: Encodable, Sendable, LocalConvertibleSortingQuery  
-   
+   case predefinedFilter = "predefined_filter"
- 
+   case filterValues = "filter_values"
-   public let filter: Filter<ChannelListFilterScope>
+   case sortValues = "sort_values"
-   public let sort: [Sorting<ChannelListSortingKey>]
+   
-   public var pagination: Pagination
+ 
-   public let messagesLimit: Int?
+   public internal var filter: Filter<ChannelListFilterScope>
-   public let membersLimit: Int?
+   public internal var sort: [Sorting<ChannelListSortingKey>]
-   public var options: QueryOptions
+   public var pagination: Pagination
-   
+   public let messagesLimit: Int?
- 
+   public let membersLimit: Int?
-   public init(filter: Filter<ChannelListFilterScope>,sort: [Sorting<ChannelListSortingKey>] = [],pageSize: Int = .channelsPageSize,messagesLimit: Int? = nil,membersLimit: Int? = nil)
+   public var options: QueryOptions
-   
+   public let predefinedFilter: String?
- 
+   public let filterValues: [String: RawJSON]?
-   public func encode(to encoder: Encoder)throws
+   public let sortValues: [String: RawJSON]?
+   
+ 
+   public init(filter: Filter<ChannelListFilterScope>,sort: [Sorting<ChannelListSortingKey>] = [],pageSize: Int = .channelsPageSize,messagesLimit: Int? = nil,membersLimit: Int? = nil)
+   public init(predefinedFilter: String,filterValues: [String: RawJSON]? = nil,sortValues: [String: RawJSON]? = nil,pageSize: Int = .channelsPageSize,messagesLimit: Int? = nil,membersLimit: Int? = nil)
+   
+ 
+   public func encode(to encoder: Encoder)throws

@sonarqubecloud
Copy link
Copy Markdown

@laevandus laevandus marked this pull request as draft May 27, 2026 08:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✅ Feature An issue or PR related to a feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants