-
Notifications
You must be signed in to change notification settings - Fork 232
Expand file tree
/
Copy pathChannelListController.swift
More file actions
369 lines (325 loc) 路 14.8 KB
/
Copy pathChannelListController.swift
File metadata and controls
369 lines (325 loc) 路 14.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
//
// Copyright 漏 2026 Stream.io Inc. All rights reserved.
//
import CoreData
import Foundation
extension ChatClient {
/// Creates a new `ChannelListController` with the provided channel query.
/// - Parameter query: The query specify the filter and sorting of the channels the controller should fetch. ///
/// - Returns: A new instance of `ChatChannelListController`.
public func channelListController(query: ChannelListQuery) -> ChatChannelListController {
.init(query: query, client: self)
}
/// Creates a new `ChannelListController` with the provided channel query and filter block.
///
/// When passing `filter`, make sure the runtime logic matches the one expected by the filter passed in the query object.
/// If they don't match, there can be jumps when loading the list.
///
/// - Parameters:
/// - query: The query specify the filter and sorting of the channels the controller should fetch.
/// - filter: A block that determines whether the channels belongs to this controller.
/// - Returns: A new instance of `ChatChannelListController`
public func channelListController(
query: ChannelListQuery,
filter: (@Sendable (ChatChannel) -> Bool)? = nil
) -> ChatChannelListController {
.init(query: query, client: self, filter: filter)
}
}
/// `ChatChannelListController` is a controller class which allows observing a list of chat channels based on the provided query.
///
/// - 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 internal(set) var query: ChannelListQuery
/// The `ChatClient` instance this controller belongs to.
public let client: ChatClient
/// The channels matching the query of this controller.
///
/// To observe changes of the channels, set your class as a delegate of this controller or use the provided
/// `Combine` publishers.
///
public var channels: [ChatChannel] {
startChannelListObserverIfNeeded()
return channelListObserver.items
}
/// The worker used to fetch the remote data and communicate with servers.
private lazy var worker: ChannelListUpdater = self.environment
.channelQueryUpdaterBuilder(
client.databaseContainer,
client.apiClient
)
/// The worker used to update current user data.
private lazy var currentUserUpdater: CurrentUserUpdater = self.environment
.currentUserUpdaterBuilder(
client.databaseContainer,
client.apiClient
)
/// The validator used to determine if messages can be marked as delivered.
private let deliveryCriteriaValidator: MessageDeliveryCriteriaValidating
/// A Boolean value that returns whether pagination is finished
public private(set) var hasLoadedAllPreviousChannels: Bool = false
/// A type-erased delegate.
var multicastDelegate: MulticastDelegate<ChatChannelListControllerDelegate> = .init() {
didSet {
stateMulticastDelegate.set(mainDelegate: multicastDelegate.mainDelegate)
stateMulticastDelegate.set(additionalDelegates: multicastDelegate.additionalDelegates)
// After setting delegate local changes will be fetched and observed.
startChannelListObserverIfNeeded()
}
}
private(set) lazy var channelListObserver: BackgroundListDatabaseObserver<ChatChannel, ChannelDTO> = {
if let updated = worker.loadPredefinedFilter(for: query) {
query = updated
}
return makeChannelListObserver()
}()
private func makeChannelListObserver() -> BackgroundListDatabaseObserver<ChatChannel, ChannelDTO> {
let request = ChannelDTO.channelListFetchRequest(query: self.query, chatClientConfig: client.config)
let observer = environment.createChannelListDatabaseObserver(
client.databaseContainer,
request,
{ try $0.asModel() },
query.runtimeSortingValues
)
observer.onDidChange = { [weak self] changes in
self?.delegateCallback { [weak self] in
guard let self = self else {
log.warning("Callback called while self is nil")
return
}
log.debug("didChangeChannels: \(changes.map(\.debugDescription))")
$0.controller(self, didChangeChannels: changes)
}
}
return observer
}
var _basePublishers: Any?
/// An internal backing object for all publicly available Combine publishers. We use it to simplify the way we expose
/// publishers. Instead of creating custom `Publisher` types, we use `CurrentValueSubject` and `PassthroughSubject` internally,
/// and expose the published values by mapping them to a read-only `AnyPublisher` type.
var basePublishers: BasePublishers {
if let value = _basePublishers as? BasePublishers {
return value
}
_basePublishers = BasePublishers(controller: self)
return _basePublishers as? BasePublishers ?? .init(controller: self)
}
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
)
/// Creates a new `ChannelListController`.
///
/// - Parameters:
/// - query: The query used for filtering the channels.
/// - client: The `Client` instance this controller belongs to.
/// - filter: A block that determines whether the channels belongs to this controller.
init(
query: ChannelListQuery,
client: ChatClient,
filter: (@Sendable (ChatChannel) -> Bool)? = nil,
environment: Environment = .init()
) {
self.client = client
self.query = query
self.filter = filter
self.environment = environment
self.deliveryCriteriaValidator = environment.deliveryCriteriaValidatorBuilder()
super.init()
}
override public func synchronize(_ completion: (@MainActor (_ error: Error?) -> Void)? = nil) {
startChannelListObserverIfNeeded()
channelListLinker.start(with: client.eventNotificationCenter)
client.syncRepository.startTrackingChannelListController(self)
updateChannelList { result in
completion?(result.error)
}
}
// MARK: - Actions
/// Loads next channels from backend.
///
/// - Parameters:
/// - limit: Limit for page size.
/// - completion: The completion. Will be called on a **callbackQueue** when the network request is finished.
/// If request fails, the completion will be called with an error.
///
public func loadNextChannels(
limit: Int? = nil,
completion: (@MainActor (Error?) -> Void)? = nil
) {
if hasLoadedAllPreviousChannels {
callback {
completion?(nil)
}
return
}
let limit = limit ?? query.pagination.pageSize
var updatedQuery = query
updatedQuery.pagination = Pagination(pageSize: limit, offset: channels.count)
worker.update(channelListQuery: updatedQuery) { result in
switch result {
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()
}
self.callback { completion?(nil) }
case let .failure(error):
self.callback { completion?(error) }
}
}
}
// MARK: - Internal
func refreshLoadedChannels(completion: @escaping @Sendable (Result<Set<ChannelId>, Error>) -> Void) {
let channelCount = channelListObserver.items.count
worker.refreshLoadedChannels(for: query, channelCount: channelCount, completion: completion)
}
// MARK: - Helpers
private func updateChannelList(
_ 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(updateResult):
self?.state = .remoteDataFetched
self?.hasLoadedAllPreviousChannels = updateResult.channels.count < limit
// Mark channels as delivered if synchronization was successful
self?.markChannelsAsDeliveredIfNeeded(channels: updateResult.channels)
// Predefined filters can update local query representation
if let updatedQuery = updateResult.updatedQuery {
self?.query = updatedQuery
self?.updateChannelListObserver()
}
self?.callback { completion?(.success(updateResult)) }
case let .failure(error):
self?.state = .remoteDataFetchFailed(ClientError(with: 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]) {
guard let currentUser = client.currentUserController().currentUser else { return }
// Extract channels that should be marked as delivered
let deliveries: [MessageDeliveryInfo] = channels.compactMap { channel in
guard let message = channel.latestMessages.first else {
return nil
}
guard deliveryCriteriaValidator.canMarkMessageAsDelivered(message, for: currentUser, in: channel) else {
return nil
}
return MessageDeliveryInfo(channelId: channel.cid, messageId: message.id)
}
// Only make the API call if there are channels to mark as delivered
guard !deliveries.isEmpty else { return }
// Mark channels as delivered
currentUserUpdater.markMessagesAsDelivered(deliveries) { error in
if let error = error {
log.error("Failed to mark channels as delivered: \(error)")
}
}
}
/// If the `state` of the controller is `initialized`, this method calls `startObserving` on the
/// `channelListObserver` to fetch the local data and start observing the changes. It also changes
/// `state` based on the result.
///
/// It's safe to call this method repeatedly.
///
private func startChannelListObserverIfNeeded() {
guard state == .initialized else { return }
do {
try channelListObserver.startObserving()
state = .localDataFetched
} catch {
state = .localDataFetchFailed(ClientError(with: error))
log.error("Failed to perform fetch request with error: \(error). This is an internal error.")
}
}
private func updateChannelListObserver() {
channelListObserver = makeChannelListObserver()
do {
try channelListObserver.startObserving()
} catch {
state = .localDataFetchFailed(ClientError(with: error))
log.error("Failed to update the channel list observer: \(error)")
}
}
}
extension ChatChannelListController {
struct Environment {
var channelQueryUpdaterBuilder: (
_ database: DatabaseContainer,
_ apiClient: APIClient
) -> ChannelListUpdater = ChannelListUpdater.init
var channelListLinkerBuilder: (
_ query: ChannelListQuery,
_ filter: (@Sendable (ChatChannel) -> Bool)?,
_ clientConfig: ChatClientConfig,
_ databaseContainer: DatabaseContainer,
_ worker: ChannelListUpdater,
_ channelWatcherHandler: ChannelWatcherHandling
) -> ChannelListLinker = ChannelListLinker.init
var currentUserUpdaterBuilder: (
_ database: DatabaseContainer,
_ apiClient: APIClient
) -> CurrentUserUpdater = CurrentUserUpdater.init
var deliveryCriteriaValidatorBuilder: () -> MessageDeliveryCriteriaValidating = {
MessageDeliveryCriteriaValidator()
}
var createChannelListDatabaseObserver: (
_ database: DatabaseContainer,
_ fetchRequest: NSFetchRequest<ChannelDTO>,
_ itemCreator: @escaping (ChannelDTO) throws -> ChatChannel,
_ sort: [SortValue<ChatChannel>]
)
-> BackgroundListDatabaseObserver<ChatChannel, ChannelDTO> = {
BackgroundListDatabaseObserver(
database: $0,
fetchRequest: $1,
itemCreator: $2,
itemReuseKeyPaths: (\ChatChannel.cid.rawValue, \ChannelDTO.cid),
runtimeSorting: $3
)
}
}
}
extension ChatChannelListController {
/// Set the delegate of `ChannelListController` to observe the changes in the system.
public weak var delegate: ChatChannelListControllerDelegate? {
get { multicastDelegate.mainDelegate }
set { multicastDelegate.set(mainDelegate: newValue) }
}
}
/// `ChatChannelListController` uses this protocol to communicate changes to its delegate.
public protocol ChatChannelListControllerDelegate: DataControllerStateDelegate {
/// The controller changed the list of observed channels.
///
/// - Parameters:
/// - controller: The controller emitting the change callback.
/// - changes: The change to the list of channels.\
///
func controller(
_ controller: ChatChannelListController,
didChangeChannels changes: [ListChange<ChatChannel>]
)
}
public extension ChatChannelListControllerDelegate {
func controller(
_ controller: ChatChannelListController,
didChangeChannels changes: [ListChange<ChatChannel>]
) {}
}
extension ClientError {
public final class FetchFailed: Error {
public let localizedDescription: String = "Failed to perform fetch request. This is an internal error."
}
}