-
Notifications
You must be signed in to change notification settings - Fork 233
Support predefined filters in ChannelListQuery #4113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
82fef7e
34de19d
160479f
e444cfa
6fc2f12
440e2da
b0a5371
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,24 +7,48 @@ import Foundation | |
| struct ChannelListPayload { | ||
| /// A list of channels response (see `ChannelQuery`). | ||
| let channels: [ChannelPayload] | ||
|
|
||
| /// Server-resolved predefined filter, present only when the query was made with a predefined filter. | ||
| let predefinedFilter: PredefinedFilterPayload? | ||
|
|
||
| init(channels: [ChannelPayload], predefinedFilter: PredefinedFilterPayload? = nil) { | ||
| self.channels = channels | ||
| self.predefinedFilter = predefinedFilter | ||
| } | ||
| } | ||
|
|
||
| extension ChannelListPayload: Decodable { | ||
| enum CodingKeys: String, CodingKey { | ||
| case channels | ||
| case predefinedFilter = "predefined_filter" | ||
| } | ||
|
|
||
| init(from decoder: Decoder) throws { | ||
| let container = try decoder.container(keyedBy: CodingKeys.self) | ||
| let channels = try container | ||
| .decodeArrayIgnoringFailures([ChannelPayload].self, forKey: .channels) | ||
| let predefinedFilter = try container | ||
| .decodeIfPresent(PredefinedFilterPayload.self, forKey: .predefinedFilter) | ||
|
|
||
| self.init( | ||
| channels: channels | ||
| channels: channels, | ||
| predefinedFilter: predefinedFilter | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| final class PredefinedFilterPayload: Decodable, Sendable { | ||
| let name: String | ||
| let filter: [String: RawJSON] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This gets saved in CoreData first so it is better to keep the form what backend gives us. The typed filter transformation can change the encoded JSON format (e.g. decoding adds implicit $eq). Other case is that OpenAPI decoding will give me the same form so it is easier to migrate that later on. |
||
| let sort: [[String: RawJSON]] | ||
|
|
||
| init(name: String, filter: [String: RawJSON], sort: [[String: RawJSON]]) { | ||
| self.name = name | ||
| self.filter = filter | ||
| self.sort = sort | ||
| } | ||
| } | ||
|
|
||
| struct ChannelPayload { | ||
| let channel: ChannelDetailPayload | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,7 +35,7 @@ extension ChatClient { | |
| /// - Note: For an async-await alternative of the `ChatChannelListController`, please check ``ChannelList`` in the async-await supported [state layer](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/). | ||
| public class ChatChannelListController: DataController, DelegateCallable, DataStoreProvider, @unchecked Sendable { | ||
| /// The query specifying and filtering the list of channels. | ||
| public let query: ChannelListQuery | ||
| public internal(set) var query: ChannelListQuery | ||
|
|
||
| /// The `ChatClient` instance this controller belongs to. | ||
| public let client: ChatClient | ||
|
|
@@ -51,11 +51,7 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt | |
| } | ||
|
|
||
| /// The worker used to fetch the remote data and communicate with servers. | ||
| private lazy var worker: ChannelListUpdater = self.environment | ||
| .channelQueryUpdaterBuilder( | ||
| client.databaseContainer, | ||
| client.apiClient | ||
| ) | ||
| private let worker: ChannelListUpdater | ||
|
|
||
| /// The worker used to update current user data. | ||
| private lazy var currentUserUpdater: CurrentUserUpdater = self.environment | ||
|
|
@@ -82,8 +78,15 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt | |
| } | ||
|
|
||
| private(set) lazy var channelListObserver: BackgroundListDatabaseObserver<ChatChannel, ChannelDTO> = { | ||
| if let updated = worker.loadPredefinedFilter(for: query) { | ||
| query = updated | ||
| } | ||
|
Comment on lines
+81
to
+83
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| return makeChannelListObserver() | ||
| }() | ||
|
|
||
| private func makeChannelListObserver() -> BackgroundListDatabaseObserver<ChatChannel, ChannelDTO> { | ||
| let request = ChannelDTO.channelListFetchRequest(query: self.query, chatClientConfig: client.config) | ||
| let observer = self.environment.createChannelListDatabaseObserver( | ||
| let observer = environment.createChannelListDatabaseObserver( | ||
| client.databaseContainer, | ||
| request, | ||
| { try $0.asModel() }, | ||
|
|
@@ -101,7 +104,7 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt | |
| } | ||
| } | ||
| return observer | ||
| }() | ||
| } | ||
|
|
||
| var _basePublishers: Any? | ||
| /// An internal backing object for all publicly available Combine publishers. We use it to simplify the way we expose | ||
|
|
@@ -117,10 +120,18 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt | |
|
|
||
| private let filter: (@Sendable (ChatChannel) -> Bool)? | ||
| private let environment: Environment | ||
| private lazy var channelListLinker: ChannelListLinker = self.environment | ||
| .channelListLinkerBuilder( | ||
| query, filter, client.config, client.databaseContainer, worker, client.channelWatcherHandler | ||
| private var channelListLinker: ChannelListLinker | ||
|
|
||
| private func makeChannelListLinker() -> ChannelListLinker { | ||
| environment.channelListLinkerBuilder( | ||
| query, | ||
| filter, | ||
| client.config, | ||
| client.databaseContainer, | ||
| worker, | ||
| client.channelWatcherHandler | ||
| ) | ||
| } | ||
|
|
||
| /// Creates a new `ChannelListController`. | ||
| /// | ||
|
|
@@ -139,21 +150,28 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt | |
| self.filter = filter | ||
| self.environment = environment | ||
| self.deliveryCriteriaValidator = environment.deliveryCriteriaValidatorBuilder() | ||
| let worker = environment.channelQueryUpdaterBuilder(client.databaseContainer, client.apiClient) | ||
| self.worker = worker | ||
| channelListLinker = environment.channelListLinkerBuilder( | ||
| query, | ||
| filter, | ||
| client.config, | ||
| client.databaseContainer, | ||
| worker, | ||
| client.channelWatcherHandler | ||
| ) | ||
| super.init() | ||
| } | ||
|
|
||
| override public func synchronize(_ completion: (@MainActor (_ error: Error?) -> Void)? = nil) { | ||
| startChannelListObserverIfNeeded() | ||
| channelListLinker.start(with: client.eventNotificationCenter) | ||
| client.syncRepository.startTrackingChannelListController(self) | ||
| updateChannelList { [weak self] error in | ||
| guard let completion else { return } | ||
| self?.callback { | ||
| completion(error) | ||
| } | ||
| updateChannelList { result in | ||
| self.callback { completion?(result.error) } | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Actions | ||
|
|
||
| /// Loads next channels from backend. | ||
|
|
@@ -179,9 +197,13 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt | |
| updatedQuery.pagination = Pagination(pageSize: limit, offset: channels.count) | ||
| worker.update(channelListQuery: updatedQuery) { result in | ||
| switch result { | ||
| case let .success(channels): | ||
| self.markChannelsAsDeliveredIfNeeded(channels: channels) | ||
| self.hasLoadedAllPreviousChannels = channels.count < limit | ||
| case let .success(updateResult): | ||
| self.markChannelsAsDeliveredIfNeeded(channels: updateResult.channels) | ||
| self.hasLoadedAllPreviousChannels = updateResult.channels.count < limit | ||
| if let updatedQuery = updateResult.updatedQuery { | ||
| self.query = updatedQuery | ||
| self.updateChannelListObserver() | ||
| } | ||
|
laevandus marked this conversation as resolved.
|
||
| self.callback { completion?(nil) } | ||
| case let .failure(error): | ||
| self.callback { completion?(error) } | ||
|
|
@@ -199,28 +221,34 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt | |
| // MARK: - Helpers | ||
|
|
||
| private func updateChannelList( | ||
| _ completion: (@MainActor (_ error: Error?) -> Void)? = nil | ||
| _ completion: (@MainActor (Result<ChannelListUpdateResult, Error>) -> Void)? = nil | ||
| ) { | ||
| let limit = query.pagination.pageSize | ||
| worker.update( | ||
| channelListQuery: query | ||
| ) { [weak self] result in | ||
| switch result { | ||
| case let .success(channels): | ||
| case let .success(updateResult): | ||
| self?.state = .remoteDataFetched | ||
| self?.hasLoadedAllPreviousChannels = channels.count < limit | ||
| self?.hasLoadedAllPreviousChannels = updateResult.channels.count < limit | ||
|
|
||
| // Mark channels as delivered if synchronization was successful | ||
| self?.markChannelsAsDeliveredIfNeeded(channels: channels) | ||
| self?.markChannelsAsDeliveredIfNeeded(channels: updateResult.channels) | ||
|
|
||
| // Predefined filters can update local query representation (query gets backend defined filter and sort which must be set to FRC) | ||
| if let updatedQuery = updateResult.updatedQuery { | ||
| self?.query = updatedQuery | ||
| self?.updateChannelListObserver() | ||
| } | ||
|
|
||
| self?.callback { completion?(nil) } | ||
| self?.callback { completion?(.success(updateResult)) } | ||
| case let .failure(error): | ||
| self?.state = .remoteDataFetchFailed(ClientError(with: error)) | ||
| self?.callback { completion?(error) } | ||
| self?.callback { completion?(.failure(error)) } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Marks channels as delivered if they meet the specified criteria. | ||
| /// - Parameter channels: The channels to evaluate for marking as delivered. | ||
| private func markChannelsAsDeliveredIfNeeded(channels: [ChatChannel]) { | ||
|
|
@@ -264,6 +292,18 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt | |
| log.error("Failed to perform fetch request with error: \(error). This is an internal error.") | ||
| } | ||
| } | ||
|
|
||
| private func updateChannelListObserver() { | ||
| channelListObserver = makeChannelListObserver() | ||
| channelListLinker = makeChannelListLinker() | ||
| channelListLinker.start(with: client.eventNotificationCenter) | ||
| do { | ||
| try channelListObserver.startObserving() | ||
| } catch { | ||
| state = .localDataFetchFailed(ClientError(with: error)) | ||
| log.error("Failed to update the channel list observer: \(error)") | ||
| } | ||
| } | ||
| } | ||
|
|
||
| extension ChatChannelListController { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Align
Upcomingchangelog structure with the required format.Use
### Added/### Fixed/### Changed(without emoji), and include the## StreamChatCommonUIsubsection under# Upcomingas required.As per coding guidelines, "Follow Keep a Changelog format with
### Added,### Fixed,### Changedsubsections in CHANGELOG.md" and "Include separate subsections in CHANGELOG.md for StreamChat, StreamChatUI, and StreamChatCommonUI".π€ Prompt for AI Agents