Support predefined filters in ChannelListQuery#4113
Conversation
# Conflicts: # CHANGELOG.md # DemoApp/StreamChat/Components/DemoChatChannelListVC.swift
📝 WalkthroughWalkthroughThis 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. ChangesPredefined Channel List Filters
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
Tests/StreamChatTests/Workers/ChannelListUpdater_Tests.swift (1)
452-452: 💤 Low valueConsider renaming the Core Data attribute for clarity.
The predicate uses attribute name
filterHashbut queries againstquery.queryHash. This naming mismatch could cause confusion for future maintainers. If the attribute now stores query hashes rather than filter hashes, consider renaming it toqueryHashin 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
📒 Files selected for processing (30)
CHANGELOG.mdDemoApp/StreamChat/Components/DemoChatChannelListVC.swiftSources/StreamChat/APIClient/Endpoints/Payloads/ChannelListPayload.swiftSources/StreamChat/Controllers/ChannelListController/ChannelListController.swiftSources/StreamChat/Database/DTOs/ChannelDTO.swiftSources/StreamChat/Database/DTOs/ChannelListQueryDTO.swiftSources/StreamChat/Database/DatabaseSession.swiftSources/StreamChat/Database/StreamChatModel.xcdatamodeld/StreamChatModel.xcdatamodel/contentsSources/StreamChat/Query/ChannelListQuery+PredefinedFilter.swiftSources/StreamChat/Query/ChannelListQuery.swiftSources/StreamChat/Query/Filter.swiftSources/StreamChat/Query/Sorting/ChannelListSortingKey.swiftSources/StreamChat/Repositories/SyncOperations.swiftSources/StreamChat/StateLayer/ChannelList.swiftSources/StreamChat/StateLayer/ChannelListState+Observer.swiftSources/StreamChat/StateLayer/ChannelListState.swiftSources/StreamChat/Utils/Dictionary+Extensions.swiftSources/StreamChat/Workers/ChannelListUpdater.swiftStreamChat.xcodeproj/project.pbxprojTestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swiftTestTools/StreamChatTestTools/SpyPattern/Spy/ChannelListUpdater_Spy.swiftTests/StreamChatTests/APIClient/Endpoints/Payloads/ChannelListPayload_Tests.swiftTests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swiftTests/StreamChatTests/Database/DTOs/ChannelListQueryDTO_Tests.swiftTests/StreamChatTests/Query/ChannelListQuery_PredefinedFilter_Tests.swiftTests/StreamChatTests/Query/ChannelListQuery_Tests.swiftTests/StreamChatTests/Query/Filter_Tests.swiftTests/StreamChatTests/Query/Sorting/ListDatabaseObserver+Sorting_Tests.swiftTests/StreamChatTests/StateLayer/ChannelList_Tests.swiftTests/StreamChatTests/Workers/ChannelListUpdater_Tests.swift
| ## 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) | ||
|
|
There was a problem hiding this comment.
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.
| if let updatedQuery = updateResult.updatedQuery { | ||
| self.query = updatedQuery | ||
| self.updateChannelListObserver() | ||
| } |
There was a problem hiding this comment.
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.
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| guard let coreDataMetadata = ChannelListFilterScope.predefinedFilterKeyMapping[key] else { | ||
| StreamCore.log.error("Unknown channel list filtering key '\(key)' - dropping from local predefined filter.") | ||
| return self | ||
| } |
There was a problem hiding this comment.
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.
| func reload(with newQuery: ChannelListQuery) -> [ChatChannel] { | ||
| query = newQuery | ||
| channelListObserver = Self.makeChannelListObserver( | ||
| for: newQuery, | ||
| database: database, | ||
| clientConfig: clientConfig | ||
| ) |
There was a problem hiding this comment.
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.
| 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) }) | ||
| } |
There was a problem hiding this comment.
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.
martinmitrevski
left a comment
There was a problem hiding this comment.
looks good, but some changes at tricky places - we should do extensive testing here
| completion(error) | ||
| } | ||
| updateChannelList { result in | ||
| completion?(result.error) |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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?)? |
There was a problem hiding this comment.
why Any? Shouldn't we be more specific here?
| } | ||
| return Dictionary( | ||
| uniqueKeysWithValues: [ | ||
| map(.archived), |
There was a problem hiding this comment.
not pretty - wonder how we can make sure to update this list when introducing new keys
| return | ||
| } | ||
|
|
||
| let keys = container.allKeys |
There was a problem hiding this comment.
why are all these changes needed?
| private let channelListLinker: ChannelListLinker | ||
| private let channelListUpdater: ChannelListUpdater | ||
| private let database: DatabaseContainer | ||
| private let dynamicFilter: ((ChatChannel) -> Bool)? |
There was a problem hiding this comment.
no more dynamic filter?
|
|
||
| final class PredefinedFilterPayload: Decodable, Sendable { | ||
| let name: String | ||
| let filter: [String: RawJSON] |
There was a problem hiding this comment.
we won't use the typesafe Filter we use for the other cases? (Same for sorting)
Generated by 🚫 Danger |
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 |
|



🔗 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
ChannelListQuery(predefinedFilter:filterValues:sortValues:)initializer.predefined_filterresponse are decoded and re-applied locally for Core Data caching.ChatChannelListController.queryandChannelListState.querybecomeinternal(set)so the SDK can swap to the resolved query after the first response.Filterdecoder now supports multi-key objects as an implicit$and, matching the Stream filter JSON shape.🛠 Implementation
ChannelListQuery+PredefinedFilter.swiftdecodes persisted filter/sort JSON and re-attaches Core Data wiring (keyPathString,valueMapper,predicateMapper) viaChannelListFilterScope.predefinedFilterKeyMapping. Unknown keys are logged and dropped.ChannelListPayloaddecodes the newpredefined_filterresponse key.ChannelListUpdaterpersists the resolved filter+sort ontoChannelListQueryDTOand returns an updatedChannelListQueryto the controller/state layer.ChannelListSortingKey.predefinedSortingKeyMappingprovides the same key registry for sort fields.loadPredefinedFilter(for:)rehydrates the locally cached resolved query so Core Data fetch predicates work before the first server response lands.🧪 Manual Testing Notes
New pre-defined filters can be added through the dashboard.
☑️ Contributor Checklist
docs-contentrepoSummary by CodeRabbit
Release Notes
New Features
Documentation