Skip to content

Support predefined filters in ChannelListQuery#4113

Merged
laevandus merged 15 commits into
developfrom
feature/predefined-filters
Jun 3, 2026
Merged

Support predefined filters in ChannelListQuery#4113
laevandus merged 15 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 implicit $and and the { key: value } short form as implicit $eq, 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.
  • The predefinedFilterKeyMapping / predefinedSortingKeyMapping registries are generated by Sourcery from the channel-list FilterKey / ChannelListSortingKey declarations (Generated/PredefinedFilter+Generated.swift). Adding a key no longer needs a manual map edit — make generate regenerates them and CI fails if the committed output is stale.
  • 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.
  • 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

  • New Features

    • Added support for predefined channel list filters via ChannelListQuery(predefinedFilter:filterValues:sortValues:), enabling server-side filtering with substitutable parameter values.
    • Channel lists can now be filtered using predefined messaging and livestream filter templates.
  • Documentation

    • Updated changelog with predefined filter query support reference.

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

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

This PR introduces server-side predefined channel list filters with transparent query resolution. Clients create queries with a predefinedFilter identifier and optional substitution maps; the backend resolves these to concrete filter/sort JSON. The resolved values are cached in Core Data and applied transparently to immutable-external mutable-internal ChannelListQuery properties across state and controller layers.

Changes

Predefined Channel List Filters

Layer / File(s) Summary
Query model with predefined filter support
Sources/StreamChat/Query/ChannelListQuery.swift
ChannelListQuery gains predefinedFilter, filterValues, and sortValues properties; filter and sort become mutable with internal-set access; new initializer for predefined queries; queryHash computed property for Core Data persistence; conditional JSON encoding for predefined vs traditional queries.
Filter JSON decoding enhancements
Sources/StreamChat/Query/Filter.swift, Sources/StreamChat/Query/ChannelListQuery+PredefinedFilter.swift, Sources/StreamChat/Utils/Dictionary+Extensions.swift
Filter decoding refactored to support implicit $eq (short form) and multi-key $and (implicit grouping); decodeFilterValue helper handles typed null, scalars, and homogeneous arrays; new Filter+PredefinedFilter file adds recursive JSON-to-Filter wiring with Core Data keyPath resolution; Dictionary extension adds sortedDescription for deterministic hashing.
API payload and channel list decoding
Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swift
ChannelListPayload adds optional predefinedFilter property decoded from predefined_filter JSON; introduces PredefinedFilterPayload type to carry server-provided filter name, encoded filter, and sort data.
Core Data schema and DTO additions
Sources/StreamChat/Database/StreamChatModel.xcdatamodeld/..., Sources/StreamChat/Database/DTOs/ChannelListQueryDTO.swift
Core Data model adds sortJSONData optional binary attribute; ChannelListQueryDTO gains load(query:context:) using queryHash for lookup; adds loadPredefinedFilter(for:) to reconstruct resolved queries from cached JSON; updates saveQuery(query:predefinedFilter:) with upsert logic and selective JSON encoding.
Database session protocol and contract updates
Sources/StreamChat/Database/DatabaseSession.swift
ChannelDatabaseSession protocol changes from filterHash-based to query-based lookups; adds loadPredefinedFilter(for:) method; updates saveQuery signature to accept optional PredefinedFilterPayload; adds backwards-compatible saveQuery(query:) extension.
Mutable query state and observer reload support
Sources/StreamChat/StateLayer/ChannelListState.swift, Sources/StreamChat/StateLayer/ChannelListState+Observer.swift
ChannelListState applies predefined filter resolution on init via loadPredefinedFilter, makes query mutable with setQuery(_:) method; ChannelListState.Observer stores mutable query and handler, adds reload(with:) for query-driven observer recreation and observation restart.
Controller query mutation and observer management
Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift
ChatChannelListController.query becomes mutable; observer initialization consults loadPredefinedFilter; updateChannelList accepts completion returning ChannelListUpdateResult; on success with updatedQuery, replaces query and refreshes observer via new updateChannelListObserver() helper.
Worker result enrichment and predefined filter handling
Sources/StreamChat/Workers/ChannelListUpdater.swift
Introduces ChannelListUpdateResult carrying channels and optional updatedQuery; update(channelListQuery:) returns richer result; adds loadPredefinedFilter(for:) helper to read resolved queries; writeChannelListPayload populates updatedQuery when predefined filter differs from incoming.
ChannelList state coordination with query updates
Sources/StreamChat/StateLayer/ChannelList.swift
ChannelList reads state.query for pagination instead of stored query; loadChannels(with:) updates pagination, calls updater, and conditionally persists updatedQuery back to state; loadMoreChannels and refreshLoadedChannels use current state.query.
Channel DTO query persistence updates
Sources/StreamChat/Database/DTOs/ChannelDTO.swift
ChannelDTO.saveChannelList passes payload.predefinedFilter to saveQuery; delete(query:) and channelListFetchRequest use query.queryHash instead of query.filter.filterHash for lookups.
Sync operation logging cleanup
Sources/StreamChat/Repositories/SyncOperations.swift
RefreshChannelListOperation removes channelList.query.filter details from log messages, using generic "channel list" text.
Build and code generation infrastructure
Sources/StreamChat/.sourcery.yml, Makefile, fastlane/Fastfile, Scripts/bootstrap.sh, .swiftlint.yml, Package.swift, Githubfile
Adds Sourcery configuration for code generation; Makefile generate target; Fastlane generate and validate_generated_code lanes; bootstrap script Sourcery installation; generated code exclusions to SwiftLint and Package; exports SOURCERY_VERSION.
Test infrastructure: mocks, spies, and fixtures
TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift, TestTools/StreamChatTestTools/SpyPattern/Spy/ChannelListUpdater_Spy.swift, TestTools/StreamChatTestTools/TestData/FilterTestScope.swift
DatabaseSession_Mock forwards loadPredefinedFilter and updates signatures; ChannelListUpdater_Spy returns ChannelListUpdateResult with optional updatedQuery; FilterTestScope adds inArrayString, equalArrayInt, nor test cases.
Comprehensive test coverage for all layers
Tests/StreamChatTests/Query/ChannelListQuery_Tests.swift, Tests/StreamChatTests/Query/Filter_Tests.swift, Tests/StreamChatTests/Query/ChannelListQuery_PredefinedFilter_Tests.swift, Tests/StreamChatTests/APIClient/Endpoints/Payloads/ChannelListPayload_Tests.swift, Tests/StreamChatTests/Database/DTOs/ChannelListQueryDTO_Tests.swift, Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift, Tests/StreamChatTests/Workers/ChannelListUpdater_Tests.swift, Tests/StreamChatTests/StateLayer/ChannelList_Tests.swift, Tests/StreamChatTests/Query/Sorting/ListDatabaseObserver+Sorting_Tests.swift, StreamChat.xcodeproj/project.pbxproj
Tests for ChannelListQuery encoding and queryHash stability; Filter implicit $eq and $and decoding; ChannelListPayload predefined filter JSON decoding; ChannelListQueryDTO persistence and loading; ChannelListQuery+PredefinedFilter key mappings and recursive decoding; ChannelListController predefined resolution and observer behavior; ChannelListUpdater result types and filter handling; ChannelList state coordination with resolved queries; test fixture updates.
Demo app UI and changelog
DemoApp/StreamChat/Components/DemoChatChannelListVC.swift, CHANGELOG.md
DemoChatChannelListVC adds two predefined queries (messaging, livestream) with corresponding filter action sheet entries and handlers; changelog documents new ChannelListQuery(predefinedFilter:filterValues:sortValues:) initializer.
CI validation step
.github/workflows/smoke-checks.yml
Adds validate_generated_code fastlane step in smoke-checks workflow to ensure generated code is fresh.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

🟢 QAed

Suggested reviewers

  • martinmitrevski

Poem

🐰 In channels deep where queries flow,
Predefined filters steal the show,
Mutations dance through state and core,
While Core Data locks them in the store.
From API down to UI gleam,
This feature makes a seamless dream!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.88% 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 and accurately describes the main change: adding support for server-side predefined filters to ChannelListQuery.
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 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 thread Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift Outdated
Comment thread Sources/StreamChat/Database/DTOs/ChannelListQueryDTO.swift Outdated
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 thread Sources/StreamChat/StateLayer/ChannelListState+Observer.swift
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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Adding back, got dropped with refactoring.

/// - 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?

Comment thread Sources/StreamChat/Query/ChannelListQuery+PredefinedFilter.swift Outdated
Comment thread Sources/StreamChat/Query/ChannelListQuery.swift Outdated
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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Backend implementation can return JSON which looks like:

{ "type": "messaging", "members": { "$in": ["amy"]
  } }

which should be treated as $and.

Current parsing would only pick the first node. These changes fixes it by turning that case into explicit $and. $eq has a similar case where the operator can be omitted from the JSON.

I will see how to make this code more understandable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OK, not really, but I did add more tests since there were some small issues with the implicit $and handling.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It is just unused variable here, it is passed into ChannelListLinker in the init method. Probably should revert this cleanup noise.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 27, 2026

2 Warnings
⚠️ Big PR
⚠️ The changes should be manually QAed before the Pull Request will be merged

Generated by 🚫 Danger

@laevandus laevandus marked this pull request as draft May 27, 2026 08:32
Comment on lines +85 to +87
if let updated = worker.loadPredefinedFilter(for: query) {
query = updated
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

For supporting app relaunches where API call has not finished and we load the filter from CoreData and pass it to FRC.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I had 2 options here:
a) load it async, but then FRC needs to be recreated/reloaded anyway (2 times, one without correct local filter, second with loaded filter) (this is because only CoreData knows the fetches filter JSON)
b) block, load filter, create FRC once

@laevandus laevandus marked this pull request as ready for review May 29, 2026 14:01
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: 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 `@fastlane/Fastfile`:
- Around line 733-740: The validate_generated_code lane currently runs generate
and then checks Sources/StreamChat/Generated/PredefinedFilter+Generated.swift
with a raw git status, which can fail if Sourcery output isn't formatted exactly
like the committed file; update the lane (or the generate task) to format the
generated file before comparing — e.g., after calling generate (or within the
make generate target that runs sourcery), run the project's formatter/linter fix
command (the same invoked by fastlane run_swift_format / swiftformat --fix
and/or swiftlint --fix) on the generated file
(Sources/StreamChat/Generated/PredefinedFilter+Generated.swift) and only then
perform the git status check in validate_generated_code so the staleness test
ignores differences only due to formatting.
🪄 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: 7b68af7b-5f82-4fa6-b330-7dbd504636ce

📥 Commits

Reviewing files that changed from the base of the PR and between cfce125 and e42cc6d.

⛔ Files ignored due to path filters (2)
  • Sources/StreamChat/Generated/PredefinedFilter+Generated.swift is excluded by !**/generated/**
  • Sources/StreamChat/Generated/PredefinedFilter.stencil is excluded by !**/generated/**
📒 Files selected for processing (10)
  • .github/workflows/smoke-checks.yml
  • .swiftlint.yml
  • Githubfile
  • Makefile
  • Package.swift
  • Scripts/bootstrap.sh
  • Sources/StreamChat/.sourcery.yml
  • Sources/StreamChat/Query/ChannelListQuery.swift
  • Tests/StreamChatTests/Query/ChannelListQuery_PredefinedFilter_Tests.swift
  • fastlane/Fastfile
💤 Files with no reviewable changes (1)
  • Sources/StreamChat/Query/ChannelListQuery.swift
✅ Files skipped from review due to trivial changes (4)
  • Sources/StreamChat/.sourcery.yml
  • Makefile
  • .swiftlint.yml
  • Githubfile
🚧 Files skipped from review as they are similar to previous changes (1)
  • Tests/StreamChatTests/Query/ChannelListQuery_PredefinedFilter_Tests.swift

Comment thread fastlane/Fastfile
private let channelListUpdater: ChannelListUpdater
private let client: ChatClient
@MainActor private var stateBuilder: StateBuilder<ChannelListState>
let query: ChannelListQuery
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was only for sync repo and now it needs to be mutable. Easiest was to just get rid of it.

@laevandus laevandus added the 🤞 Ready For QA A PR that is Ready for QA label Jun 1, 2026
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.

lgtm ✅ another one for @testableapple to keep an eye on.

// Generated using Sourcery 2.3.0 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT

// Run `make generate` after changing channel-list FilterKey or ChannelListSortingKey members.
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.

this is a good solution

laevandus added 3 commits June 2, 2026 12:29
# Conflicts:
#	CHANGELOG.md
#	Sources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.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/Repositories/SyncOperations.swift
#	Sources/StreamChat/StateLayer/ChannelList.swift
#	Sources/StreamChat/StateLayer/ChannelListState.swift
#	Sources/StreamChat/Workers/ChannelListUpdater.swift
#	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/Query/Sorting/ListDatabaseObserver+Sorting_Tests.swift
#	Tests/StreamChatTests/StateLayer/ChannelList_Tests.swift
#	Tests/StreamChatTests/Workers/ChannelListUpdater_Tests.swift
@Stream-SDK-Bot
Copy link
Copy Markdown
Collaborator

SDK Size

title develop branch diff status
StreamChat 6.92 MB 6.96 MB +41 KB 🟢
StreamChatUI 4.25 MB 4.25 MB 0 KB 🟢
StreamChatCommonUI 0.84 MB 0.84 MB 0 KB 🟢

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 2, 2026

Public Interface

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

 @MainActor public final class ChannelListState: ObservableObject  
-   public private var 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

@Stream-SDK-Bot
Copy link
Copy Markdown
Collaborator

StreamChat XCSize

Object Diff (bytes)
UserListController.o -24806
ChannelListController.o +24497
PredefinedFilter+Generated.o +12430
ChannelListQuery+PredefinedFilter.o +6834
ChannelListPayload.o +5697
Show 45 more objects
Object Diff (bytes)
ChannelListQuery.o +5089
Filter.o +3990
ChannelListUpdater.o +3654
ChannelListQueryDTO.o +2725
MessageDTO.o +2340
ChannelList.o +1912
ChannelListState+Observer.o +1766
StreamCDNStorage.o +1252
DataStore.o +960
ThreadDTO.o -524
MessageSender.o -496
RequestEncoder.o +400
SyncOperations.o -352
ChatClient+Factory.o -228
ChannelDTO.o +196
StreamCore.o -196
CurrentUserPayloads.o -172
MessagePayloads.o +168
UnknownChannelEvent.o -168
MessageReactionDTO.o -156
ChannelUpdater.o +148
LivestreamChannelController.o +140
AttachmentDownloader.o +137
ChannelListSortingKey.o -129
FlagUserPayload.o -124
UserListUpdater.o +112
CastPollVoteRequestBody.o +108
MessageReminderDTO.o -100
MemberEventMiddleware.o -95
ChannelRepository.o +80
Chat.o -64
APIClient.o +64
LocationPayloads.o +64
Sequence+CompactMapLoggingError.o -60
ChatClient+Environment.o +60
ChannelListLinker.o +60
MessageSearch.o -60
ChannelListState.o +60
OfflineRequestsRepository.o +52
EndpointPath.o -52
MarkdownParser.o -52
PollVoteListController.o +52
ListChange.o -48
ChatMessageAttachment.o +44
AttachmentTypes.o +44

@Stream-SDK-Bot
Copy link
Copy Markdown
Collaborator

StreamChatUI XCSize

Object Diff (bytes)
ChatChannelListRouter.o -44

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Jun 2, 2026

@testableapple testableapple added 🧪 QAing 🟢 QAed A PR that was QAed and removed 🤞 Ready For QA A PR that is Ready for QA 🧪 QAing labels Jun 2, 2026
- run: bundle exec fastlane rubocop
- run: bundle exec fastlane run_swift_format strict:true
- run: bundle exec fastlane validate_public_interface
- run: bundle exec fastlane validate_generated_code
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.

🔥

@laevandus laevandus merged commit 2b2fa41 into develop Jun 3, 2026
17 checks passed
@laevandus laevandus deleted the feature/predefined-filters branch June 3, 2026 09:23
@Stream-SDK-Bot Stream-SDK-Bot mentioned this pull request Jun 3, 2026
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 🟢 QAed A PR that was QAed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants