diff --git a/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV15.swift b/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV15.swift index 2a97b47b597..fb9282d2543 100644 --- a/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV15.swift +++ b/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV15.swift @@ -82,6 +82,7 @@ private struct SearchResultContactV15: Decodable, ToAPIModelConvertible { documents: documents.map { $0.toAPIModel() } ) } + } private struct ContactV15: Decodable, ToAPIModelConvertible { diff --git a/wire-ios-data-model/Source/Notifications/SearchUserObserverCenter.swift b/wire-ios-data-model/Source/Notifications/SearchUserObserverCenter.swift index 32e0177c5e7..970aff1e99a 100644 --- a/wire-ios-data-model/Source/Notifications/SearchUserObserverCenter.swift +++ b/wire-ios-data-model/Source/Notifications/SearchUserObserverCenter.swift @@ -37,7 +37,7 @@ extension NSManagedObjectContext { } } -public class SearchUserSnapshot { +public final class SearchUserSnapshot { /// Keys that we want to be notified for static let observableKeys: [String] = [ diff --git a/wire-ios-request-strategy/Sources/Request Strategies/Conversation/Actions/RemoveParticipantActionHandlerTests.swift b/wire-ios-request-strategy/Sources/Request Strategies/Conversation/Actions/RemoveParticipantActionHandlerTests.swift index 15ccdca8b1e..45cdb8120f4 100644 --- a/wire-ios-request-strategy/Sources/Request Strategies/Conversation/Actions/RemoveParticipantActionHandlerTests.swift +++ b/wire-ios-request-strategy/Sources/Request Strategies/Conversation/Actions/RemoveParticipantActionHandlerTests.swift @@ -17,6 +17,7 @@ // import XCTest + @testable import WireRequestStrategy class RemoveParticipantActionHandlerTests: MessagingTestBase { diff --git a/wire-ios-sync-engine/Source/Use cases/SearchUsersUseCase.swift b/wire-ios-sync-engine/Source/Use cases/SearchUsersUseCase.swift index 6bfc9eb9eaf..c6076f033b2 100644 --- a/wire-ios-sync-engine/Source/Use cases/SearchUsersUseCase.swift +++ b/wire-ios-sync-engine/Source/Use cases/SearchUsersUseCase.swift @@ -72,24 +72,14 @@ public final class SearchUsersUseCase: SearchUsersUseCaseProtocol { team: team ) - return try await withCheckedThrowingContinuation { continuation in - // TODO: [WPB-23110] SWIFT TASK CONTINUATION MISUSE: invoke(query:options:messageProtocol:) leaked its continuation without resuming it. This may cause tasks waiting on it to remain suspended forever. - guard !Task.isCancelled else { - continuation.resume(throwing: CancellationError()) - self.activeSearchTask = nil - return + let task = searchDirectory.createSearchTask(with: request) + activeSearchTask = task + defer { + if activeSearchTask === task { + activeSearchTask = nil } - - let task = searchDirectory.perform(request) - task.addResultHandler { result, isCompleted in - if isCompleted { - continuation.resume(returning: result) - self.activeSearchTask = nil - } - } - task.start() - activeSearchTask = task } + return await task.start() } // MARK: - Private methods diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift index ef3654c8d00..c676840b0f4 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift @@ -18,14 +18,13 @@ import Foundation -@objcMembers -public class SearchDirectory: NSObject { +public final class SearchDirectory: NSObject { - let contextProvider: ContextProvider - let transportSession: TransportSessionType + private let contextProvider: ContextProvider + private let transportSession: TransportSessionType private let apiVersion: WireTransport.APIVersion? - var isTornDown = false + private var isTornDown = false private let refreshUsersMissingMetadataAction: RecurringAction private let refreshConversationsMissingMetadataAction: RecurringAction @@ -64,50 +63,27 @@ public class SearchDirectory: NSObject { self.refreshConversationsMissingMetadataAction = refreshConversationsMissingMetadataAction } - /// Perform a search request. - /// - /// Returns a SearchTask which should be retained until the results arrive. - public func perform(_ request: SearchRequest) -> SearchTask { - let task = SearchTask( - task: .search(searchRequest: request), + public func createSearchTask(with request: SearchRequest) -> SearchTask { + SearchTask( + type: .search(searchRequest: request), contextProvider: contextProvider, transportSession: transportSession, searchUsersCache: searchUsersCache, apiVersion: apiVersion ) - - task.addResultHandler { [weak self] result, _ in - self?.observeSearchUsers(result) - } - - return task } /// Lookup a user by user Id and domain (qualifiedID), returns a search user in the directory results. If the user /// doesn't exists /// an empty directory result is returned. - /// - /// Returns a SearchTask which should be retained until the results arrive. - public func lookup(qualifiedID: QualifiedID) -> SearchTask { - let task = SearchTask( - task: .lookup(qualifiedID: qualifiedID), + public func createLookupTask(with qualifiedID: QualifiedID) -> SearchTask { + SearchTask( + type: .lookup(qualifiedID: qualifiedID), contextProvider: contextProvider, transportSession: transportSession, searchUsersCache: searchUsersCache, apiVersion: apiVersion ) - - task.addResultHandler { [weak self] result, _ in - self?.observeSearchUsers(result) - } - - return task - } - - func observeSearchUsers(_ result: SearchResult) { - let searchUserObserverCenter = contextProvider.viewContext.searchUserObserverCenter - result.directory.forEach(searchUserObserverCenter.addSearchUser) - result.services.compactMap { $0 as? ZMSearchUser }.forEach(searchUserObserverCenter.addSearchUser) } public func updateIncompleteMetadataIfNeeded() async { diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchRequest.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchRequest.swift index a07d65a5ab4..97ce862944c 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchRequest.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchRequest.swift @@ -16,10 +16,10 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // -import Foundation import WireDataModel public struct SearchOptions: OptionSet { + public let rawValue: Int /// Users you are connected to via connection request. @@ -66,6 +66,7 @@ public struct SearchOptions: OptionSet { } public extension SearchOptions { + mutating func updateForSelfUserTeamRole(selfUser: UserType) { if selfUser.teamRole == .partner { insert(.excludeNonActiveTeamMembers) @@ -74,6 +75,7 @@ public extension SearchOptions { insert(.excludeNonActivePartners) } } + } public struct SearchRequest { @@ -169,4 +171,5 @@ private extension String { guard let normalized = self.normalizedForSearch() as String? else { return "" } return normalized.trimmingCharacters(in: .whitespaces) } + } diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchResult.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchResult.swift index cead556662c..02d32b479a9 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchResult.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchResult.swift @@ -52,6 +52,16 @@ public struct SearchResult { extension SearchResult { + init() { + self.context = .init(concurrencyType: .privateQueueConcurrencyType) + self.contacts = [] + self.teamMembers = [] + self.directory = [] + self.conversations = [] + self.services = [] + self.searchUsersCache = nil + } + public init?( payload: [AnyHashable: Any], query: SearchRequest.Query, @@ -223,4 +233,16 @@ extension SearchResult { ) } + func union(prependingDirectory result: SearchResult) -> SearchResult { + SearchResult( + context: context, + contacts: contacts, + teamMembers: teamMembers, + directory: result.directory + directory, + conversations: conversations, + services: services, + searchUsersCache: searchUsersCache + ) + } + } diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index d1a2b355e12..e3ed64e94d5 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -16,221 +16,199 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // +import CoreData import Foundation import WireUtilities -public class SearchTask { +public final class SearchTask { - public enum Task { + public enum `Type` { case search(searchRequest: SearchRequest) case lookup(qualifiedID: QualifiedID) } - public typealias ResultHandler = (_ partialResult: SearchResult, _ isCompleted: Bool) -> Void + private enum Status { + case pending + case started(taskGroup: TaskGroup) + } + + private var status = Status.pending + + /// A closure which modifies the passed search result in order to unite the existing and the newly found results. + /// + /// The closure is used because there are four different ways of aggregating search results: + /// - union(withLocalResult:) + /// - union(withServiceResult:) + /// - union(withDirectoryResult:) + /// - union(prependingDirectory:) + typealias SearchResultAggregator = (inout SearchResult) -> Void private let apiVersion: WireTransport.APIVersion? private let transportSession: TransportSessionType private let contextProvider: ContextProvider private let searchUsersCache: SearchUsersCache? - private let task: Task - private var userLookupTaskIdentifier: ZMTaskIdentifier? - private var directoryTaskIdentifier: ZMTaskIdentifier? - private var teamMembershipTaskIdentifier: ZMTaskIdentifier? - private var handleTaskIdentifier: ZMTaskIdentifier? - private var servicesTaskIdentifier: ZMTaskIdentifier? - private var resultHandlers: [ResultHandler] = [] - private var aggregatedResult = SearchResult( - context: .init(concurrencyType: .privateQueueConcurrencyType), - contacts: [], - teamMembers: [], - directory: [], - conversations: [], - services: [], - searchUsersCache: nil - ) - - private let tasksRemainingLock = NSRecursiveLock() - private var _tasksRemaining = 0 - private var tasksRemaining: Int { - get { - tasksRemainingLock.withLock { - _tasksRemaining - } - } - set { - let oldValue = tasksRemainingLock.withLock { - let oldValue = _tasksRemaining - _tasksRemaining = newValue - return oldValue - } - // only trigger handles if decrement to 0 - if oldValue > newValue { - let isCompleted = newValue == 0 - resultHandlers.forEach { $0(aggregatedResult, isCompleted) } - - if isCompleted { - resultHandlers.removeAll() - } - } - } - } - - convenience init( - request: SearchRequest, - contextProvider: ContextProvider, - transportSession: TransportSessionType, - searchUsersCache: SearchUsersCache?, - apiVersion: WireTransport.APIVersion? - ) { - self.init( - task: .search(searchRequest: request), - contextProvider: contextProvider, - transportSession: transportSession, - searchUsersCache: searchUsersCache, - apiVersion: apiVersion - ) - } - - convenience init( - qualifiedID: QualifiedID, - contextProvider: ContextProvider, - transportSession: TransportSessionType, - searchUsersCache: SearchUsersCache?, - apiVersion: WireTransport.APIVersion? - ) { - self.init( - task: .lookup(qualifiedID: qualifiedID), - contextProvider: contextProvider, - transportSession: transportSession, - searchUsersCache: searchUsersCache, - apiVersion: apiVersion - ) - } + private let type: `Type` - public init( - task: Task, + init( + type: Type, contextProvider: ContextProvider, transportSession: TransportSessionType, searchUsersCache: SearchUsersCache?, apiVersion: WireTransport.APIVersion? ) { - self.task = task + self.type = type self.transportSession = transportSession self.contextProvider = contextProvider self.searchUsersCache = searchUsersCache self.apiVersion = apiVersion } - public func addResultHandler(_ resultHandler: @escaping ResultHandler) { - resultHandlers.append(resultHandler) - } - /// Cancel a previously started task public func cancel() { - resultHandlers.removeAll() - - teamMembershipTaskIdentifier.map(transportSession.cancelTask) - userLookupTaskIdentifier.map(transportSession.cancelTask) - directoryTaskIdentifier.map(transportSession.cancelTask) - servicesTaskIdentifier.map(transportSession.cancelTask) - handleTaskIdentifier.map(transportSession.cancelTask) + guard case let .started(taskGroup) = status else { + assertionFailure() + return + } - tasksRemaining = 0 + taskGroup.cancelAll() } - /// Start the search task. Results will be sent to the result handlers - /// added via the `onResult()` method. - public func start() { - // search services - performRemoteSearchForServices() + /// Start the search task. Errors will not be thrown. + public func start() async -> SearchResult { + guard case .pending = status else { + assertionFailure() + return SearchResult() + } + + return await withTaskGroup( + of: SearchResultAggregator.self, + returning: SearchResult.self + ) { @MainActor taskGroup in - // search People or groups - performLocalLookup() - performLocalSearch() + status = .started(taskGroup: taskGroup) - // v1 - performRemoteSearchForTeamUser() + // search services + taskGroup.addTask { + await self.performRemoteSearchForServices() + } - // v2+ - performRemoteSearch() - performUserLookup() + // search People or groups + taskGroup.addTask { + await self.performLocalLookup() + } + taskGroup.addTask { + await self.performLocalSearch() + } + + // v1 + taskGroup.addTask { + await self.performRemoteSearchForTeamUser() + } + + // v2+ + taskGroup.addTask { + await self.performRemoteSearch() + } + taskGroup.addTask { + await self.performUserLookup() + } + + var result = SearchResult() + while let aggregator = await taskGroup.next() { + aggregator(&result) + } + + if Task.isCancelled { // TODO: [WPB-20362] make this code throwing + return SearchResult() + } + + // add to search users cache + let searchUserObserverCenter = self.contextProvider.viewContext.searchUserObserverCenter + result.directory.forEach(searchUserObserverCenter.addSearchUser) + result.services.compactMap { $0 as? ZMSearchUser }.forEach(searchUserObserverCenter.addSearchUser) + + return result + } } } extension SearchTask { - /// look up a user ID from contacts and teamMembers locally. - private func performLocalLookup() { - guard case let .lookup(qualifiedID) = task else { return } + /// Look up a user ID from contacts and teamMembers locally. + private func performLocalLookup() async -> SearchResultAggregator { - tasksRemaining += 1 + guard case let .lookup(qualifiedID) = type else { + return { _ in } + } let searchContext = contextProvider.newBackgroundContext() - let viewContext = contextProvider.viewContext - searchContext.perform { [self] in + let (teamMemberIDs, connectedUserIDs) = await searchContext.perform { + let selfUser = ZMUser.selfUser(in: searchContext) var options = SearchOptions() - options.updateForSelfUserTeamRole(selfUser: selfUser) /// search for the local user with matching user ID and active - let activeMembers = teamMembers( + let activeMembers = self.teamMembers( matchingQuery: "", team: selfUser.team, searchOptions: options, in: searchContext ) - let teamMembers = activeMembers.filter { $0.remoteIdentifier == qualifiedID.uuid } - let connectedUsers = connectedUsers(matchingQuery: "", hostedOnDomain: nil, in: searchContext) + let teamMembers = activeMembers .filter { $0.remoteIdentifier == qualifiedID.uuid } + .compactMap(\.user) + let connectedUsers = self.connectedUsers(matchingQuery: "", hostedOnDomain: nil, in: searchContext) + .filter { $0.remoteIdentifier == qualifiedID.uuid } + return (teamMembers.map(\.objectID), connectedUsers.map(\.objectID)) - viewContext.performGroupedBlock { [self] in - - let copiedTeamMembers = teamMembers.compactMap(\.user) - .compactMap { viewContext.object(with: $0.objectID) as? Member } - let copiedConnectedUsers = connectedUsers - .compactMap { viewContext.object(with: $0.objectID) as? ZMUser } + } - let partialResult = SearchResult( - context: viewContext, - contacts: copiedConnectedUsers.map { - ZMSearchUser( - contextProvider: contextProvider, - user: $0, - searchUsersCache: searchUsersCache - ) - }, - teamMembers: copiedTeamMembers.compactMap(\.user).map { - ZMSearchUser( - contextProvider: contextProvider, - user: $0, - searchUsersCache: searchUsersCache - ) - }, - directory: [], - conversations: [], - services: [], - searchUsersCache: searchUsersCache - ) + let viewContext = contextProvider.viewContext + return await viewContext.perform { () -> SearchResultAggregator in + + let copiedTeamMembers = teamMemberIDs + .compactMap { viewContext.object(with: $0) as? Member } + let copiedConnectedUsers = connectedUserIDs + .compactMap { viewContext.object(with: $0) as? ZMUser } + + let result = SearchResult( + context: viewContext, + contacts: copiedConnectedUsers.map { + ZMSearchUser( + contextProvider: self.contextProvider, + user: $0, + searchUsersCache: self.searchUsersCache + ) + }, + teamMembers: copiedTeamMembers.compactMap(\.user).map { + ZMSearchUser( + contextProvider: self.contextProvider, + user: $0, + searchUsersCache: self.searchUsersCache + ) + }, + directory: [], + conversations: [], + services: [], + searchUsersCache: self.searchUsersCache + ) - aggregatedResult = aggregatedResult - .union(withLocalResult: partialResult.copy(on: viewContext)) + return { $0 = $0.union(withLocalResult: result.copy(on: viewContext)) } - tasksRemaining -= 1 - } } - } - func performLocalSearch() { - guard case let .search(request) = task else { return } + } - tasksRemaining += 1 + func performLocalSearch() async -> SearchResultAggregator { + guard case let .search(request) = type else { + return { _ in } + } let searchContext = contextProvider.newBackgroundContext() - let viewContext = contextProvider.viewContext - searchContext.perform { [self] in + let (connectedUserIDs, teamMemberIDs, conversationIDs) = await searchContext.perform { [self] in var team: Team? if let teamObjectID = request.team?.objectID { @@ -257,48 +235,54 @@ extension SearchTask { in: searchContext ) : [] - viewContext.performGroupedBlock { [self] in + return ( + connectedUsers.map(\.objectID), + teamMembers.map(\.objectID), + conversations.map(\.objectID) + ) - let copiedConnectedUsers = connectedUsers - .compactMap { viewContext.object(with: $0.objectID) as? ZMUser } - let searchConnectedUsers = copiedConnectedUsers - .map { - ZMSearchUser( - contextProvider: contextProvider, - user: $0, - searchUsersCache: searchUsersCache - ) - } - .filter { !$0.hasEmptyName } + } - let copiedteamMembers = teamMembers.compactMap { - viewContext.object(with: $0.objectID) as? Member + let viewContext = contextProvider.viewContext + return await viewContext.perform { [self] in + + let copiedConnectedUsers = connectedUserIDs + .compactMap { viewContext.object(with: $0) as? ZMUser } + let searchConnectedUsers = copiedConnectedUsers + .map { + ZMSearchUser( + contextProvider: contextProvider, + user: $0, + searchUsersCache: searchUsersCache + ) } - let searchTeamMembers = copiedteamMembers - .compactMap(\.user) - .map { - ZMSearchUser( - contextProvider: contextProvider, - user: $0, - searchUsersCache: searchUsersCache - ) - } + .filter { $0.name?.isEmpty == false } - let partialResult = SearchResult( - context: viewContext, - contacts: searchConnectedUsers, - teamMembers: searchTeamMembers, - directory: [], - conversations: conversations, - services: [], - searchUsersCache: searchUsersCache - ) + let copiedteamMembers = teamMemberIDs.compactMap { + contextProvider.viewContext.object(with: $0) as? Member + } + let searchTeamMembers = copiedteamMembers + .compactMap(\.user) + .map { + ZMSearchUser( + contextProvider: contextProvider, + user: $0, + searchUsersCache: searchUsersCache + ) + } - aggregatedResult = aggregatedResult - .union(withLocalResult: partialResult.copy(on: viewContext)) + let result = SearchResult( + context: contextProvider.viewContext, + contacts: searchConnectedUsers, + teamMembers: searchTeamMembers, + directory: [], + conversations: conversationIDs.compactMap { viewContext.object(with: $0) as? ZMConversation }, + services: [], + searchUsersCache: searchUsersCache + ) + + return { $0 = $0.union(withLocalResult: result.copy(on: viewContext)) } - tasksRemaining -= 1 - } } } @@ -316,7 +300,7 @@ extension SearchTask { } } - func teamMembers( + private func teamMembers( matchingQuery query: String, team: Team?, searchOptions: SearchOptions, @@ -350,7 +334,7 @@ extension SearchTask { return partialResult } - func connectedUsers( + private func connectedUsers( matchingQuery query: String, hostedOnDomain: String?, in context: NSManagedObjectContext @@ -367,7 +351,7 @@ extension SearchTask { return context.fetchOrAssert(request: fetchRequest) as? [ZMUser] ?? [] } - func conversations( + private func conversations( matchingQuery query: SearchRequest.Query, selfUser: ZMUser, in context: NSManagedObjectContext @@ -407,21 +391,16 @@ extension SearchTask { extension SearchTask { - func performUserLookup() { + func performUserLookup() async -> SearchResultAggregator { guard - case let .lookup(qualifiedID) = task, + case let .lookup(qualifiedID) = type, let apiVersion - else { return } + else { return { _ in } } - tasksRemaining += 1 + return await withCheckedContinuation { continuation in - let searchContext = contextProvider.newBackgroundContext() - searchContext.perform { [self] in - let request = type(of: self).searchRequestForUser(qualifiedID: qualifiedID, apiVersion: apiVersion) + let request = Self.searchRequestForUser(qualifiedID: qualifiedID, apiVersion: apiVersion) request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in - defer { - self?.tasksRemaining -= 1 - } guard let self, @@ -431,14 +410,9 @@ extension SearchTask { contextProvider: contextProvider, searchUsersCache: searchUsersCache ) - else { return } + else { return continuation.resume(returning: { _ in }) } - let updatedResult = aggregatedResult.union(withDirectoryResult: partialResult) - aggregatedResult = updatedResult - }) - - request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in - self?.userLookupTaskIdentifier = taskIdentifier + continuation.resume(returning: { $0 = $0.union(withDirectoryResult: partialResult) }) }) transportSession.enqueueOneTime(request) @@ -449,7 +423,7 @@ extension SearchTask { // GET /users/:id has been removed in v1. // We should use the qualified endpoint GET /users/:domain/:id instead. // https://wearezeta.atlassian.net/wiki/spaces/ENGINEERIN/pages/603095166/API+changes+v1+v2 - static func searchRequestForUser(qualifiedID: QualifiedID, apiVersion: APIVersion) -> ZMTransportRequest { + private static func searchRequestForUser(qualifiedID: QualifiedID, apiVersion: APIVersion) -> ZMTransportRequest { (apiVersion <= .v1) ? .init(getFromPath: "/users/\(qualifiedID.uuid.transportString())", apiVersion: apiVersion.rawValue) : .init( @@ -462,21 +436,20 @@ extension SearchTask { extension SearchTask { - func performRemoteSearch() { + func performRemoteSearch() async -> SearchResultAggregator { guard let apiVersion, apiVersion >= .v1, - case let .search(searchRequest) = task, + case let .search(searchRequest) = type, + !searchRequest.query.string.isEmpty, // backend won't return anything for empty queries !searchRequest.searchOptions.contains(.localResultsOnly), !searchRequest.searchOptions.isDisjoint(with: [.directory, .teamMembers, .federated]) else { - return + return { _ in } } - tasksRemaining += 1 + return await withCheckedContinuation { continuation in - let searchContext = contextProvider.newBackgroundContext() - searchContext.perform { [self] in let request = Self.searchRequestInDirectory(withRequest: searchRequest, apiVersion: apiVersion) request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in @@ -492,85 +465,81 @@ extension SearchTask { searchUsersCache: searchUsersCache ) else { - completeRemoteSearch() - return + return continuation.resume(returning: { _ in }) } if searchRequest.searchOptions.contains(.teamMembers) { - performTeamMembershipLookup(on: partialResult, searchRequest: searchRequest) + Task { + let aggregator = await self.performTeamMembershipLookup( + on: partialResult, + searchRequest: searchRequest + ) + continuation.resume(returning: aggregator) + } } else { - completeRemoteSearch(searchResult: partialResult) + continuation.resume(returning: { $0 = $0.union(withDirectoryResult: partialResult) }) } }) - request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in - self?.directoryTaskIdentifier = taskIdentifier - }) - transportSession.enqueueOneTime(request) } } - func performTeamMembershipLookup(on searchResult: SearchResult, searchRequest: SearchRequest) { - let teamMembersIDs = searchResult.teamMembers.compactMap(\.remoteIdentifier) + private func performTeamMembershipLookup( + on searchResult: SearchResult, + searchRequest: SearchRequest + ) async -> SearchResultAggregator { + + let viewContext = contextProvider.viewContext + let (teamMembersIDs, teamID) = await contextProvider.viewContext.perform { [viewContext] in + let teamMembersIDs = searchResult.teamMembers.compactMap(\.remoteIdentifier) + let teamID = ZMUser.selfUser(in: viewContext).team?.remoteIdentifier + return (teamMembersIDs, teamID) + } guard let apiVersion, - let teamID = ZMUser.selfUser(in: contextProvider.viewContext).team?.remoteIdentifier, + let teamID, !teamMembersIDs.isEmpty else { - completeRemoteSearch(searchResult: searchResult) - return + return { $0 = $0.union(withDirectoryResult: searchResult) } } - let request = type(of: self).fetchTeamMembershipRequest( + let request = Self.fetchTeamMembershipRequest( teamID: teamID, teamMemberIDs: teamMembersIDs, apiVersion: apiVersion ) - request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in - guard let self else { return } + return await withCheckedContinuation { continuation in - guard - let rawData = response.rawData, - let payload = MembershipListPayload(rawData) - else { - completeRemoteSearch() - return - } + request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in - var updatedResult = searchResult - updatedResult.extendWithMembershipPayload(payload: payload) - updatedResult.filterBy( - searchOptions: searchRequest.searchOptions, - query: searchRequest.query.string, - contextProvider: contextProvider - ) + guard + let self, + let rawData = response.rawData, + let payload = MembershipListPayload(rawData) + else { return continuation.resume(returning: { _ in }) } - completeRemoteSearch(searchResult: updatedResult) + var updatedResult = searchResult + updatedResult.extendWithMembershipPayload(payload: payload) + updatedResult.filterBy( + searchOptions: searchRequest.searchOptions, + query: searchRequest.query.string, + contextProvider: contextProvider + ) - }) + continuation.resume(returning: { $0 = $0.union(withDirectoryResult: updatedResult) }) - let searchContext = contextProvider.newBackgroundContext() - request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in - self?.teamMembershipTaskIdentifier = taskIdentifier - }) + }) - transportSession.enqueueOneTime(request) - } + transportSession.enqueueOneTime(request) - func completeRemoteSearch(searchResult: SearchResult? = nil) { - defer { - tasksRemaining -= 1 } - if let searchResult { - aggregatedResult = aggregatedResult.union(withDirectoryResult: searchResult) - } } - static func searchRequestInDirectory( + private static func searchRequestInDirectory( withRequest searchRequest: SearchRequest, fetchLimit: Int = 10, apiVersion: APIVersion @@ -592,7 +561,7 @@ extension SearchTask { return ZMTransportRequest(getFromPath: path, apiVersion: apiVersion.rawValue) } - static func fetchTeamMembershipRequest( + private static func fetchTeamMembershipRequest( teamID: UUID, teamMemberIDs: [UUID], apiVersion: APIVersion @@ -617,36 +586,31 @@ extension SearchTask { extension SearchTask { - func performRemoteSearchForTeamUser() { + func performRemoteSearchForTeamUser() async -> SearchResultAggregator { guard let apiVersion, apiVersion <= .v1, - case let .search(searchRequest) = task, + case let .search(searchRequest) = type, !searchRequest.searchOptions.contains(.localResultsOnly), searchRequest.searchOptions.contains(.directory) - else { return } + else { return { _ in } } - tasksRemaining += 1 + let viewContext = contextProvider.viewContext + return await withCheckedContinuation { continuation in - let searchContext = contextProvider.newBackgroundContext() - searchContext.perform { [self] in - let request = type(of: self).searchRequestInDirectory( + let request = Self.searchRequestInDirectory( withHandle: searchRequest.query.string, apiVersion: apiVersion ) - request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in - - defer { - self?.tasksRemaining -= 1 - } + request.add(ZMCompletionHandler(on: viewContext) { [weak self] response in guard let self, let payload = response.payload?.asArray(), let userPayload = (payload.first as? ZMTransportData)?.asDictionary() else { - return + return continuation.resume(returning: { _ in }) } guard @@ -654,7 +618,7 @@ extension SearchTask { let name = userPayload["name"] as? String, let id = userPayload["id"] as? String else { - return + return continuation.resume(returning: { _ in }) } let document = ["handle": handle, "name": name, "id": id] @@ -666,35 +630,37 @@ extension SearchTask { contextProvider: contextProvider, searchUsersCache: searchUsersCache ) else { - return + return continuation.resume(returning: { _ in }) } if let user = partialResult.directory.first, !user.isSelfUser { - let prevResult = aggregatedResult - // prepend result to prevResult only if it doesn't contain it - if !prevResult.directory.contains(user) { - aggregatedResult = SearchResult( - context: prevResult.context, - contacts: prevResult.contacts, - teamMembers: prevResult.teamMembers, - directory: partialResult.directory + prevResult.directory, - conversations: prevResult.conversations, - services: prevResult.services, - searchUsersCache: searchUsersCache - ) - } + let partialResult = SearchResult( + context: viewContext, + contacts: [], + teamMembers: [], + directory: partialResult.directory, + conversations: [], + services: [], + searchUsersCache: searchUsersCache + ) + continuation.resume(returning: { aggregatedResult in + if !aggregatedResult.directory.contains(user) { + aggregatedResult = aggregatedResult.union(prependingDirectory: partialResult) + } + }) + } else { + continuation.resume(returning: { _ in }) } }) - request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in - self?.handleTaskIdentifier = taskIdentifier - }) - transportSession.enqueueOneTime(request) } } - static func searchRequestInDirectory(withHandle handle: String, apiVersion: APIVersion) -> ZMTransportRequest { + private static func searchRequestInDirectory( + withHandle handle: String, + apiVersion: APIVersion + ) -> ZMTransportRequest { var handle = handle.lowercased() if handle.hasPrefix("@") { @@ -711,24 +677,24 @@ extension SearchTask { extension SearchTask { - func performRemoteSearchForServices() { + func performRemoteSearchForServices() async -> SearchResultAggregator { + let searchContext = contextProvider.newBackgroundContext() - let teamIdentifier = searchContext.performAndWait { + let teamIdentifier = await searchContext.perform { ZMUser.selfUser(in: searchContext).team?.remoteIdentifier } + guard let apiVersion, let teamIdentifier, - case let .search(searchRequest) = task, + case let .search(searchRequest) = type, !searchRequest.searchOptions.contains(.localResultsOnly), searchRequest.searchOptions.contains(.services) - else { return } + else { return { _ in } } - tasksRemaining += 1 + return await withCheckedContinuation { continuation in - searchContext.perform { [self] in - - let request = type(of: self).servicesSearchRequest( + let request = Self.servicesSearchRequest( teamIdentifier: teamIdentifier, query: searchRequest.query.string, apiVersion: apiVersion @@ -736,10 +702,6 @@ extension SearchTask { request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in - defer { - self?.tasksRemaining -= 1 - } - guard let self, let payload = response.payload?.asDictionary(), @@ -749,19 +711,14 @@ extension SearchTask { contextProvider: contextProvider, searchUsersCache: searchUsersCache ) - else { - return - } + else { return continuation.resume(returning: { _ in }) } - let updatedResult = aggregatedResult.union(withServiceResult: partialResult) - aggregatedResult = updatedResult - }) + continuation.resume { $0 = $0.union(withServiceResult: partialResult) } - request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in - self?.servicesTaskIdentifier = taskIdentifier }) transportSession.enqueueOneTime(request) + } } @@ -781,14 +738,3 @@ extension SearchTask { return ZMTransportRequest(getFromPath: urlStr, apiVersion: apiVersion.rawValue) } } - -public extension ZMSearchUser { - - var hasEmptyName: Bool { - guard let name else { - return true - } - return name.isEmpty - } - -} diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift index eb410209ff3..7a06dd3b19b 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift @@ -17,8 +17,8 @@ // import Foundation - import WireMockTransport + @testable import WireSyncEngine @testable import WireSyncEngineSupport diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift index 7b753d9c401..91363afe1a1 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift @@ -57,39 +57,43 @@ final class SearchTaskTests: DatabaseTest { super.tearDown() } - func createConnectedUser(withName name: String, domain: String? = nil) -> ZMUser { - let user = ZMUser.insertNewObject(in: uiMOC) - user.name = name - user.remoteIdentifier = UUID.create() - user.domain = domain + private func createConnectedUser(withName name: String, domain: String? = nil) async throws -> ZMUser { + try await uiMOC.perform { [uiMOC] in + let user = ZMUser.insertNewObject(in: uiMOC) + user.name = name + user.remoteIdentifier = UUID.create() + user.domain = domain - let connection = ZMConnection.insertNewObject(in: uiMOC) - connection.to = user - connection.status = .accepted + let connection = ZMConnection.insertNewObject(in: uiMOC) + connection.to = user + connection.status = .accepted - uiMOC.saveOrRollback() + try uiMOC.save() - return user + return user + } } - func createGroupConversation(withName name: String) -> ZMConversation { - let conversation = ZMConversation.insertNewObject(in: uiMOC) - let selfUser = ZMUser.selfUser(in: uiMOC) - selfUser.name = "Me" - conversation.userDefinedName = name - conversation.conversationType = .group - conversation.addParticipantAndUpdateConversationState(user: selfUser, role: nil) + private func createGroupConversation(withName name: String) async throws -> ZMConversation { + try await uiMOC.perform { [uiMOC] in + let conversation = ZMConversation.insertNewObject(in: uiMOC) + let selfUser = ZMUser.selfUser(in: uiMOC) + selfUser.name = "Me" + conversation.userDefinedName = name + conversation.conversationType = .group + conversation.addParticipantAndUpdateConversationState(user: selfUser, role: nil) - uiMOC.saveOrRollback() + try uiMOC.save() - return conversation + return conversation + } } - func testThatItFindsASingleUnconnectedUserByHandle() { + // MARK: - - // given - let remoteResultArrived = customExpectation(description: "received remote result") + func testThatItFindsASingleUnconnectedUserByHandle() async throws { + // given mockTransportSession.performRemoteChanges { remoteChanges in let mockUser = remoteChanges.insertUser(withName: "Dale Cooper") mockUser.handle = "bob" @@ -98,22 +102,20 @@ final class SearchTaskTests: DatabaseTest { let request = SearchRequest(query: "bob", searchOptions: [.directory]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - remoteResultArrived.fulfill() - XCTAssertEqual(result.directory.count, 1) - let user = result.directory.first - XCTAssertEqual(user?.name, "Dale Cooper") - XCTAssertEqual(user?.handle, "bob") - } - // when - task.performRemoteSearchForTeamUser() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performRemoteSearchForTeamUser() + resultAggregator(&result) + + // then + XCTAssertEqual(result.directory.count, 1) + let user = result.directory.first + XCTAssertEqual(user?.name, "Dale Cooper") + XCTAssertEqual(user?.handle, "bob") } - func testThatItReturnsNothingWhenSearchingForSelfUserByHandle() { + func testThatItReturnsNothingWhenSearchingForSelfUserByHandle() async throws { // given var selfUserID: UUID! @@ -126,766 +128,781 @@ final class SearchTaskTests: DatabaseTest { } // update self user locally - syncMOC.performGroupedAndWait { - ZMUser.selfUser(in: self.syncMOC).remoteIdentifier = selfUserID - self.syncMOC.saveOrRollback() + try await syncMOC.perform { [syncMOC] in + ZMUser.selfUser(in: syncMOC).remoteIdentifier = selfUserID + try syncMOC.save() } - let remoteResultArrived = customExpectation(description: "received remote result") let request = SearchRequest(query: "einstein", searchOptions: [.directory]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - remoteResultArrived.fulfill() - XCTAssertEqual(result.directory.count, 0) - } - // when - task.performRemoteSearchForTeamUser() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performRemoteSearchForTeamUser() + resultAggregator(&result) + + // then + XCTAssertEqual(result.directory.count, 0) } // MARK: Contacts Search - func testThatItFindsASingleUser() { + func testThatItFindsASingleUser() async throws { // given - let resultArrived = customExpectation(description: "received result") - let user = createConnectedUser(withName: "userA") + let user = try await createConnectedUser(withName: "userA") let request = SearchRequest(query: "userA", searchOptions: [.contacts]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertTrue(result.contacts.compactMap(\.user).contains(user)) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertTrue(result.contacts.compactMap(\.user).contains(user)) } - func testThatItDoesFindUsersContainingButNotBeginningWithSearchString() { + func testThatItDoesFindUsersContainingButNotBeginningWithSearchString() async throws { // given - let resultArrived = customExpectation(description: "received result") - _ = createConnectedUser(withName: "userA") + _ = try await createConnectedUser(withName: "userA") let request = SearchRequest(query: "serA", searchOptions: [.contacts]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.contacts.count, 1) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(result.contacts.count, 1) } - func testThatItFindsUsersBeginningWithSearchString() { + func testThatItFindsUsersBeginningWithSearchString() async throws { // given - let resultArrived = customExpectation(description: "received result") - let user = createConnectedUser(withName: "userA") + let user = try await createConnectedUser(withName: "userA") let request = SearchRequest(query: "user", searchOptions: [.contacts]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertTrue(result.contacts.compactMap(\.user).contains(user)) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertTrue(result.contacts.compactMap(\.user).contains(user)) } - func testThatItUsesAllQueryComponentsToFindAUser() { + func testThatItUsesAllQueryComponentsToFindAUser() async throws { // given - let resultArrived = customExpectation(description: "received result") - let user1 = createConnectedUser(withName: "Some Body") - _ = createConnectedUser(withName: "Some") - _ = createConnectedUser(withName: "Any Body") + let user1 = try await createConnectedUser(withName: "Some Body") + _ = try await createConnectedUser(withName: "Some") + _ = try await createConnectedUser(withName: "Any Body") let request = SearchRequest(query: "Some Body", searchOptions: [.contacts]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.contacts.compactMap(\.user), [user1]) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(result.contacts.compactMap(\.user), [user1]) } - func testThatItFindsSeveralUsers() { + func testThatItFindsSeveralUsers() async throws { // given - let resultArrived = customExpectation(description: "received result") - let user1 = createConnectedUser(withName: "Grant") - let user2 = createConnectedUser(withName: "Greg") - _ = createConnectedUser(withName: "Bob") + let user1 = try await createConnectedUser(withName: "Grant") + let user2 = try await createConnectedUser(withName: "Greg") + _ = try await createConnectedUser(withName: "Bob") let request = SearchRequest(query: "Gr", searchOptions: [.contacts]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.contacts.compactMap(\.user), [user1, user2]) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(result.contacts.compactMap(\.user), [user1, user2]) } - func testThatUserSearchIsCaseInsensitive() { + func testThatUserSearchIsCaseInsensitive() async throws { // given - let resultArrived = customExpectation(description: "received result") - let user1 = createConnectedUser(withName: "Somebody") + let user1 = try await createConnectedUser(withName: "Somebody") let request = SearchRequest(query: "someBodY", searchOptions: [.contacts]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.contacts.compactMap(\.user), [user1]) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(result.contacts.compactMap(\.user), [user1]) } - func testThatUserSearchIsInsensitiveToDiacritics() { + func testThatUserSearchIsInsensitiveToDiacritics() async throws { // given - let resultArrived = customExpectation(description: "received result") - let user1 = createConnectedUser(withName: "Sömëbodÿ") + let user1 = try await createConnectedUser(withName: "Sömëbodÿ") let request = SearchRequest(query: "Sømebôdy", searchOptions: [.contacts]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.contacts.compactMap(\.user), [user1]) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(result.contacts.compactMap(\.user), [user1]) } - func testThatUserSearchOnlyReturnsConnectedUsers() { + func testThatUserSearchOnlyReturnsConnectedUsers() async throws { // given - let resultArrived = customExpectation(description: "received result") - let user1 = createConnectedUser(withName: "Somebody Blocked") - user1.connection?.status = .blocked - let user2 = createConnectedUser(withName: "Somebody Pending") - user2.connection?.status = .pending - let user3 = createConnectedUser(withName: "Somebody") + let user1 = try await createConnectedUser(withName: "Somebody Blocked") + await uiMOC.perform { + user1.connection?.status = .blocked + } + let user2 = try await createConnectedUser(withName: "Somebody Pending") + await uiMOC.perform { + user2.connection?.status = .pending + } + let user3 = try await createConnectedUser(withName: "Somebody") let request = SearchRequest(query: "Some", searchOptions: [.contacts]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.contacts.compactMap(\.user), [user3]) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(result.contacts.compactMap(\.user), [user3]) } - func testThatItDoesNotReturnTheSelfUser() { + func testThatItDoesNotReturnTheSelfUser() async throws { // given - let resultArrived = customExpectation(description: "received result") - let selfUser = ZMUser.selfUser(in: uiMOC) - selfUser.name = "Some self user" - let user = createConnectedUser(withName: "Somebody") + await uiMOC.perform { [self] in + let selfUser = ZMUser.selfUser(in: uiMOC) + selfUser.name = "Some self user" + } + let user = try await createConnectedUser(withName: "Somebody") let request = SearchRequest(query: "Some", searchOptions: [.contacts]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.contacts.compactMap(\.user), [user]) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(result.contacts.compactMap(\.user), [user]) } - func testThatItDoesNotFindUsersWithOtherDomainsIfSearchDomainIsRequired() { + func testThatItDoesNotFindUsersWithOtherDomainsIfSearchDomainIsRequired() async throws { // given - let resultArrived = customExpectation(description: "received result") - let user = createConnectedUser(withName: "userA", domain: "bella.com") + _ = try await createConnectedUser(withName: "userA", domain: "bella.com") let request = SearchRequest(query: "userA@bella.com", searchDomain: "anta.com", searchOptions: [.contacts]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.contacts.count, 0) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(result.contacts.count, 0) } - func testThatItFindsUsersWithSameDomainAsSelfUser() { + func testThatItFindsUsersWithSameDomainAsSelfUser() async throws { // given - let resultArrived = customExpectation(description: "received result") - let user = createConnectedUser(withName: "userA", domain: "anta.com") + let user = try await createConnectedUser(withName: "userA", domain: "anta.com") let request = SearchRequest(query: "userA@anta.com", searchOptions: [.contacts]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertTrue(result.contacts.compactMap(\.user).contains(user)) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertTrue(result.contacts.compactMap(\.user).contains(user)) } - func testThatItFindsUsersWithOtherDomainsIfSearchDomainIsNotRequired() { + func testThatItFindsUsersWithOtherDomainsIfSearchDomainIsNotRequired() async throws { // given - let resultArrived = customExpectation(description: "received result") - let user = createConnectedUser(withName: "userA", domain: "bella.com") + let user = try await createConnectedUser(withName: "userA", domain: "bella.com") let request = SearchRequest(query: "userA@bella.com", searchOptions: [.contacts]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertTrue(result.contacts.compactMap(\.user).contains(user)) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertTrue(result.contacts.compactMap(\.user).contains(user)) } // MARK: Team member local search - func testThatItCanSearchForTeamMembersLocally() { - // given - let resultArrived = customExpectation(description: "received result") - let team = Team.insertNewObject(in: uiMOC) - let user = ZMUser.insertNewObject(in: uiMOC) - let member = Member.insertNewObject(in: uiMOC) - - user.name = "Member A" + func testThatItCanSearchForTeamMembersLocally() async throws { + let (user, task) = await uiMOC.perform { [self] in + // given + let team = Team.insertNewObject(in: uiMOC) + let user = ZMUser.insertNewObject(in: uiMOC) + let member = Member.insertNewObject(in: uiMOC) - member.team = team - member.user = user + user.name = "Member A" - uiMOC.saveOrRollback() + member.team = team + member.user = user - let request = SearchRequest(query: "@member", searchOptions: [.teamMembers], team: team) - let task = makeSearchTask(request: request) + uiMOC.saveOrRollback() - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.teamMembers.compactMap(\.user), [user]) + let request = SearchRequest(query: "@member", searchOptions: [.teamMembers], team: team) + let task = makeSearchTask(request: request) + return (user, task) } // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) - } - - func testThatItCanExcludeNonActiveTeamMembersLocally() { - // given - let resultArrived = customExpectation(description: "received result") - let team = Team.insertNewObject(in: uiMOC) - let userA = ZMUser.insertNewObject(in: uiMOC) - let userB = ZMUser.insertNewObject(in: uiMOC) - let memberA = Member.insertNewObject(in: uiMOC) - let memberB = Member.insertNewObject(in: uiMOC) // non-active team-member - let conversation = ZMConversation.insertNewObject(in: uiMOC) - - conversation.conversationType = .group - conversation.remoteIdentifier = UUID() - conversation.addParticipantsAndUpdateConversationState( - users: Set([userA, ZMUser.selfUser(in: uiMOC)]), - role: nil - ) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) + } - userA.name = "Member A" - userB.name = "Member B" + // then + XCTAssertEqual(result.teamMembers.compactMap(\.user), [user]) + } + + func testThatItCanExcludeNonActiveTeamMembersLocally() async throws { + let (userA, task) = await uiMOC.perform { [self] in + // given + let team = Team.insertNewObject(in: uiMOC) + let userA = ZMUser.insertNewObject(in: uiMOC) + let userB = ZMUser.insertNewObject(in: uiMOC) + let memberA = Member.insertNewObject(in: uiMOC) + let memberB = Member.insertNewObject(in: uiMOC) // non-active team-member + let conversation = ZMConversation.insertNewObject(in: uiMOC) + + conversation.conversationType = .group + conversation.remoteIdentifier = UUID() + conversation.addParticipantsAndUpdateConversationState( + users: Set([userA, ZMUser.selfUser(in: uiMOC)]), + role: nil + ) - memberA.team = team - memberA.user = userA + userA.name = "Member A" + userB.name = "Member B" - memberB.team = team - memberB.user = userB + memberA.team = team + memberA.user = userA - uiMOC.saveOrRollback() + memberB.team = team + memberB.user = userB - let request = SearchRequest(query: "", searchOptions: [.teamMembers, .excludeNonActiveTeamMembers], team: team) - let task = makeSearchTask(request: request) + uiMOC.saveOrRollback() - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA]) + let request = SearchRequest( + query: "", + searchOptions: [.teamMembers, .excludeNonActiveTeamMembers], + team: team + ) + let task = makeSearchTask(request: request) + return (userA, task) } // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) - } + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) + } - func testThatItIncludesNonActiveTeamMembersLocally_WhenSelfUserWasCreatedByThem() { - // given - let resultArrived = customExpectation(description: "received result") - let team = Team.insertNewObject(in: uiMOC) - let userA = ZMUser.insertNewObject(in: uiMOC) - let memberA = Member.insertNewObject(in: uiMOC) // non-active team-member - let selfUser = ZMUser.selfUser(in: uiMOC) + // then + XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA]) + } - userA.name = "Member A" - userA.handle = "abc" + func testThatItIncludesNonActiveTeamMembersLocally_WhenSelfUserWasCreatedByThem() async throws { + let (userA, task) = await uiMOC.perform { [self] in + // given + let team = Team.insertNewObject(in: uiMOC) + let userA = ZMUser.insertNewObject(in: uiMOC) + let memberA = Member.insertNewObject(in: uiMOC) // non-active team-member + let selfUser = ZMUser.selfUser(in: uiMOC) - selfUser.membership?.permissions = .partner - selfUser.membership?.createdBy = userA + userA.name = "Member A" + userA.handle = "abc" - memberA.team = team - memberA.user = userA - memberA.permissions = .admin + selfUser.membership?.permissions = .partner + selfUser.membership?.createdBy = userA - uiMOC.saveOrRollback() + memberA.team = team + memberA.user = userA + memberA.permissions = .admin - let request = SearchRequest(query: "", searchOptions: [.teamMembers, .excludeNonActiveTeamMembers], team: team) - let task = makeSearchTask(request: request) + uiMOC.saveOrRollback() - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA]) + let request = SearchRequest( + query: "", + searchOptions: [.teamMembers, .excludeNonActiveTeamMembers], + team: team + ) + let task = makeSearchTask(request: request) + return (userA, task) } // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) - } - - func testThatItCanExcludeNonActivePartnersLocally() { - // given - let resultArrived = customExpectation(description: "received result") - let team = Team.insertNewObject(in: uiMOC) - let userA = ZMUser.insertNewObject(in: uiMOC) - let userB = ZMUser.insertNewObject(in: uiMOC) - let userC = ZMUser.insertNewObject(in: uiMOC) - let memberA = Member.insertNewObject(in: uiMOC) - let memberB = Member.insertNewObject(in: uiMOC) // active partner - let memberC = Member.insertNewObject(in: uiMOC) // non-active partner - let conversation = ZMConversation.insertNewObject(in: uiMOC) - - conversation.conversationType = .group - conversation.remoteIdentifier = UUID() - conversation.addParticipantsAndUpdateConversationState( - users: Set([userA, userB, ZMUser.selfUser(in: uiMOC)]), - role: nil - ) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) + } - userA.name = "Member A" - userB.name = "Member B" - userC.name = "Member C" + // then + XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA]) + } + + func testThatItCanExcludeNonActivePartnersLocally() async throws { + let (userA, userB, task) = try await uiMOC.perform { [self] in + // given + let team = Team.insertNewObject(in: uiMOC) + let userA = ZMUser.insertNewObject(in: uiMOC) + let userB = ZMUser.insertNewObject(in: uiMOC) + let userC = ZMUser.insertNewObject(in: uiMOC) + let memberA = Member.insertNewObject(in: uiMOC) + let memberB = Member.insertNewObject(in: uiMOC) // active partner + let memberC = Member.insertNewObject(in: uiMOC) // non-active partner + let conversation = ZMConversation.insertNewObject(in: uiMOC) + + conversation.conversationType = .group + conversation.remoteIdentifier = UUID() + conversation.addParticipantsAndUpdateConversationState( + users: Set([userA, userB, ZMUser.selfUser(in: uiMOC)]), + role: nil + ) - memberA.team = team - memberA.user = userA - memberA.permissions = .member + userA.name = "Member A" + userB.name = "Member B" + userC.name = "Member C" - memberB.team = team - memberB.user = userB - memberB.permissions = .partner + memberA.team = team + memberA.user = userA + memberA.permissions = .member - memberC.team = team - memberC.user = userC - memberC.permissions = .partner + memberB.team = team + memberB.user = userB + memberB.permissions = .partner - uiMOC.saveOrRollback() + memberC.team = team + memberC.user = userC + memberC.permissions = .partner - let request = SearchRequest(query: "", searchOptions: [.teamMembers, .excludeNonActivePartners], team: team) - let task = makeSearchTask(request: request) + try uiMOC.save() - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA, userB]) + let request = SearchRequest(query: "", searchOptions: [.teamMembers, .excludeNonActivePartners], team: team) + let task = makeSearchTask(request: request) + return (userA, userB, task) } // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) - } + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) + } - func testThatItIncludesNonActivePartnersLocally_WhenSearchingWithExactHandle() { - // given - let resultArrived = customExpectation(description: "received result") - let team = Team.insertNewObject(in: uiMOC) - let userA = ZMUser.insertNewObject(in: uiMOC) - let memberA = Member.insertNewObject(in: uiMOC) // non-active partner + // then + XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA, userB]) + } - userA.name = "Member A" - userA.handle = "abc" + func testThatItIncludesNonActivePartnersLocally_WhenSearchingWithExactHandle() async throws { + let (userA, task) = await uiMOC.perform { [self] in + // given + let team = Team.insertNewObject(in: uiMOC) + let userA = ZMUser.insertNewObject(in: uiMOC) + let memberA = Member.insertNewObject(in: uiMOC) // non-active partner - memberA.team = team - memberA.user = userA - memberA.permissions = .partner + userA.name = "Member A" + userA.handle = "abc" - uiMOC.saveOrRollback() + memberA.team = team + memberA.user = userA + memberA.permissions = .partner - let request = SearchRequest(query: "@abc", searchOptions: [.teamMembers, .excludeNonActivePartners], team: team) - let task = makeSearchTask(request: request) + uiMOC.saveOrRollback() - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA]) + let searchOptions: SearchOptions = [.teamMembers, .excludeNonActivePartners] + let request = SearchRequest(query: "@abc", searchOptions: searchOptions, team: team) + let task = makeSearchTask(request: request) + return (userA, task) } // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) + } + + // then + XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA]) } - func testThatItIncludesNonActivePartnersLocally_WhenSelfUserCreatedPartner() { - // given - let resultArrived = customExpectation(description: "received result") - let team = Team.insertNewObject(in: uiMOC) - let userA = ZMUser.insertNewObject(in: uiMOC) - let memberA = Member.insertNewObject(in: uiMOC) // non-active partner + func testThatItIncludesNonActivePartnersLocally_WhenSelfUserCreatedPartner() async throws { + let (userA, team) = try await uiMOC.perform { [uiMOC] in + // given + let team = Team.insertNewObject(in: uiMOC) + let userA = ZMUser.insertNewObject(in: uiMOC) + let memberA = Member.insertNewObject(in: uiMOC) // non-active partner + + userA.name = "Member A" + userA.handle = "abc" - userA.name = "Member A" - userA.handle = "abc" + memberA.team = team + memberA.user = userA + memberA.permissions = .partner + memberA.createdBy = ZMUser.selfUser(in: uiMOC) - memberA.team = team - memberA.user = userA - memberA.permissions = .partner - memberA.createdBy = ZMUser.selfUser(in: uiMOC) + try uiMOC.save() - uiMOC.saveOrRollback() + return (userA, team) + } let request = SearchRequest(query: "", searchOptions: [.teamMembers, .excludeNonActivePartners], team: team) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA]) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA]) } // MARK: Conversation Search - func testThatItFindsASingleConversation() { + func testThatItFindsASingleConversation() async throws { // given - let resultArrived = customExpectation(description: "received result") - let conversation = createGroupConversation(withName: "Somebody") + let conversation = try await createGroupConversation(withName: "Somebody") let request = SearchRequest(query: "Somebody", searchOptions: [.conversations]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.conversations, [conversation]) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(result.conversations, [conversation]) } - func testThatItDoesFindConversationsUsingPartialNames() { + func testThatItDoesFindConversationsUsingPartialNames() async throws { // given - let resultArrived = customExpectation(description: "received result") - let conversation = createGroupConversation(withName: "Somebody") + let conversation = try await createGroupConversation(withName: "Somebody") let request = SearchRequest(query: "mebo", searchOptions: [.conversations]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.conversations, [conversation]) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(result.conversations, [conversation]) } - func testThatItFindsSeveralConversations() { + func testThatItFindsSeveralConversations() async throws { // given - let resultArrived = customExpectation(description: "received result") - let conversation1 = createGroupConversation(withName: "Candy Apple Records") - let conversation2 = createGroupConversation(withName: "Landspeed Records") - _ = createGroupConversation(withName: "New Day Rising") + let conversation1 = try await createGroupConversation(withName: "Candy Apple Records") + let conversation2 = try await createGroupConversation(withName: "Landspeed Records") + _ = try await createGroupConversation(withName: "New Day Rising") let request = SearchRequest(query: "Records", searchOptions: [.conversations]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.conversations, [conversation1, conversation2]) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(result.conversations, [conversation1, conversation2]) } - func testThatConversationSearchIsCaseInsensitive() { + func testThatConversationSearchIsCaseInsensitive() async throws { // given - let resultArrived = customExpectation(description: "received result") - let conversation = createGroupConversation(withName: "SoMEBody") + let conversation = try await createGroupConversation(withName: "SoMEBody") let request = SearchRequest(query: "someBodY", searchOptions: [.conversations]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.conversations, [conversation]) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(result.conversations, [conversation]) } - func testThatConversationSearchIsInsensitiveToDiacritics() { + func testThatConversationSearchIsInsensitiveToDiacritics() async throws { // given - let resultArrived = customExpectation(description: "received result") - let conversation = createGroupConversation(withName: "Sömëbodÿ") + let conversation = try await createGroupConversation(withName: "Sömëbodÿ") let request = SearchRequest(query: "Sømebôdy", searchOptions: [.conversations]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.conversations, [conversation]) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(result.conversations, [conversation]) } - func testThatItOnlyFindsGroupConversations() { + func testThatItOnlyFindsGroupConversations() async throws { // given - let resultArrived = customExpectation(description: "received result") - let groupConversation = createGroupConversation(withName: "Group Conversation") - let oneOnOneConversation = createGroupConversation(withName: "OneOnOne Conversation") - oneOnOneConversation.conversationType = .oneOnOne - let selfConversation = createGroupConversation(withName: "Self Conversation") - selfConversation.conversationType = .self + let groupConversation = try await createGroupConversation(withName: "Group Conversation") + let oneOnOneConversation = try await createGroupConversation(withName: "OneOnOne Conversation") + await uiMOC.perform { + oneOnOneConversation.conversationType = .oneOnOne + } + let selfConversation = try await createGroupConversation(withName: "Self Conversation") - uiMOC.saveOrRollback() + try await uiMOC.perform { [uiMOC] in + selfConversation.conversationType = .self + try uiMOC.save() + } let request = SearchRequest(query: "Conversation", searchOptions: [.conversations]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.conversations, [groupConversation]) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(result.conversations, [groupConversation]) } - func testThatItFindsConversationsThatDoNotHaveAUserDefinedName() { + func testThatItFindsConversationsThatDoNotHaveAUserDefinedName() async throws { // given - let resultArrived = customExpectation(description: "received result") - let conversation = ZMConversation.insertNewObject(in: uiMOC) - conversation.conversationType = .group - - let user1 = createConnectedUser(withName: "Shinji") - let user2 = createConnectedUser(withName: "Asuka") - let user3 = createConnectedUser(withName: "Rëï") + let conversation = await uiMOC.perform { [uiMOC] in + let conversation = ZMConversation.insertNewObject(in: uiMOC) + conversation.conversationType = .group + return conversation + } - conversation.addParticipantsAndUpdateConversationState(users: [user1, user2, user3], role: nil) + let user1 = try await createConnectedUser(withName: "Shinji") + let user2 = try await createConnectedUser(withName: "Asuka") + let user3 = try await createConnectedUser(withName: "Rëï") - uiMOC.saveOrRollback() + try await uiMOC.perform { [uiMOC] in + conversation.addParticipantsAndUpdateConversationState(users: [user1, user2, user3], role: nil) + try uiMOC.save() + } let request = SearchRequest(query: "Rei", searchOptions: [.conversations, .contacts]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.conversations, [conversation]) - XCTAssertEqual(result.contacts.compactMap(\.user), [user3]) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(result.conversations, [conversation]) + XCTAssertEqual(result.contacts.compactMap(\.user), [user3]) } - func testThatItFindsConversationsThatContainsSearchTermOnlyInParticipantName() { + func testThatItFindsConversationsThatContainsSearchTermOnlyInParticipantName() async throws { // given - let resultArrived = customExpectation(description: "received result") - let conversation = createGroupConversation(withName: "Summertime") - let user = createConnectedUser(withName: "Rëï") - conversation.addParticipantAndUpdateConversationState(user: user, role: nil) - - uiMOC.saveOrRollback() + let conversation = try await createGroupConversation(withName: "Summertime") + let user = try await createConnectedUser(withName: "Rëï") + try await uiMOC.perform { [uiMOC] in + conversation.addParticipantAndUpdateConversationState(user: user, role: nil) + try uiMOC.save() + } let request = SearchRequest(query: "Rei", searchOptions: [.conversations]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.conversations, [conversation]) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(result.conversations, [conversation]) } - func testThatItOrdersConversationsByUserDefinedName() { + func testThatItOrdersConversationsByUserDefinedName() async throws { // given - let resultArrived = customExpectation(description: "received result") - let conversation1 = createGroupConversation(withName: "FooA") - let conversation2 = createGroupConversation(withName: "FooC") - let conversation3 = createGroupConversation(withName: "FooB") + let conversation1 = try await createGroupConversation(withName: "FooA") + let conversation2 = try await createGroupConversation(withName: "FooC") + let conversation3 = try await createGroupConversation(withName: "FooB") let request = SearchRequest(query: "Foo", searchOptions: [.conversations]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.conversations, [conversation1, conversation3, conversation2]) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(result.conversations, [conversation1, conversation3, conversation2]) } - func testThatItOrdersConversationsByUserDefinedNameFirstAndByParticipantNameSecond() { + func testThatItOrdersConversationsByUserDefinedNameFirstAndByParticipantNameSecond() async throws { // given - let resultArrived = customExpectation(description: "received result") - let user1 = createConnectedUser(withName: "Bla") - let user2 = createConnectedUser(withName: "FooB") + let user1 = try await createConnectedUser(withName: "Bla") + let user2 = try await createConnectedUser(withName: "FooB") - let conversation1 = createGroupConversation(withName: "FooA") - let conversation2 = createGroupConversation(withName: "Bar") - let conversation3 = createGroupConversation(withName: "FooB") - let conversation4 = createGroupConversation(withName: "Bar") + let conversation1 = try await createGroupConversation(withName: "FooA") + let conversation2 = try await createGroupConversation(withName: "Bar") + let conversation3 = try await createGroupConversation(withName: "FooB") + let conversation4 = try await createGroupConversation(withName: "Bar") - conversation2.addParticipantAndUpdateConversationState(user: user1, role: nil) - conversation4.addParticipantsAndUpdateConversationState(users: [user1, user2], role: nil) + try await uiMOC.perform { [uiMOC] in + conversation2.addParticipantAndUpdateConversationState(user: user1, role: nil) + conversation4.addParticipantsAndUpdateConversationState(users: [user1, user2], role: nil) - uiMOC.saveOrRollback() + try uiMOC.save() + } let request = SearchRequest(query: "Foo", searchOptions: [.conversations]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.conversations, [conversation1, conversation3, conversation4]) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(result.conversations, [conversation1, conversation3, conversation4]) } - func testThatItFiltersConversationWhenTheQueryStartsWithAtSymbol() { + func testThatItFiltersConversationWhenTheQueryStartsWithAtSymbol() async throws { // given - let resultArrived = customExpectation(description: "received result") - _ = createGroupConversation(withName: "New Day Rising") - _ = createGroupConversation(withName: "Landspeed Records") + _ = try await createGroupConversation(withName: "New Day Rising") + _ = try await createGroupConversation(withName: "Landspeed Records") let request = SearchRequest(query: "@records", searchOptions: [.conversations]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.conversations, []) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(result.conversations, []) } - func testThatItReturnsAllConversationsWhenPassingTeamParameter() { + func testThatItReturnsAllConversationsWhenPassingTeamParameter() async throws { // given - let resultArrived = customExpectation(description: "received result") - let team = Team.insertNewObject(in: uiMOC) - let conversationInTeam = createGroupConversation(withName: "Beach Club") - let conversationNotInTeam = createGroupConversation(withName: "Beach Club") - - conversationInTeam.team = team + let team = await uiMOC.perform { [uiMOC] in + Team.insertNewObject(in: uiMOC) + } + let conversationInTeam = try await createGroupConversation(withName: "Beach Club") + let conversationNotInTeam = try await createGroupConversation(withName: "Beach Club") - uiMOC.saveOrRollback() + try await uiMOC.perform { [uiMOC] in + conversationInTeam.team = team + try uiMOC.save() + } let request = SearchRequest(query: "Beach", searchOptions: [.conversations], team: team) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(Set(result.conversations), Set([conversationInTeam, conversationNotInTeam])) + // when + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + await uiMOC.perform { + resultAggregator(&result) } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(Set(result.conversations), Set([conversationInTeam, conversationNotInTeam])) } // MARK: Directory Search - func testThatItSendsASearchRequest() { + func testThatItSendsASearchRequest() async throws { // given let request = SearchRequest(query: "Steve O'Hara & Söhne", searchOptions: [.directory]) let task = makeSearchTask(request: request, apiVersion: .v2) // when - task.performRemoteSearch() + _ = await task.performRemoteSearch() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then @@ -895,39 +912,39 @@ final class SearchTaskTests: DatabaseTest { ) } - func testThatItDoesNotSendASearchRequestIfSeachingLocally() { + func testThatItDoesNotSendASearchRequestIfSeachingLocally() async throws { // given let request = SearchRequest(query: "Steve O'Hara & Söhne", searchOptions: [.contacts]) let task = makeSearchTask(request: request) // when - task.performRemoteSearch() + _ = await task.performRemoteSearch() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then XCTAssertEqual(mockTransportSession.receivedRequests().count, 0) } - func testThatItDoesNotSendASearchRequestIfLocalResultsOnly() { + func testThatItDoesNotSendASearchRequestIfLocalResultsOnly() async throws { // given let request = SearchRequest(query: "Steve O'Hara & Söhne", searchOptions: [.directory, .localResultsOnly]) let task = makeSearchTask(request: request) // when - task.performRemoteSearch() + _ = await task.performRemoteSearch() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then XCTAssertEqual(mockTransportSession.receivedRequests().count, 0) } - func testThatItEncodesAPlusCharacterInTheSearchURL() { + func testThatItEncodesAPlusCharacterInTheSearchURL() async throws { // given let request = SearchRequest(query: "foo+bar@example.com", searchOptions: [.directory]) let task = makeSearchTask(request: request, apiVersion: .v2) // when - task.performRemoteSearch() + _ = await task.performRemoteSearch() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then @@ -937,7 +954,7 @@ final class SearchTaskTests: DatabaseTest { ) } - func testThatItEncodesUnsafeCharactersInRequest() { + func testThatItEncodesUnsafeCharactersInRequest() async throws { // RFC 3986 Section 3.4 "Query" // // @@ -948,7 +965,7 @@ final class SearchTaskTests: DatabaseTest { let task = makeSearchTask(request: request, apiVersion: .v2) // when - task.performRemoteSearch() + _ = await task.performRemoteSearch() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then @@ -958,9 +975,8 @@ final class SearchTaskTests: DatabaseTest { ) } - func testThatItCallsCompletionHandlerForDirectorySearch() { + func testThatItCallsCompletionHandlerForDirectorySearch() async throws { // given - let resultArrived = customExpectation(description: "received result") let request = SearchRequest(query: "User", searchOptions: [.directory]) let task = makeSearchTask(request: request, apiVersion: .v2) @@ -968,20 +984,18 @@ final class SearchTaskTests: DatabaseTest { remoteChanges.insertUser(withName: "User A") } - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.directory.first?.name, "User A") - } - // when - task.performRemoteSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performRemoteSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.directory.first?.name, "User A") } // MARK: Directory Search - Membership lookup - func testThatItMakesRequestToFetchTeamMembershipMetadata() { + func testThatItMakesRequestToFetchTeamMembershipMetadata() async throws { // given let request = SearchRequest(query: "User", searchOptions: [.directory, .teamMembers]) let task = makeSearchTask(request: request, apiVersion: .v2) @@ -995,7 +1009,7 @@ final class SearchTaskTests: DatabaseTest { } // when - task.performRemoteSearch() + _ = await task.performRemoteSearch() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then @@ -1007,7 +1021,7 @@ final class SearchTaskTests: DatabaseTest { ) } - func testThatItDoesNotMakeRequestToFetchTeamMembershipMetadata_WhenLocalResultsOnly() { + func testThatItDoesNotMakeRequestToFetchTeamMembershipMetadata_WhenLocalResultsOnly() async throws { // given let request = SearchRequest(query: "User", searchOptions: [.directory, .teamMembers, .localResultsOnly]) let task = makeSearchTask(request: request, apiVersion: .v2) @@ -1021,16 +1035,15 @@ final class SearchTaskTests: DatabaseTest { } // when - task.performRemoteSearch() + _ = await task.performRemoteSearch() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then XCTAssertTrue(mockTransportSession.receivedRequests().isEmpty) } - func testThatItCallsCompletionHandlerForTeamMemberDirectorySearch() { + func testThatItCallsCompletionHandlerForTeamMemberDirectorySearch() async throws { // given - let resultArrived = customExpectation(description: "received result") let request = SearchRequest(query: "User", searchOptions: [.directory, .teamMembers]) let task = makeSearchTask(request: request, apiVersion: .v2) @@ -1045,27 +1058,25 @@ final class SearchTaskTests: DatabaseTest { member.permissions = .admin } - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.teamMembers.first?.name, "User A") - XCTAssertEqual(result.teamMembers.first?.teamRole, .admin) - } - // when - task.performRemoteSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performRemoteSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.teamMembers.first?.name, "User A") + XCTAssertEqual(result.teamMembers.first?.teamRole, .admin) } // MARK: Services search - func testThatItSendsASearchServicesRequest() { + func testThatItSendsASearchServicesRequest() async throws { // given let request = SearchRequest(query: "Steve O'Hara & Söhne", searchOptions: [.services]) let task = makeSearchTask(request: request) // when - task.performRemoteSearchForServices() + _ = await task.performRemoteSearchForServices() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 1)) // wait again to fix flaky test so second group is entered XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 1)) @@ -1077,22 +1088,21 @@ final class SearchTaskTests: DatabaseTest { ) } - func testThatItDoesNotSendASearchServicesRequest_WhenLocalResultsOnly() { + func testThatItDoesNotSendASearchServicesRequest_WhenLocalResultsOnly() async throws { // given let request = SearchRequest(query: "Steve O'Hara & Söhne", searchOptions: [.services, .localResultsOnly]) let task = makeSearchTask(request: request) // when - task.performRemoteSearchForServices() + _ = await task.performRemoteSearchForServices() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then XCTAssertTrue(mockTransportSession.receivedRequests().isEmpty) } - func testThatItCallsCompletionHandlerForServicesSearch() { + func testThatItCallsCompletionHandlerForServicesSearch() async throws { // given - let resultArrived = customExpectation(description: "received result") let request = SearchRequest(query: "Service", searchOptions: [.services]) let task = makeSearchTask(request: request) @@ -1104,15 +1114,13 @@ final class SearchTaskTests: DatabaseTest { ) } - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.services.first?.name, "Service A") - } - // when - task.performRemoteSearchForServices() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performRemoteSearchForServices() + resultAggregator(&result) + + // then + XCTAssertEqual(result.services.first?.name, "Service A") } func testThatItTrimsThePrefixQuery() throws { @@ -1142,27 +1150,27 @@ final class SearchTaskTests: DatabaseTest { // MARK: User lookup - func testThatItSendsAUserLookupRequest() { + func testThatItSendsAUserLookupRequest() async throws { // given let userId = UUID() let task = makeSearchTask(lookupUserId: userId) // when - task.performUserLookup() + _ = await task.performUserLookup() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then XCTAssertEqual(mockTransportSession.receivedRequests().first?.path, "/users/\(userId.transportString())") } - func testThatItSendsAUserLookupRequest_IfApiVersionIsV2AndAbove() { + func testThatItSendsAUserLookupRequest_IfApiVersionIsV2AndAbove() async throws { // given let userId = UUID() let domain = "wire.com" let task = makeSearchTask(lookupUserId: userId, domain: domain, apiVersion: .v2) // when - task.performUserLookup() + _ = await task.performUserLookup() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then @@ -1172,10 +1180,8 @@ final class SearchTaskTests: DatabaseTest { ) } - func testThatItCallsCompletionHandlerForUserLookup() { + func testThatItCallsCompletionHandlerForUserLookup() async throws { // given - let resultArrived = customExpectation(description: "received result") - var userId: UUID! mockTransportSession.performRemoteChanges { remoteChanges in let mockUser = remoteChanges.insertUser(withName: "User A") @@ -1183,39 +1189,37 @@ final class SearchTaskTests: DatabaseTest { } let task = makeSearchTask(lookupUserId: userId) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.directory.first?.name, "User A") - } - // when - task.performUserLookup() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performUserLookup() + resultAggregator(&result) + + // then + XCTAssertEqual(result.directory.first?.name, "User A") } // MARK: Federated search - func testThatItDoesNotSendAFederatedUserSearchRequest__WhenLocalSearchOnly() throws { + func testThatItDoesNotSendAFederatedUserSearchRequest__WhenLocalSearchOnly() async throws { // given let searchRequest = SearchRequest(query: "john@example.com", searchOptions: [.federated, .localResultsOnly]) let task = makeSearchTask(request: searchRequest, apiVersion: .v3) // when - task.performRemoteSearch() + _ = await task.performRemoteSearch() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then XCTAssertTrue(mockTransportSession.receivedRequests().isEmpty) } - func testThatItSendsAFederatedUserSearchRequest() throws { + func testThatItSendsAFederatedUserSearchRequest() async throws { // given let searchRequest = SearchRequest(query: "john@example.com", searchOptions: .federated) let task = makeSearchTask(request: searchRequest, apiVersion: .v3) // when - task.performRemoteSearch() + _ = await task.performRemoteSearch() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then @@ -1224,10 +1228,9 @@ final class SearchTaskTests: DatabaseTest { XCTAssertEqual(request.path, "/v3/search/contacts?q=john&domain=example.com&size=10") } - func testThatItCallsCompletionHandlerForFederatedUserSearch_WhenUserExists() { + func testThatItCallsCompletionHandlerForFederatedUserSearch_WhenUserExists() async throws { // given let federatedDomain = "example.com" - let resultArrived = customExpectation(description: "received result") mockTransportSession.federatedDomains = [federatedDomain] mockTransportSession.performRemoteChanges { remoteChanges in @@ -1239,42 +1242,36 @@ final class SearchTaskTests: DatabaseTest { let searchRequest = SearchRequest(query: "john@example.com", searchOptions: .federated) let task = makeSearchTask(request: searchRequest, apiVersion: .v3) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.directory.first?.name, "John Doe") - } - // when - task.performRemoteSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performRemoteSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.directory.first?.name, "John Doe") } - func testThatItCallsCompletionHandlerForFederatedUserSearch_WhenUserDoesntExist() { + func testThatItCallsCompletionHandlerForFederatedUserSearch_WhenUserDoesntExist() async throws { // given - let resultArrived = customExpectation(description: "received result") mockTransportSession.federatedDomains = ["example.com"] let searchRequest = SearchRequest(query: "john@example.com", searchOptions: .federated) let task = makeSearchTask(request: searchRequest, apiVersion: .v3) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertTrue(result.directory.isEmpty) - } - // when - task.performRemoteSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performRemoteSearch() + resultAggregator(&result) + + // then + XCTAssertTrue(result.directory.isEmpty) } // MARK: Combined results - func testThatRemoteResultsIncludePreviousLocalResults() { + func testThatRemoteResultsIncludePreviousLocalResults() async throws { // given - let localResultArrived = customExpectation(description: "received local result") - let user = createConnectedUser(withName: "userA") + let user = try await createConnectedUser(withName: "userA") mockTransportSession.performRemoteChanges { remoteChanges in remoteChanges.insertUser(withName: "UserB") @@ -1283,34 +1280,25 @@ final class SearchTaskTests: DatabaseTest { let request = SearchRequest(query: "user", searchOptions: [.contacts, .directory]) let task = makeSearchTask(request: request, apiVersion: .v2) - // expect - task.addResultHandler { result, _ in - localResultArrived.fulfill() - XCTAssertTrue(result.contacts.compactMap(\.user).contains(user)) - } + // when - perform local search + var result = SearchResult() + let localResultAggregator = await task.performLocalSearch() + localResultAggregator(&result) - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then - local result contains user + XCTAssertTrue(result.contacts.compactMap(\.user).contains(user)) - // given - let remoteResultArrived = customExpectation(description: "received remote result") + // when - perform remote search + let remoteResultAggregator = await task.performRemoteSearch() + remoteResultAggregator(&result) - // expect - task.addResultHandler { result, _ in - remoteResultArrived.fulfill() - XCTAssertTrue(result.contacts.compactMap(\.user).contains(user)) - } - - // when - task.performRemoteSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then - remote result still contains local user + XCTAssertTrue(result.contacts.compactMap(\.user).contains(user)) } - func testThatLocalResultsIncludePreviousRemoteResults() { + func testThatLocalResultsIncludePreviousRemoteResults() async throws { // given - let remoteResultArrived = customExpectation(description: "received remote result") - _ = createConnectedUser(withName: "userA") + _ = try await createConnectedUser(withName: "userA") mockTransportSession.performRemoteChanges { remoteChanges in remoteChanges.insertUser(withName: "UserB") @@ -1319,52 +1307,39 @@ final class SearchTaskTests: DatabaseTest { let request = SearchRequest(query: "user", searchOptions: [.contacts, .directory]) let task = makeSearchTask(request: request, apiVersion: .v2) - // expect - task.addResultHandler { result, _ in - remoteResultArrived.fulfill() - XCTAssertEqual(result.directory.count, 1) - } + // when - perform remote search + var result = SearchResult() + let remoteResultAggregator = await task.performRemoteSearch() + remoteResultAggregator(&result) - // when - task.performRemoteSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then - remote result contains directory user + XCTAssertEqual(result.directory.count, 1) - // given - let localResultArrived = customExpectation(description: "received local result") + // when - perform local search + let localResultAggregator = await task.performLocalSearch() + localResultAggregator(&result) - // expect - task.addResultHandler { result, _ in - localResultArrived.fulfill() - XCTAssertEqual(result.directory.count, 1) - } - - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then - local result still contains directory user + XCTAssertEqual(result.directory.count, 1) } - func testThatTaskIsCompletedAfterLocalResult() { + func testThatTaskIsCompletedAfterLocalResult() async throws { // given - let localResultArrived = customExpectation(description: "received local result") - let user = createConnectedUser(withName: "userA") + let user = try await createConnectedUser(withName: "userA") let request = SearchRequest(query: "user", searchOptions: [.contacts]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, completed in - localResultArrived.fulfill() - XCTAssertTrue(result.contacts.compactMap(\.user).contains(user)) - XCTAssertTrue(completed) - } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertTrue(result.contacts.compactMap(\.user).contains(user)) } - func testThatTaskIsCompletedAfterRemoteResults() { + func testThatTaskIsCompletedAfterRemoteResults() async throws { // given - let remoteResultArrived = customExpectation(description: "received remote result") mockTransportSession.performRemoteChanges { remoteChanges in remoteChanges.insertUser(withName: "UserB") } @@ -1372,43 +1347,13 @@ final class SearchTaskTests: DatabaseTest { let request = SearchRequest(query: "user", searchOptions: [.directory]) let task = makeSearchTask(request: request, apiVersion: .v2) - // expect - task.addResultHandler { result, completed in - remoteResultArrived.fulfill() - XCTAssertEqual(result.directory.count, 1) - XCTAssertTrue(completed) - } - // when - task.performRemoteSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) - } + var result = SearchResult() + let resultAggregator = await task.performRemoteSearch() + resultAggregator(&result) - func testThatTaskIsCompletedOnlyAfterFinalResultArrives() { - // given - let intermediateResultArrived = customExpectation(description: "received intermediate result") - let finalResultsArrived = customExpectation(description: "received final result") - _ = createConnectedUser(withName: "userA") - - mockTransportSession.performRemoteChanges { remoteChanges in - remoteChanges.insertUser(withName: "UserB") - } - - let request = SearchRequest(query: "user", searchOptions: [.contacts, .directory]) - let task = makeSearchTask(request: request) - - // expect - task.addResultHandler { _, completed in - if completed { - finalResultsArrived.fulfill() - } else { - intermediateResultArrived.fulfill() - } - } - - // when - task.start() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then + XCTAssertEqual(result.directory.count, 1) } // MARK: - Helpers @@ -1418,7 +1363,7 @@ final class SearchTaskTests: DatabaseTest { apiVersion: APIVersion = .v0 ) -> SearchTask { SearchTask( - request: request, + type: .search(searchRequest: request), contextProvider: coreDataStack!, transportSession: mockTransportSession, searchUsersCache: mockCache, @@ -1431,12 +1376,14 @@ final class SearchTaskTests: DatabaseTest { domain: String = "wire.com", apiVersion: APIVersion = .v0 ) -> SearchTask { - SearchTask( - qualifiedID: QualifiedID(uuid: lookupUserId, domain: domain), + let qualifiedID = QualifiedID(uuid: lookupUserId, domain: domain) + return SearchTask( + type: .lookup(qualifiedID: qualifiedID), contextProvider: coreDataStack!, transportSession: mockTransportSession, searchUsersCache: mockCache, apiVersion: apiVersion ) } + } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/SearchResultsViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/SearchResultsViewController.swift index 020f56df06c..983c457f218 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/SearchResultsViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/SearchResultsViewController.swift @@ -261,7 +261,6 @@ final class SearchResultsViewController: UIViewController { options: SearchOptions ) { pendingSearchTask?.cancel() - pendingSearchTask = nil searchResultsView.emptyResultContainer.isHidden = true pendingSearchTask = Task { @@ -275,7 +274,7 @@ final class SearchResultsViewController: UIViewController { messageProtocol: filterConversation?.messageProtocol ) - handleSearchResult(result: result, isCompleted: true) + handleSearchResult(result: result) } catch { WireLogger.search.warn("Search failed with error: \(error.localizedDescription)") } @@ -315,12 +314,9 @@ final class SearchResultsViewController: UIViewController { } } - func handleSearchResult(result: SearchResult, isCompleted: Bool) { + func handleSearchResult(result: SearchResult) { updateSections(withSearchResult: result) - - if isCompleted { - isResultEmpty = sectionController.visibleSections.isEmpty - } + isResultEmpty = sectionController.visibleSections.isEmpty } func updateVisibleSections() { diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/StartUIViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/StartUIViewController.swift index b82c90e5462..67b1b8c58ef 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/StartUIViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/StartUIViewController.swift @@ -430,7 +430,7 @@ extension StartUIViewController: UISearchResultsUpdating, UISearchBarDelegate { object: nil ) - perform(#selector(performSearch), with: nil, afterDelay: 0.2) + perform(#selector(performSearch), with: nil, afterDelay: 0.25) } func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/SearchUserViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/SearchUserViewController.swift index 8a809fa530c..ce5d387cb42 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/SearchUserViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/SearchUserViewController.swift @@ -83,16 +83,7 @@ final class SearchUserViewController: UIViewController { super.viewDidLoad() activityIndicator.start() - - if let task = searchDirectory?.lookup(qualifiedID: qualifiedID) { - task.addResultHandler { [weak self] in - self?.activityIndicator.stop() - self?.handleSearchResult(searchResult: $0, isCompleted: $1) - } - task.start() - - pendingSearchTask = task - } + startLookup() } override func viewWillAppear(_ animated: Bool) { @@ -108,8 +99,21 @@ final class SearchUserViewController: UIViewController { // MARK: - Methods - private func handleSearchResult(searchResult: SearchResult, isCompleted: Bool) { - guard !resultHandled, isCompleted else { return } + private func startLookup() { + guard let searchDirectory else { return } + + Task { + let task = searchDirectory.createLookupTask(with: qualifiedID) + pendingSearchTask = task + let searchResult = await task.start() + pendingSearchTask = nil + activityIndicator.stop() + handleSearchResult(searchResult: searchResult) + } + } + + private func handleSearchResult(searchResult: SearchResult) { + guard !resultHandled else { return } guard let selfUser = ZMUser.selfUser() else { assertionFailure("ZMUser.selfUser() is nil") return