diff --git a/Package.swift b/Package.swift index d959fcd441..17c6eec1c8 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.6 +// swift-tools-version:5.7 import Foundation import PackageDescription @@ -56,8 +56,6 @@ let package = Package( ] ) -#if swift(>=5.6) package.dependencies.append( .package(url: "https://github.com/apple/swift-docc-plugin", exact: "1.0.0") ) -#endif diff --git a/README.md b/README.md index 9166a4624f..e6c9c82f6e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@

- +

@@ -39,7 +39,7 @@ The **StreamChatSwiftUI SDK** is our UI SDK for SwiftUI components. If your appl - **Familiar behavior**: The UI elements are good platform citizens and behave like native elements; they respect `tintColor`, `layoutMargins`, light/dark mode, dynamic font sizes, etc. - **Swift native API:** Uses Swift's powerful language features to make the SDK usage easy and type-safe. - `UIKit` and `SwiftUI` SDKs use native patterns and paradigms from respective UI frameworks: The API follows the design of native system SDKs. It makes integration with your existing code easy and familiar. - - `UIKit` SDK is part of this repository whereas `SwiftUI` SDK is available [here](https://github.com/GetStream/stream-chat-swiftui). + - `UIKit` SDK is part of this repository whereas `SwiftUI` SDK is available [here](https://github.com/GetStream/stream-chat-swiftui). - **First-class support for `Combine` and `Structured Concurrency`**: Refer to our getting started guides for [Combine](https://getstream.io/chat/docs/sdk/ios/combine/) and [Structured Concurrency](https://getstream.io/chat/docs/sdk/ios/client/state-layer/state-layer-overview/). - **Fully open-source implementation:** You have access to the complete source code of the SDK here on GitHub. - **Supports iOS 13+:** We proudly support older versions of iOS, so your app can stay available to almost everyone. diff --git a/Sources/StreamChat/Config/StreamRuntimeCheck.swift b/Sources/StreamChat/Config/StreamRuntimeCheck.swift index 9c80676ea2..12f2a3873c 100644 --- a/Sources/StreamChat/Config/StreamRuntimeCheck.swift +++ b/Sources/StreamChat/Config/StreamRuntimeCheck.swift @@ -32,11 +32,6 @@ public enum StreamRuntimeCheck { /// Enables reusing unchanged converted items in database observers. public static var _isDatabaseObserverItemReusingEnabled = true - /// For *internal use* only - /// - /// Uses version 2 for offline state sync. - public static var _isSyncV2Enabled = true - /// For *internal use* only /// /// Core Data prefetches data used for creating immutable model objects (faulting is disabled). diff --git a/Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift b/Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift index fec5446a3c..ca74316729 100644 --- a/Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift +++ b/Sources/StreamChat/Controllers/ChannelListController/ChannelListController.swift @@ -197,28 +197,6 @@ public class ChatChannelListController: DataController, DelegateCallable, DataSt let channelCount = channelListObserver.items.count worker.refreshLoadedChannels(for: query, channelCount: channelCount, completion: completion) } - - func resetQuery( - watchedAndSynchedChannelIds: Set, - synchedChannelIds: Set, - completion: @escaping (Result<(synchedAndWatched: [ChatChannel], unwanted: Set), Error>) -> Void - ) { - let pageSize = query.pagination.pageSize - worker.resetChannelsQuery( - for: query, - pageSize: pageSize, - watchedAndSynchedChannelIds: watchedAndSynchedChannelIds, - synchedChannelIds: synchedChannelIds - ) { [weak self] result in - switch result { - case let .success((newChannels, unwantedCids)): - self?.hasLoadedAllPreviousChannels = newChannels.count < pageSize - completion(.success((newChannels, unwantedCids))) - case let .failure(error): - completion(.failure(error)) - } - } - } // MARK: - Helpers diff --git a/Sources/StreamChat/Database/DTOs/ChannelDTO.swift b/Sources/StreamChat/Database/DTOs/ChannelDTO.swift index b57d5583c3..9ed3f411b4 100644 --- a/Sources/StreamChat/Database/DTOs/ChannelDTO.swift +++ b/Sources/StreamChat/Database/DTOs/ChannelDTO.swift @@ -373,18 +373,6 @@ extension NSManagedObjectContext { delete(dto) } - func cleanChannels(cids: Set) { - let channels = ChannelDTO.load(cids: Array(cids), context: self) - for channelDTO in channels { - channelDTO.resetEphemeralValues() - channelDTO.messages.removeAll() - channelDTO.members.removeAll() - channelDTO.pinnedMessages.removeAll() - channelDTO.reads.removeAll() - channelDTO.oldestMessageAt = nil - } - } - func removeChannels(cids: Set) { let channels = ChannelDTO.load(cids: Array(cids), context: self) channels.forEach(delete) diff --git a/Sources/StreamChat/Database/DatabaseSession.swift b/Sources/StreamChat/Database/DatabaseSession.swift index 438f49d039..5a19d0f44f 100644 --- a/Sources/StreamChat/Database/DatabaseSession.swift +++ b/Sources/StreamChat/Database/DatabaseSession.swift @@ -302,9 +302,6 @@ protocol ChannelDatabaseSession { /// Removes channel list query from database. func delete(query: ChannelListQuery) - /// Cleans a list of channels based on their id - func cleanChannels(cids: Set) - /// Removes a list of channels based on their id func removeChannels(cids: Set) } diff --git a/Sources/StreamChat/Repositories/SyncOperations.swift b/Sources/StreamChat/Repositories/SyncOperations.swift index 2b3b0a93da..bead0bbac7 100644 --- a/Sources/StreamChat/Repositories/SyncOperations.swift +++ b/Sources/StreamChat/Repositories/SyncOperations.swift @@ -107,25 +107,6 @@ final class RefreshChannelListOperation: AsyncOperation, @unchecked Sendable { } } -final class GetChannelIdsOperation: AsyncOperation, @unchecked Sendable { - init(database: DatabaseContainer, context: SyncContext, activeChannelIds: [ChannelId]) { - super.init(maxRetries: syncOperationsMaximumRetries) { [weak database] _, done in - guard let database = database else { - done(.continue) - return - } - database.backgroundReadOnlyContext.perform { - let cids = database.backgroundReadOnlyContext.loadAllChannelListQueries() - .flatMap(\.channels) - .compactMap { try? ChannelId(cid: $0.cid) } - log.info("0. Retrieved channels from existing queries from DB. Count \(cids.count)", subsystems: .offlineSupport) - context.localChannelIds = Set(cids + activeChannelIds) - done(.continue) - } - } - } -} - final class SyncEventsOperation: AsyncOperation, @unchecked Sendable { init(syncRepository: SyncRepository, context: SyncContext, recovery: Bool) { super.init(maxRetries: syncOperationsMaximumRetries) { [weak syncRepository] _, done in @@ -208,96 +189,6 @@ final class WatchChannelOperation: AsyncOperation, @unchecked Sendable { } } -final class RefetchChannelListQueryOperation: AsyncOperation, @unchecked Sendable { - init(controller: ChatChannelListController, context: SyncContext) { - super.init(maxRetries: syncOperationsMaximumRetries) { [weak controller] _, done in - guard let controller = controller, controller.canBeRecovered else { - done(.continue) - return - } - - let query = controller.query - - log.info("3 & 4. Refetching channel lists queries & Cleaning up local message history", subsystems: .offlineSupport) - controller.resetQuery( - watchedAndSynchedChannelIds: context.watchedAndSynchedChannelIds, - synchedChannelIds: context.synchedChannelIds - ) { result in - Self.handleResult(result, query: query, context: context, done: done) - } - } - } - - init(query: ChannelListQuery, channelListUpdater: ChannelListUpdater, context: SyncContext) { - super.init(maxRetries: syncOperationsMaximumRetries) { _, done in - log.info("3 & 4. Refetching channel lists queries (step 2)", subsystems: .offlineSupport) - channelListUpdater.resetChannelsQuery( - for: query, - pageSize: query.pagination.pageSize, - watchedAndSynchedChannelIds: context.watchedAndSynchedChannelIds, - synchedChannelIds: context.synchedChannelIds - ) { result in - Self.handleResult(result, query: query, context: context, done: done) - } - } - } - - private static func handleResult( - _ result: Result<(synchedAndWatched: [ChatChannel], unwanted: Set), any Error>, - query: ChannelListQuery, - context: SyncContext, - done: (AsyncOperation.Output) -> Void - ) { - switch result { - case let .success((watchedChannels, unwantedCids)): - log.info("Successfully refetched query for \(query.debugDescription)", subsystems: .offlineSupport) - let queryChannelIds = watchedChannels.map(\.cid) - context.watchedAndSynchedChannelIds.formUnion(queryChannelIds) - context.unwantedChannelIds.formUnion(unwantedCids) - done(.continue) - case let .failure(error): - log.error( - "Failed refetching query for \(query.debugDescription): \(error)", - subsystems: .offlineSupport - ) - done(.retry) - } - } -} - -final class DeleteUnwantedChannelsOperation: AsyncOperation, @unchecked Sendable { - init(database: DatabaseContainer, context: SyncContext) { - super.init(maxRetries: syncOperationsMaximumRetries) { [weak database] _, done in - log.info("4. Clean up unwanted channels", subsystems: .offlineSupport) - - guard let database = database, !context.unwantedChannelIds.isEmpty else { - done(.continue) - return - } - - // We are going to remove those channels that are not present in remote queries, and that have not - // been watched. - database.write { session in - // We remove watchedAndSynched from unwantedChannels because it might happen that a channel marked - // as unwanted in one query, might still be needed in another query (scenario where multiple queries - // are active at the same time). - let idsToRemove = context.unwantedChannelIds.subtracting(context.watchedAndSynchedChannelIds) - session.removeChannels(cids: idsToRemove) - } completion: { error in - if let error = error { - log.error( - "Failed removing unwanted channels: \(error)", - subsystems: .offlineSupport - ) - done(.retry) - } else { - done(.continue) - } - } - } - } -} - final class ExecutePendingOfflineActions: AsyncOperation, @unchecked Sendable { init(offlineRequestsRepository: OfflineRequestsRepository) { super.init(maxRetries: syncOperationsMaximumRetries) { [weak offlineRequestsRepository] _, done in diff --git a/Sources/StreamChat/Repositories/SyncRepository.swift b/Sources/StreamChat/Repositories/SyncRepository.swift index 7ca233158c..2f146cce17 100644 --- a/Sources/StreamChat/Repositories/SyncRepository.swift +++ b/Sources/StreamChat/Repositories/SyncRepository.swift @@ -36,7 +36,6 @@ class SyncRepository { private let database: DatabaseContainer private let apiClient: APIClient private let channelListUpdater: ChannelListUpdater - var usesV2Sync = StreamRuntimeCheck._isSyncV2Enabled let offlineRequestsRepository: OfflineRequestsRepository let eventNotificationCenter: EventNotificationCenter @@ -137,15 +136,11 @@ class SyncRepository { } return } - if self?.usesV2Sync == true { - self?.syncLocalStateV2(lastSyncAt: lastSyncAt, completion: completion) - } else { - self?.syncLocalState(lastSyncAt: lastSyncAt, completion: completion) - } + self?.syncLocalState(lastSyncAt: lastSyncAt, completion: completion) } } - // MARK: - V2 + // MARK: - /// Runs offline tasks and updates the local state for channels /// @@ -161,7 +156,7 @@ class SyncRepository { /// * channel controllers targeting other channels /// * no channel lists active, but channel controllers are /// 4. Re-watch channels what we were watching before disconnect - private func syncLocalStateV2(lastSyncAt: Date, completion: @escaping () -> Void) { + private func syncLocalState(lastSyncAt: Date, completion: @escaping () -> Void) { let context = SyncContext(lastSyncAt: lastSyncAt) var operations: [Operation] = [] let start = CFAbsoluteTimeGetCurrent() @@ -213,96 +208,6 @@ class SyncRepository { } operationQueue.addOperations(operations, waitUntilFinished: false) } - - // MARK: - V1 - - /// Syncs the local state with the server to make sure the local database is up to date. - /// It features queuing, serialization and retries - /// - /// [Sync and watch channels](https://www.notion.so/2-Sync-and-watch-channels-ac44feb55de3482f8f0f99e100ca40c6) - /// 1. Call `/sync` endpoint and get missing events for all locally existed channels - /// 2. Start watching open channels - /// 3. Refetch channel lists queries, link only what backend returns (the 1st page) - /// 4. Clean up unwanted channels - /// 5. Run offline actions requests - /// - /// - Parameter completion: A block that will get executed upon completion of the synchronization - private func syncLocalState(lastSyncAt: Date, completion: @escaping () -> Void) { - log.info("Starting to recover offline state", subsystems: .offlineSupport) - let context = SyncContext(lastSyncAt: lastSyncAt) - var operations: [Operation] = [] - - // Enter recovery mode so no other requests are triggered. - apiClient.enterRecoveryMode() - - // Run offline actions requests as the first thing - if config.isLocalStorageEnabled { - operations.append(ExecutePendingOfflineActions(offlineRequestsRepository: offlineRequestsRepository)) - } - - // Get the existing channelIds - let activeChannelIds = activeChannelControllers.allObjects.compactMap(\.cid) - operations.append(GetChannelIdsOperation(database: database, context: context, activeChannelIds: activeChannelIds)) - - // 1. Call `/sync` endpoint and get missing events for all locally existed channels - operations.append(SyncEventsOperation(syncRepository: self, context: context, recovery: true)) - - // 2. Start watching open channels. - let watchChannelOperations: [AsyncOperation] = activeChannelControllers.allObjects.map { controller in - WatchChannelOperation(controller: controller, context: context, recovery: true) - } - operations.append(contentsOf: watchChannelOperations) - - // 3. Refetch channel lists queries, link only what backend returns (the 1st page) - // We use `context.synchedChannelIds` to keep track of the channels that were synched both in the previous step and - // after each ChannelListController recovery. - let refetchChannelListQueryOperations: [AsyncOperation] = activeChannelListControllers.allObjects - .map { controller in - RefetchChannelListQueryOperation( - controller: controller, - context: context - ) - } - operations.append(contentsOf: refetchChannelListQueryOperations) - - let channelListQueries: [ChannelListQuery] = { - let queries = activeChannelLists.allObjects - .map(\.query) - .map { ($0.filter.filterHash, $0) } - let uniqueQueries = Dictionary(queries, uniquingKeysWith: { _, last in last }) - return Array(uniqueQueries.values) - }() - operations.append(contentsOf: channelListQueries - .map { channelListQuery in - RefetchChannelListQueryOperation( - query: channelListQuery, - channelListUpdater: channelListUpdater, - context: context - ) - }) - - // 4. Clean up unwanted channels - operations.append(DeleteUnwantedChannelsOperation(database: database, context: context)) - - operations.append(BlockOperation(block: { [weak self] in - log.info("Finished recovering offline state", subsystems: .offlineSupport) - DispatchQueue.main.async { - self?.apiClient.exitRecoveryMode() - completion() - } - })) - - // We are making sure the operations happen sequentially one after the other by setting one as the dependency - // of the following one - var previousOperation: Operation? - operations.reversed().forEach { operation in - defer { previousOperation = operation } - guard let previousOperation = previousOperation else { return } - previousOperation.addDependency(operation) - } - - operationQueue.addOperations(operations, waitUntilFinished: false) - } /// Syncs the events for the active chat channels using the last sync date. /// - Parameter completion: A block that will get executed upon completion of the synchronization diff --git a/Sources/StreamChat/Workers/ChannelListUpdater.swift b/Sources/StreamChat/Workers/ChannelListUpdater.swift index 8fcd4156d1..5f8de35b89 100644 --- a/Sources/StreamChat/Workers/ChannelListUpdater.swift +++ b/Sources/StreamChat/Workers/ChannelListUpdater.swift @@ -42,8 +42,14 @@ class ChannelListUpdater: Worker { } func refreshLoadedChannels(for query: ChannelListQuery, channelCount: Int, completion: @escaping (Result, Error>) -> Void) { + guard channelCount > 0 else { + completion(.success(Set())) + return + } + var allPages = [ChannelListQuery]() - for offset in stride(from: 0, to: channelCount, by: .channelsPageSize) { + let pageSize = query.pagination.pageSize > 0 ? query.pagination.pageSize : .channelsPageSize + for offset in stride(from: 0, to: channelCount, by: pageSize) { var pageQuery = query pageQuery.pagination = Pagination(pageSize: .channelsPageSize, offset: offset) allPages.append(pageQuery) @@ -90,66 +96,6 @@ class ChannelListUpdater: Worker { } } } - - func resetChannelsQuery( - for query: ChannelListQuery, - pageSize: Int, - watchedAndSynchedChannelIds: Set, - synchedChannelIds: Set, - completion: @escaping (Result<(synchedAndWatched: [ChatChannel], unwanted: Set), Error>) -> Void - ) { - var updatedQuery = query - updatedQuery.pagination = .init(pageSize: pageSize, offset: 0) - - var unwantedCids = Set() - // Fetches the channels matching the query, and stores them in the database. - apiClient.recoveryRequest(endpoint: .channels(query: query)) { [weak self] result in - switch result { - case let .success(channelListPayload): - self?.writeChannelListPayload( - payload: channelListPayload, - query: updatedQuery, - initialActions: { session in - guard let queryDTO = session.channelListQuery(filterHash: updatedQuery.filter.filterHash) else { return } - - let localQueryCIDs = Set(queryDTO.channels.compactMap { try? ChannelId(cid: $0.cid) }) - let remoteQueryCIDs = Set(channelListPayload.channels.map(\.channel.cid)) - - let updatedChannels = synchedChannelIds.union(watchedAndSynchedChannelIds) - let localNotInRemote = localQueryCIDs.subtracting(remoteQueryCIDs) - let localInRemote = localQueryCIDs.intersection(remoteQueryCIDs) - - // We unlink those local channels that are no longer in remote - for cid in localNotInRemote { - guard let channelDTO = session.channel(cid: cid) else { continue } - queryDTO.channels.remove(channelDTO) - } - - // We are going to clean those channels that are present in the both the local and remote query, - // and that have not been synched nor watched. Those are outdated, can contain gaps. - let cidsToClean = localInRemote.subtracting(updatedChannels) - session.cleanChannels(cids: cidsToClean) - - // We are also going to keep track of the unwanted channels - // Those are the ones that exist locally but we are not interested in anymore in this context. - // In this case, it is going to query local ones not appearing in remote, subtracting the ones - // that are already being watched. - unwantedCids = localNotInRemote.subtracting(watchedAndSynchedChannelIds) - }, - completion: { result in - switch result { - case let .success(newSynchedAndWatchedChannels): - completion(.success((newSynchedAndWatchedChannels, unwantedCids))) - case let .failure(error): - completion(.failure(error)) - } - } - ) - case let .failure(error): - completion(.failure(error)) - } - } - } /// Starts watching the channels with the given ids and updates the channels in the local storage. /// diff --git a/StreamChat-XCFramework.podspec b/StreamChat-XCFramework.podspec index 18235e95b3..329896cf55 100644 --- a/StreamChat-XCFramework.podspec +++ b/StreamChat-XCFramework.podspec @@ -9,7 +9,7 @@ Pod::Spec.new do |spec| spec.author = { "getstream.io" => "support@getstream.io" } spec.social_media_url = "https://getstream.io" - spec.swift_version = '5.6' + spec.swift_version = '5.7' spec.ios.deployment_target = '13.0' spec.requires_arc = true diff --git a/StreamChat.podspec b/StreamChat.podspec index f747a731c1..62cc71e612 100644 --- a/StreamChat.podspec +++ b/StreamChat.podspec @@ -9,7 +9,7 @@ Pod::Spec.new do |spec| spec.author = { "getstream.io" => "support@getstream.io" } spec.social_media_url = "https://getstream.io" - spec.swift_version = '5.6' + spec.swift_version = '5.7' spec.ios.deployment_target = '13.0' spec.osx.deployment_target = '11.0' spec.requires_arc = true diff --git a/StreamChatUI-XCFramework.podspec b/StreamChatUI-XCFramework.podspec index 9bf3e2a0be..ae645a2099 100644 --- a/StreamChatUI-XCFramework.podspec +++ b/StreamChatUI-XCFramework.podspec @@ -9,7 +9,7 @@ Pod::Spec.new do |spec| spec.author = { "getstream.io" => "support@getstream.io" } spec.social_media_url = "https://getstream.io" - spec.swift_version = '5.6' + spec.swift_version = '5.7' spec.platform = :ios, "13.0" spec.requires_arc = true diff --git a/StreamChatUI.podspec b/StreamChatUI.podspec index 2f1ee75243..3d7c0bd19e 100644 --- a/StreamChatUI.podspec +++ b/StreamChatUI.podspec @@ -9,7 +9,7 @@ Pod::Spec.new do |spec| spec.author = { "getstream.io" => "support@getstream.io" } spec.social_media_url = "https://getstream.io" - spec.swift_version = '5.6' + spec.swift_version = '5.7' spec.platform = :ios, "13.0" spec.requires_arc = true diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelListController_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelListController_Mock.swift index b35e0a8182..22a2ae8e44 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelListController_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Controllers/ChatChannelListController_Mock.swift @@ -9,7 +9,6 @@ class ChatChannelListController_Mock: ChatChannelListController, Spy { let spyState = SpyState() var loadNextChannelsIsCalled = false var loadNextChannelsCallCount = 0 - var resetChannelsQueryResult: Result<(synchedAndWatched: [ChatChannel], unwanted: Set), Error>? var refreshLoadedChannelsResult: Result, any Error>? /// Creates a new mock instance of `ChatChannelListController`. @@ -37,15 +36,6 @@ class ChatChannelListController_Mock: ChatChannelListController, Spy { record() refreshLoadedChannelsResult.map(completion) } - - override func resetQuery( - watchedAndSynchedChannelIds: Set, - synchedChannelIds: Set, - completion: @escaping (Result<(synchedAndWatched: [ChatChannel], unwanted: Set), Error>) -> Void - ) { - record() - resetChannelsQueryResult.map(completion) - } } extension ChatChannelListController_Mock { diff --git a/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift b/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift index e1e9686379..c82d7439b1 100644 --- a/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift +++ b/TestTools/StreamChatTestTools/Mocks/StreamChat/Database/DatabaseSession_Mock.swift @@ -98,10 +98,6 @@ class DatabaseSession_Mock: DatabaseSession { underlyingSession.deleteQuery(query) } - func cleanChannels(cids: Set) { - underlyingSession.cleanChannels(cids: cids) - } - func removeChannels(cids: Set) { underlyingSession.removeChannels(cids: cids) } diff --git a/TestTools/StreamChatTestTools/SpyPattern/Spy/ChannelListUpdater_Spy.swift b/TestTools/StreamChatTestTools/SpyPattern/Spy/ChannelListUpdater_Spy.swift index b2784a9729..fe9aa5510b 100644 --- a/TestTools/StreamChatTestTools/SpyPattern/Spy/ChannelListUpdater_Spy.swift +++ b/TestTools/StreamChatTestTools/SpyPattern/Spy/ChannelListUpdater_Spy.swift @@ -16,7 +16,7 @@ final class ChannelListUpdater_Spy: ChannelListUpdater, Spy { @Atomic var fetch_queries: [ChannelListQuery] = [] @Atomic var fetch_completion: ((Result) -> Void)? - var resetChannelsQueryResult: Result<(synchedAndWatched: [ChatChannel], unwanted: Set), Error>? + @Atomic var refreshLoadedChannelsResult: Result, Error>? @Atomic var markAllRead_completion: ((Error?) -> Void)? @@ -63,16 +63,14 @@ final class ChannelListUpdater_Spy: ChannelListUpdater, Spy { _fetch_queries.mutate { $0.append(channelListQuery) } fetch_completion = completion } - - override func resetChannelsQuery( + + override func refreshLoadedChannels( for query: ChannelListQuery, - pageSize: Int, - watchedAndSynchedChannelIds: Set, - synchedChannelIds: Set, - completion: @escaping (Result<(synchedAndWatched: [ChatChannel], unwanted: Set), Error>) -> Void + channelCount: Int, + completion: @escaping (Result, any Error>) -> Void ) { record() - resetChannelsQueryResult.map(completion) + refreshLoadedChannelsResult?.invoke(with: completion) } override func link( diff --git a/Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift b/Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift index add00a7a6a..1b13597628 100644 --- a/Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift +++ b/Tests/StreamChatTests/Controllers/ChannelListController/ChannelListController_Tests.swift @@ -818,11 +818,9 @@ final class ChannelListController_Tests: XCTestCase { AssertAsync.canBeReleased(&weakController) } - // MARK: - Reset query - - func test_resetQuery_whenSucceeds_updates_hasLoadedAllPreviousChannels_whenRecevingAFullPage() { - XCTAssertFalse(controller.hasLoadedAllPreviousChannels) + // MARK: - Refresh Loaded Channels + func test_refreshLoadedChannels_whenSucceedsThenControllerSucceeds() { // Simulate synchronize to create all dependencies controller.synchronize() @@ -830,51 +828,20 @@ final class ChannelListController_Tests: XCTestCase { let channels: [ChatChannel] = (0.. [ChannelDTO] { try database.viewContext.fetch(ChannelDTO.allChannelsFetchRequest) } diff --git a/Tests/StreamChatTests/Repositories/SyncRepository_Tests.swift b/Tests/StreamChatTests/Repositories/SyncRepository_Tests.swift index 76df92b83d..030e1a2579 100644 --- a/Tests/StreamChatTests/Repositories/SyncRepository_Tests.swift +++ b/Tests/StreamChatTests/Repositories/SyncRepository_Tests.swift @@ -6,34 +6,6 @@ @testable import StreamChatTestTools import XCTest -class SyncRepositoryV2_Tests: SyncRepository_Tests { - override func setUp() { - super.setUp() - repository.usesV2Sync = true - } - - func test_syncLocalEvents_bySkippingAlreadyFetchedChannelIds() throws { - let lastSyncDate = Date() - let cid = ChannelId.unique - try prepareForSyncLocalStorage( - createUser: true, - lastSynchedEventDate: lastSyncDate, - createChannel: true, - cid: cid - ) - - // One channel list controller which fetches the state for cid - let chatListController = ChatChannelListController_Mock(query: .init(filter: .exists(.cid)), client: client) - chatListController.state_mock = .remoteDataFetched - chatListController.channels_mock = [.mock(cid: cid)] - repository.startTrackingChannelListController(chatListController) - chatListController.refreshLoadedChannelsResult = .success(Set([cid])) - - // If it fails, it means /sync was called but we expect it to be skipped because channel list refresh already refreshed the channel - waitForSyncLocalStateRun() - } -} - class SyncRepository_Tests: XCTestCase { var client: ChatClient_Mock! var offlineRequestsRepository: OfflineRequestsRepository_Mock! @@ -71,7 +43,6 @@ class SyncRepository_Tests: XCTestCase { apiClient: apiClient, channelListUpdater: channelListUpdater ) - repository.usesV2Sync = false } override func tearDown() { @@ -187,33 +158,6 @@ class SyncRepository_Tests: XCTestCase { XCTAssertCall("runQueuedRequests(completion:)", on: offlineRequestsRepository, times: 1) } - func test_syncLocalState_localStorageEnabled_pendingConnectionDate_channels() throws { - try XCTSkipIf(repository.usesV2Sync, "V2 only syncs if there are active controllers") - - let channelId = ChannelId.unique - try prepareForSyncLocalStorage( - createUser: true, - lastSynchedEventDate: Date().addingTimeInterval(-3600), - createChannel: true, - cid: channelId - ) - - let firstEventDate = Date.unique - let secondEventDate = Date.unique - let payload = messageEventPayload(cid: channelId, with: [firstEventDate, secondEventDate]) - waitForSyncLocalStateRun(requestResult: .success(payload)) - - // Should use first event's created at date - XCTAssertEqual(lastSyncAtValue, secondEventDate) - // Write: API Response, lastSyncAt - XCTAssertEqual(database.writeSessionCounter, 2) - XCTAssertEqual(repository.activeChannelControllers.count, 0) - XCTAssertEqual(repository.activeChannelListControllers.count, 0) - XCTAssertEqual(apiClient.recoveryRequest_allRecordedCalls.count, 1) - XCTAssertEqual(apiClient.request_allRecordedCalls.count, 0) - XCTAssertCall("runQueuedRequests(completion:)", on: offlineRequestsRepository, times: 1) - } - func test_syncLocalState_localStorageEnabled_pendingConnectionDate_channels_activeRemoteChannelController() throws { let cid = ChannelId.unique try prepareForSyncLocalStorage( @@ -242,19 +186,11 @@ class SyncRepository_Tests: XCTestCase { // Write: API Response, lastSyncAt XCTAssertEqual(database.writeSessionCounter, 2) XCTAssertEqual(repository.activeChannelControllers.count, 1) - if repository.usesV2Sync { - XCTAssertCall("watch()", on: chat, times: 1) - } else { - XCTAssertCall("recoverWatchedChannel(recovery:completion:)", on: chatController, times: 1) - } + XCTAssertCall("watch()", on: chat, times: 1) + XCTAssertEqual(repository.activeChannelListControllers.count, 0) - if repository.usesV2Sync { - XCTAssertEqual(apiClient.recoveryRequest_allRecordedCalls.count, 0) - XCTAssertEqual(apiClient.request_allRecordedCalls.count, 1) - } else { - XCTAssertEqual(apiClient.recoveryRequest_allRecordedCalls.count, 1) - XCTAssertEqual(apiClient.request_allRecordedCalls.count, 0) - } + XCTAssertEqual(apiClient.recoveryRequest_allRecordedCalls.count, 0) + XCTAssertEqual(apiClient.request_allRecordedCalls.count, 1) XCTAssertCall("runQueuedRequests(completion:)", on: offlineRequestsRepository, times: 1) } @@ -271,11 +207,7 @@ class SyncRepository_Tests: XCTestCase { chatListController.state_mock = .remoteDataFetched chatListController.channels_mock = [.mock(cid: cid)] repository.startTrackingChannelListController(chatListController) - if repository.usesV2Sync { - chatListController.refreshLoadedChannelsResult = .success(Set()) - } else { - chatListController.resetChannelsQueryResult = .success(([], [])) - } + chatListController.refreshLoadedChannelsResult = .success(Set()) let eventDate = Date.unique waitForSyncLocalStateRun(requestResult: .success(messageEventPayload(cid: cid, with: [eventDate]))) @@ -286,57 +218,12 @@ class SyncRepository_Tests: XCTestCase { XCTAssertEqual(database.writeSessionCounter, 2) XCTAssertEqual(repository.activeChannelControllers.count, 0) XCTAssertEqual(repository.activeChannelListControllers.count, 1) - if repository.usesV2Sync { - XCTAssertCall( - "refreshLoadedChannels(completion:)", on: chatListController, - times: 1 - ) - XCTAssertEqual(apiClient.recoveryRequest_allRecordedCalls.count, 0) - XCTAssertEqual(apiClient.request_allRecordedCalls.count, 1) - } else { - XCTAssertCall( - "resetQuery(watchedAndSynchedChannelIds:synchedChannelIds:completion:)", on: chatListController, - times: 1 - ) - XCTAssertEqual(apiClient.recoveryRequest_allRecordedCalls.count, 1) - XCTAssertEqual(apiClient.request_allRecordedCalls.count, 0) - } - XCTAssertCall("runQueuedRequests(completion:)", on: offlineRequestsRepository, times: 1) - } - - func test_syncLocalState_localStorageEnabled_pendingConnectionDate_channels_activeRemoteChannelListController_unwantedChannels( - ) throws { - try XCTSkipIf(repository.usesV2Sync, "V2 does not handle unwanted channels") - - let cid = ChannelId.unique - try prepareForSyncLocalStorage( - createUser: true, - lastSynchedEventDate: Date().addingTimeInterval(-3600), - createChannel: true, - cid: cid - ) - - let chatListController = ChatChannelListController_Mock(query: .init(filter: .exists(.cid)), client: client) - chatListController.state_mock = .remoteDataFetched - repository.startTrackingChannelListController(chatListController) - let unwantedId = ChannelId.unique - chatListController.resetChannelsQueryResult = .success(([], [unwantedId])) - - let eventDate = Date.unique - waitForSyncLocalStateRun(requestResult: .success(messageEventPayload(cid: cid, with: [eventDate]))) - - // Should use first event's created at date - XCTAssertEqual(lastSyncAtValue, eventDate) - // Write: API Response, unwanted channels, lastSyncAt - XCTAssertEqual(database.writeSessionCounter, 3) - XCTAssertEqual(repository.activeChannelControllers.count, 0) - XCTAssertEqual(repository.activeChannelListControllers.count, 1) XCTAssertCall( - "resetQuery(watchedAndSynchedChannelIds:synchedChannelIds:completion:)", on: chatListController, + "refreshLoadedChannels(completion:)", on: chatListController, times: 1 ) - XCTAssertEqual(apiClient.recoveryRequest_allRecordedCalls.count, 1) - XCTAssertEqual(apiClient.request_allRecordedCalls.count, 0) + XCTAssertEqual(apiClient.recoveryRequest_allRecordedCalls.count, 0) + XCTAssertEqual(apiClient.request_allRecordedCalls.count, 1) XCTAssertCall("runQueuedRequests(completion:)", on: offlineRequestsRepository, times: 1) } @@ -351,9 +238,7 @@ class SyncRepository_Tests: XCTestCase { ) let channelController = ChatChannelController_Mock(channelQuery: ChannelQuery(cid: .unique), channelListQuery: nil, client: client) - if repository.usesV2Sync { - repository.startTrackingChannelController(channelController) - } + repository.startTrackingChannelController(channelController) let firstDate = lastSyncDate.addingTimeInterval(1) let secondDate = lastSyncDate.addingTimeInterval(2) @@ -370,6 +255,27 @@ class SyncRepository_Tests: XCTestCase { repository.stopTrackingChannelController(channelController) } + + func test_syncLocalEvents_bySkippingAlreadyFetchedChannelIds() throws { + let lastSyncDate = Date() + let cid = ChannelId.unique + try prepareForSyncLocalStorage( + createUser: true, + lastSynchedEventDate: lastSyncDate, + createChannel: true, + cid: cid + ) + + // One channel list controller which fetches the state for cid + let chatListController = ChatChannelListController_Mock(query: .init(filter: .exists(.cid)), client: client) + chatListController.state_mock = .remoteDataFetched + chatListController.channels_mock = [.mock(cid: cid)] + repository.startTrackingChannelListController(chatListController) + chatListController.refreshLoadedChannelsResult = .success(Set([cid])) + + // If it fails, it means /sync was called but we expect it to be skipped because channel list refresh already refreshed the channel + waitForSyncLocalStateRun() + } // MARK: - Sync existing channels events @@ -695,63 +601,6 @@ class SyncRepository_Tests: XCTestCase { // THEN waitForExpectations(timeout: defaultTimeout, handler: nil) } - - func test_cancelRecoveryFlow_cancelsAllOperations() throws { - try XCTSkipIf(repository.usesV2Sync, "V2 has different implementation") - // Prepare environment - try prepareForSyncLocalStorage( - createUser: true, - lastSynchedEventDate: Date(), - createChannel: true - ) - - // Add active channel component - let channelQuery = ChannelQuery(cid: .unique) - let channelController = ChatChannelController(channelQuery: channelQuery, channelListQuery: nil, client: client) - channelController.state = .remoteDataFetched - repository.startTrackingChannelController(channelController) - - // Add active channel list component - let channelListController = ChatChannelListController_Mock(query: .init(filter: .exists(.cid)), client: client) - channelListController.state_mock = .remoteDataFetched - repository.startTrackingChannelListController(channelListController) - - // Sync local state - var completionCalled = false - repository.syncLocalState { - completionCalled = true - } - - // Wait for /sync to be called - if repository.usesV2Sync { - apiClient.waitForRequest() - } else { - apiClient.waitForRecoveryRequest() - } - - // Let /sync operation to complete - let syncResponse = Result.success(.init(eventPayloads: [])) - apiClient.test_simulateRecoveryResponse(syncResponse) - apiClient.recoveryRequest_completion = nil - - // Wait for watch operation - if !repository.usesV2Sync { - AssertAsync.willBeTrue(apiClient.recoveryRequest_completion != nil) - } - - // Cancel recovery flow - repository.cancelRecoveryFlow() - - // Let watch operation to complete - let watchResponse = Result.success(dummyPayload(with: channelQuery.cid!)) - apiClient.test_simulateRecoveryResponse(watchResponse) - - // Assert left operations are not executed - AssertAsync { - Assert.staysTrue(channelListController.recordedFunctions.isEmpty) - Assert.staysFalse(completionCalled) - } - } // MARK: - Tracking @@ -852,28 +701,13 @@ extension SyncRepository_Tests { expectation.fulfill() } - if !repository.usesV2Sync { - AssertAsync.willBeTrue( - "enterRecoveryMode()".wasCalled(on: apiClient, times: 1) - ) - } - if let result = requestResult { - if repository.usesV2Sync { - apiClient.waitForRequest() - guard let callback = apiClient.request_completion as? (Result) -> Void else { - XCTFail("A request for /sync should have been executed") - return - } - callback(result) - } else { - apiClient.waitForRecoveryRequest() - guard let callback = apiClient.recoveryRequest_completion as? (Result) -> Void else { - XCTFail("A request for /sync should have been executed") - return - } - callback(result) + apiClient.waitForRequest() + guard let callback = apiClient.request_completion as? (Result) -> Void else { + XCTFail("A request for /sync should have been executed") + return } + callback(result) } waitForExpectations(timeout: defaultTimeout, handler: nil) diff --git a/Tests/StreamChatTests/StateLayer/ChannelList_Tests.swift b/Tests/StreamChatTests/StateLayer/ChannelList_Tests.swift index 116e82ef80..79a9a1af8f 100644 --- a/Tests/StreamChatTests/StateLayer/ChannelList_Tests.swift +++ b/Tests/StreamChatTests/StateLayer/ChannelList_Tests.swift @@ -237,7 +237,7 @@ final class ChannelList_Tests: XCTestCase { try await env.client.mockDatabaseContainer.write { session in session.saveChannelList(payload: incomingChannelListPayload, query: self.channelList.query) } - await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout) + await fulfillment(of: [expectation], timeout: defaultTimeout) cancellable.cancel() } @@ -266,7 +266,7 @@ final class ChannelList_Tests: XCTestCase { try await env.client.mockDatabaseContainer.write { session in session.saveChannelList(payload: incomingChannelListPayload, query: self.channelList.query) } - await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout) + await fulfillment(of: [expectation], timeout: defaultTimeout) cancellable.cancel() } @@ -303,7 +303,7 @@ final class ChannelList_Tests: XCTestCase { try await env.client.mockDatabaseContainer.write { session in session.saveChannelList(payload: incomingChannelListPayload, query: self.channelList.query) } - await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout) + await fulfillment(of: [expectation], timeout: defaultTimeout) cancellable.cancel() } @@ -339,7 +339,7 @@ final class ChannelList_Tests: XCTestCase { try await env.client.mockDatabaseContainer.write { session in session.saveChannelList(payload: incomingChannelListPayload, query: self.channelList.query) } - await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout) + await fulfillment(of: [expectation], timeout: defaultTimeout) cancellable.cancel() } @@ -381,7 +381,7 @@ final class ChannelList_Tests: XCTestCase { let eventExpectation = XCTestExpectation(description: "Event processed") env.client.eventNotificationCenter.process([event], completion: { eventExpectation.fulfill() }) - await fulfillmentCompatibility(of: [eventExpectation, stateExpectation], timeout: defaultTimeout, enforceOrder: true) + await fulfillment(of: [eventExpectation, stateExpectation], timeout: defaultTimeout, enforceOrder: true) cancellable.cancel() } @@ -414,7 +414,7 @@ final class ChannelList_Tests: XCTestCase { ) let eventExpectation = XCTestExpectation(description: "Event processed") env.client.eventNotificationCenter.process([event], completion: { eventExpectation.fulfill() }) - await fulfillmentCompatibility(of: [eventExpectation], timeout: defaultTimeout, enforceOrder: true) + await fulfillment(of: [eventExpectation], timeout: defaultTimeout, enforceOrder: true) cancellable.cancel() } @@ -539,18 +539,3 @@ extension ChannelList_Tests { } } } - -extension XCTestCase { - func fulfillmentCompatibility(of expectations: [XCTestExpectation], timeout seconds: TimeInterval, enforceOrder enforceOrderOfFulfillment: Bool = false) async { - #if swift(>=5.8) - await fulfillment(of: expectations, timeout: seconds, enforceOrder: enforceOrderOfFulfillment) - #else - await withCheckedContinuation { continuation in - Thread.detachNewThread { [self] in - wait(for: expectations, timeout: seconds, enforceOrder: enforceOrderOfFulfillment) - continuation.resume() - } - } - #endif - } -} diff --git a/Tests/StreamChatTests/StateLayer/Chat_Tests.swift b/Tests/StreamChatTests/StateLayer/Chat_Tests.swift index b0b8ee1649..0c3abf60a9 100644 --- a/Tests/StreamChatTests/StateLayer/Chat_Tests.swift +++ b/Tests/StreamChatTests/StateLayer/Chat_Tests.swift @@ -662,7 +662,7 @@ final class Chat_Tests: XCTestCase { messageId: apiResponse.message.id ) - await fulfillmentCompatibility(of: [notificationExpectation], timeout: defaultTimeout) + await fulfillment(of: [notificationExpectation], timeout: defaultTimeout) XCTAssertEqual(text, message.text) await XCTAssertEqual(1, chat.state.messages.count) @@ -1304,7 +1304,7 @@ final class Chat_Tests: XCTestCase { ) XCTAssertEqual(apiResponse.message.id, replyMessage.id) - await fulfillmentCompatibility(of: [notificationExpectation], timeout: defaultTimeout) + await fulfillment(of: [notificationExpectation], timeout: defaultTimeout) let messageState = try await chat.messageState(for: lastMessageId) await XCTAssertEqual(lastMessageId, messageState.message.id) diff --git a/Tests/StreamChatTests/StateLayer/StateLayerDatabaseObserver_Tests.swift b/Tests/StreamChatTests/StateLayer/StateLayerDatabaseObserver_Tests.swift index 53842d40ef..1e3b77d4d5 100644 --- a/Tests/StreamChatTests/StateLayer/StateLayerDatabaseObserver_Tests.swift +++ b/Tests/StreamChatTests/StateLayer/StateLayerDatabaseObserver_Tests.swift @@ -42,7 +42,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase { try await waitForDuplicateCallbacks() - await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout) + await fulfillment(of: [expectation], timeout: defaultTimeout) XCTAssertEqual("first", observer.item?.name) XCTAssertEqual(1, changeCount) @@ -70,7 +70,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase { try await waitForDuplicateCallbacks() - await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout) + await fulfillment(of: [expectation], timeout: defaultTimeout) XCTAssertEqual("second", observer.item?.name) XCTAssertEqual(1, changeCount) @@ -98,7 +98,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase { try await waitForDuplicateCallbacks() - await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout) + await fulfillment(of: [expectation], timeout: defaultTimeout) XCTAssertEqual("second", observer.item?.name) XCTAssertEqual("team2", observer.item?.team) @@ -128,7 +128,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase { try await waitForDuplicateCallbacks() - await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout) + await fulfillment(of: [expectation], timeout: defaultTimeout) XCTAssertEqual(8, observer.items.count) let expectedIds = (firstPayload.messages + secondPayload.messages).map(\.id) @@ -161,7 +161,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase { try await waitForDuplicateCallbacks() - await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout) + await fulfillment(of: [expectation], timeout: defaultTimeout) XCTAssertEqual(3, observer.items.count) XCTAssertEqual(secondPayload.messages.map(\.id), observer.items.map(\.id)) @@ -195,7 +195,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase { try await waitForDuplicateCallbacks() - await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout) + await fulfillment(of: [expectation], timeout: defaultTimeout) XCTAssertEqual(11, observer.items.count) let expectedIds = (firstPayload.messages + secondPayload.messages + thirdPayload.messages).map(\.id) @@ -254,7 +254,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase { try session.saveChannel(payload: channelPayload, query: query, cache: nil) } - await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout) + await fulfillment(of: [expectation], timeout: defaultTimeout) // 4 are reused, 1 is created XCTAssertEqual(6, itemCreatorCounter) @@ -279,7 +279,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase { try session.saveChannel(payload: secondPayload) } - await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout) + await fulfillment(of: [expectation], timeout: defaultTimeout) // 5 are reused, 5 are created XCTAssertEqual(15, messageItemCreatorCounter) @@ -329,7 +329,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase { try session.saveReaction(payload: makePayload(1).reactions[0], query: query, cache: nil) } - await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout) + await fulfillment(of: [expectation], timeout: defaultTimeout) // 4 are reused, 1 is created XCTAssertEqual(6, itemCreatorCounter) @@ -365,7 +365,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase { try session.saveThread(payload: makePayload(1).threads[0], cache: nil) } - await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout) + await fulfillment(of: [expectation], timeout: defaultTimeout) // 4 are reused, 1 is created XCTAssertEqual(6, itemCreatorCounter) @@ -405,7 +405,7 @@ final class StateLayerDatabaseObserver_Tests: XCTestCase { try session.saveUser(payload: makePayload(1).users[0]) } - await fulfillmentCompatibility(of: [expectation], timeout: defaultTimeout) + await fulfillment(of: [expectation], timeout: defaultTimeout) // 4 are reused, 1 is created XCTAssertEqual(6, itemCreatorCounter) diff --git a/Tests/StreamChatTests/StateLayer/UserSearch_Tests.swift b/Tests/StreamChatTests/StateLayer/UserSearch_Tests.swift index b73b0a2d4f..4ac4f1383f 100644 --- a/Tests/StreamChatTests/StateLayer/UserSearch_Tests.swift +++ b/Tests/StreamChatTests/StateLayer/UserSearch_Tests.swift @@ -75,7 +75,7 @@ final class UserSearch_Tests: XCTestCase { expectation1.fulfill() } - await fulfillmentCompatibility(of: [expectation1], timeout: defaultTimeout) + await fulfillment(of: [expectation1], timeout: defaultTimeout) // Search for "name" async let result2 = try await userSearch.search(term: "name") @@ -84,7 +84,7 @@ final class UserSearch_Tests: XCTestCase { expectation2.fulfill() } - await fulfillmentCompatibility(of: [expectation2], timeout: defaultTimeout) + await fulfillment(of: [expectation2], timeout: defaultTimeout) XCTAssertEqual(2, env.userListUpdaterMock.fetch_completions.count) diff --git a/Tests/StreamChatTests/Workers/ChannelListUpdater_Tests.swift b/Tests/StreamChatTests/Workers/ChannelListUpdater_Tests.swift index 8a61c77cb0..aa8d19f08f 100644 --- a/Tests/StreamChatTests/Workers/ChannelListUpdater_Tests.swift +++ b/Tests/StreamChatTests/Workers/ChannelListUpdater_Tests.swift @@ -226,170 +226,6 @@ final class ChannelListUpdater_Tests: XCTestCase { XCTAssertEqual(channelsFromQuery.count, 3) } - // MARK: - Reset Channels Query - - func test_resetChannelsQueryGreenPath() throws { - var query = ChannelListQuery(filter: .in(.members, values: [.unique])) - query.pagination = Pagination(pageSize: 10, offset: 4) - - try database.writeSynchronously { session in - session.saveQuery(query: query) - } - - let expectation = self.expectation(description: "resetChannelsQuery completion") - var receivedResult: Result<(synchedAndWatched: [ChatChannel], unwanted: Set), Error>! - listUpdater.resetChannelsQuery( - for: query, - pageSize: query.pagination.pageSize, - watchedAndSynchedChannelIds: Set(), - synchedChannelIds: Set() - ) { result in - receivedResult = result - expectation.fulfill() - } - - // Simulate API response with channel data - let cid = ChannelId(type: .messaging, id: .unique) - let payload = ChannelListPayload(channels: [dummyPayload(with: cid)]) - apiClient.test_simulateRecoveryResponse(.success(payload)) - - waitForExpectations(timeout: defaultTimeout, handler: nil) - - let requests = apiClient.recoveryRequest_allRecordedCalls - XCTAssertEqual(requests.count, 1) - XCTAssertFalse(receivedResult.isError) - - // Should reset pagination - query.pagination = Pagination(pageSize: 20, offset: 0) - let expectedBody = ["payload": query] - XCTAssertEqual(requests.first?.0.body, expectedBody.asAnyEncodable) - } - - func test_resetChannelsQuery_QueryNotInDatabase() throws { - let userId = "UserId" - var query = ChannelListQuery(filter: .in(.members, values: [userId])) - try database.writeSynchronously { session in - try session.saveUser(payload: .dummy(userId: userId)) - } - - let expectation = self.expectation(description: "resetChannelsQuery completion") - var receivedResult: Result<(synchedAndWatched: [ChatChannel], unwanted: Set), Error>! - listUpdater.resetChannelsQuery( - for: query, - pageSize: query.pagination.pageSize, - watchedAndSynchedChannelIds: Set(), - synchedChannelIds: Set() - ) { result in - receivedResult = result - expectation.fulfill() - } - - // Simulate API response with channel data - let cid = ChannelId(type: .messaging, id: "newChannel") - let payload = ChannelListPayload( - channels: [dummyPayload(with: cid, members: [.dummy(user: .dummy(userId: userId))])] - ) - apiClient.test_simulateRecoveryResponse(.success(payload)) - - waitForExpectations(timeout: defaultTimeout, handler: nil) - - let requests = apiClient.recoveryRequest_allRecordedCalls - XCTAssertEqual(requests.count, 1) - XCTAssertNil(receivedResult.error) - - // If the query does not exist, the payload should still be saved in database - XCTAssertEqual(channels(for: query, database: database).count, 1) - - // Should reset pagination - query.pagination = Pagination(pageSize: 20, offset: 0) - let expectedBody = ["payload": query] - XCTAssertEqual(requests.first?.0.body, expectedBody.asAnyEncodable) - } - - func test_resetChannelsQuery_shouldOnlyRemoveOutdatedAndNotWatchedChannels() throws { - // Preparation of the environment - let userId = "UserId" - let query = ChannelListQuery(filter: .in(.members, values: [userId])) - - let syncedId1 = ChannelId(type: .messaging, id: "syncedId1") - let syncedId2 = ChannelId(type: .messaging, id: "syncedId2") - let localId = ChannelId(type: .messaging, id: "localId") - let outdatedId = ChannelId(type: .messaging, id: "outdatedId") - let watchedAndSynchedId = ChannelId(type: .messaging, id: "watchedAndSynchedId") - let syncedAndWatchedId = ChannelId(type: .messaging, id: "syncedAndWatchedId") - let newRemoteChannel = ChannelId(type: .messaging, id: "newRemoteChannel") - let watchedChannelIds = Set([syncedAndWatchedId, watchedAndSynchedId]) - let synchedChannelIds = Set([syncedId1, syncedId2, syncedAndWatchedId]) - - try database.writeSynchronously { session in - try session.saveUser(payload: .dummy(userId: userId)) - try [syncedId1, syncedId2, outdatedId, watchedAndSynchedId, syncedAndWatchedId, localId].forEach { - let payload = self.dummyPayload(with: $0, members: [.dummy(user: .dummy(userId: userId))]) - try session.saveChannel(payload: payload, query: query, cache: nil) - } - } - - XCTAssertEqual(channels(for: query, database: database).count, 6) - - // Reset Channels Query - let expectation = self.expectation(description: "resetChannelsQuery completion") - var receivedResult: Result<(synchedAndWatched: [ChatChannel], unwanted: Set), Error>! - listUpdater.resetChannelsQuery( - for: query, - pageSize: query.pagination.pageSize, - watchedAndSynchedChannelIds: watchedChannelIds, - synchedChannelIds: synchedChannelIds - ) { result in - receivedResult = result - expectation.fulfill() - } - - // Simulate API response with channel data - let payload = ChannelListPayload(channels: [syncedAndWatchedId, syncedId2, newRemoteChannel, localId].map { - self.dummyPayload(with: $0, numberOfMessages: 0, members: [.dummy(user: .dummy(userId: userId))]) - }) - apiClient.test_simulateRecoveryResponse(.success(payload)) - - waitForExpectations(timeout: defaultTimeout, handler: nil) - - // EXPECTED RESULTS: - // syncedId1 -> Not present in remote query, but synched: Unwanted - - // syncedId2 -> Present in local and remote query, synched: Kept 1 - // outdatedId -> Not present in remote query, not synched: Unwanted - - // localId -> Present in local and remote query, not synched: Cleaned 2 - // watchedAndSynchedId -> Not present in remote query, but watched: Unlinked - - // syncedAndWatchedId -> Present in local and remote query, watched: Kept 3 - // newRemoteChannel -> Present in remote query only: Added 4 - - let requests = apiClient.recoveryRequest_allRecordedCalls - XCTAssertEqual(requests.count, 1) - XCTAssertFalse(receivedResult.isError) - - // Two channels were marked as unwanted - XCTAssertEqual(receivedResult.value?.unwanted.count, 2) - XCTAssertTrue(receivedResult.value?.unwanted.contains { $0 == outdatedId } == true) - XCTAssertTrue(receivedResult.value?.unwanted.contains { $0 == syncedId1 } == true) - - // Four channels were synched and watched, and are now part of the query - XCTAssertEqual(receivedResult.value?.synchedAndWatched.count, 4) - let queryChannels = channels(for: query, database: database) - XCTAssertEqual(queryChannels.count, 4) - [syncedId2, localId, syncedAndWatchedId, newRemoteChannel].forEach { cid in - XCTAssertTrue(queryChannels.contains { $0.cid == cid.rawValue }) - } - - // No channel should have been removed yet here - let allChannels = (try? database.viewContext.fetch(ChannelDTO.allChannelsFetchRequest)) ?? [] - XCTAssertEqual(allChannels.count, 7) - - // Cleaned channel should not have messages - XCTAssertEqual(queryChannels.first { $0.cid == localId.rawValue }?.messages.count, 0) - - // Unlinked channels should not have been cleared - XCTAssertEqual(allChannels.first { $0.cid == syncedId1.rawValue }?.messages.count, 1) - XCTAssertEqual(allChannels.first { $0.cid == watchedAndSynchedId.rawValue }?.messages.count, 1) - } - // MARK: - Fetch func test_fetch_makesCorrectAPICall() { @@ -441,6 +277,39 @@ final class ChannelListUpdater_Tests: XCTestCase { // Assert updater can be deallocated without waiting for the API response. AssertAsync.canBeReleased(&listUpdater) } + + // MARK: - Refresh Loaded Channels + + func test_refreshLoadedChannels_whenEmpty_thenRefreshDoesNotHappen() async throws { + var query = ChannelListQuery(filter: .in(.members, values: [.unique])) + query.pagination = Pagination(pageSize: 10) + let initialLoadedChannelCount = 0 + + let cids = try await listUpdater.refreshLoadedChannels(for: query, channelCount: initialLoadedChannelCount) + XCTAssertEqual(Set(), cids) + } + + func test_refreshLoadedChannels_whenMultiplePagesAreLoaded_thenAllPagesAreReloaded() async throws { + let pageSize = Int.channelsPageSize + var query = ChannelListQuery(filter: .in(.members, values: [.unique])) + query.pagination = Pagination(pageSize: pageSize) + + let initialChannels = (0..=5.7) @available(iOS 16.0, *) final class ChatMessage_Equatable_Tests: XCTestCase { var database: DatabaseContainer! @@ -245,4 +244,3 @@ extension Collection { } } } -#endif