From 1305efffc3f4c9251451245034382b8b7ff54d35 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Wed, 28 Jan 2026 10:13:26 +0100 Subject: [PATCH 01/71] remove redundant argument --- .../ManagedObjectContext/CoreDataStack.swift | 2 +- .../UserSession/Search/SearchDirectory.swift | 6 -- .../UserSession/Search/SearchTask.swift | 64 +++++++------------ .../Tests/Source/DatabaseTest.swift | 4 -- .../UserSession/SearchDirectoryTests.swift | 1 - .../Source/UserSession/SearchTaskTests.swift | 2 - 6 files changed, 25 insertions(+), 54 deletions(-) diff --git a/wire-ios-data-model/Source/ManagedObjectContext/CoreDataStack.swift b/wire-ios-data-model/Source/ManagedObjectContext/CoreDataStack.swift index 6d3876e4d76..d6d2bf5e19b 100644 --- a/wire-ios-data-model/Source/ManagedObjectContext/CoreDataStack.swift +++ b/wire-ios-data-model/Source/ManagedObjectContext/CoreDataStack.swift @@ -48,7 +48,7 @@ public protocol ContextProvider { var viewContext: NSManagedObjectContext { get } func newBackgroundContext() -> NSManagedObjectContext var syncContext: NSManagedObjectContext { get } - var searchContext: NSManagedObjectContext { get } + var searchContext: NSManagedObjectContext { get } // TODO: delete var eventContext: NSManagedObjectContext { get } } diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift index 9462ef6a786..ef3654c8d00 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift @@ -21,7 +21,6 @@ import Foundation @objcMembers public class SearchDirectory: NSObject { - let searchContext: NSManagedObjectContext let contextProvider: ContextProvider let transportSession: TransportSessionType private let apiVersion: WireTransport.APIVersion? @@ -39,7 +38,6 @@ public class SearchDirectory: NSObject { public convenience init(userSession: ZMUserSession) { self.init( - searchContext: userSession.searchManagedObjectContext, contextProvider: userSession, transportSession: userSession.transportSession, searchUsersCache: userSession.searchUsersCache, @@ -50,7 +48,6 @@ public class SearchDirectory: NSObject { } init( - searchContext: NSManagedObjectContext, contextProvider: ContextProvider, transportSession: TransportSessionType, searchUsersCache: SearchUsersCache?, @@ -58,7 +55,6 @@ public class SearchDirectory: NSObject { refreshConversationsMissingMetadataAction: RecurringAction, apiVersion: WireTransport.APIVersion? ) { - self.searchContext = searchContext self.contextProvider = contextProvider self.transportSession = transportSession self.searchUsersCache = searchUsersCache @@ -74,7 +70,6 @@ public class SearchDirectory: NSObject { public func perform(_ request: SearchRequest) -> SearchTask { let task = SearchTask( task: .search(searchRequest: request), - searchContext: searchContext, contextProvider: contextProvider, transportSession: transportSession, searchUsersCache: searchUsersCache, @@ -96,7 +91,6 @@ public class SearchDirectory: NSObject { public func lookup(qualifiedID: QualifiedID) -> SearchTask { let task = SearchTask( task: .lookup(qualifiedID: qualifiedID), - searchContext: searchContext, contextProvider: contextProvider, transportSession: transportSession, 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 a8775cad5e9..2f03a32d59f 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -30,7 +30,6 @@ public class SearchTask { private let apiVersion: WireTransport.APIVersion? private let transportSession: TransportSessionType - private let searchContext: NSManagedObjectContext private let contextProvider: ContextProvider private let searchUsersCache: SearchUsersCache? @@ -79,7 +78,6 @@ public class SearchTask { convenience init( request: SearchRequest, - searchContext: NSManagedObjectContext, contextProvider: ContextProvider, transportSession: TransportSessionType, searchUsersCache: SearchUsersCache?, @@ -87,7 +85,6 @@ public class SearchTask { ) { self.init( task: .search(searchRequest: request), - searchContext: searchContext, contextProvider: contextProvider, transportSession: transportSession, searchUsersCache: searchUsersCache, @@ -97,7 +94,6 @@ public class SearchTask { convenience init( qualifiedID: QualifiedID, - searchContext: NSManagedObjectContext, contextProvider: ContextProvider, transportSession: TransportSessionType, searchUsersCache: SearchUsersCache?, @@ -105,7 +101,6 @@ public class SearchTask { ) { self.init( task: .lookup(qualifiedID: qualifiedID), - searchContext: searchContext, contextProvider: contextProvider, transportSession: transportSession, searchUsersCache: searchUsersCache, @@ -115,7 +110,6 @@ public class SearchTask { public init( task: Task, - searchContext: NSManagedObjectContext, contextProvider: ContextProvider, transportSession: TransportSessionType, searchUsersCache: SearchUsersCache?, @@ -123,7 +117,6 @@ public class SearchTask { ) { self.task = task self.transportSession = transportSession - self.searchContext = searchContext self.contextProvider = contextProvider self.searchUsersCache = searchUsersCache self.apiVersion = apiVersion @@ -173,8 +166,8 @@ extension SearchTask { tasksRemaining += 1 - searchContext.performGroupedBlock { [self] in - let selfUser = ZMUser.selfUser(in: searchContext) + contextProvider.searchContext.performGroupedBlock { [self] in + let selfUser = ZMUser.selfUser(in: contextProvider.searchContext) var options = SearchOptions() @@ -227,14 +220,14 @@ extension SearchTask { tasksRemaining += 1 - searchContext.performGroupedBlock { [self] in + contextProvider.searchContext.performGroupedBlock { [self] in var team: Team? if let teamObjectID = request.team?.objectID { - team = (try? searchContext.existingObject(with: teamObjectID)) as? Team + team = (try? contextProvider.searchContext.existingObject(with: teamObjectID)) as? Team } - let selfUser = ZMUser.selfUser(in: searchContext) + let selfUser = ZMUser.selfUser(in: contextProvider.searchContext) let connectedUsers = request.searchOptions .contains(.contacts) ? connectedUsers( matchingQuery: request.normalizedQuery, @@ -263,7 +256,7 @@ extension SearchTask { searchUsersCache: searchUsersCache ) } - .filter { !$0.hasEmptyName } + .filter { $0.name?.isEmpty == false } let copiedteamMembers = teamMembers.compactMap { contextProvider.viewContext.object(with: $0.objectID) as? Member @@ -296,9 +289,9 @@ extension SearchTask { } private func filterNonActiveTeamMembers(members: [Member]) -> [Member] { - let activeConversations = ZMUser.selfUser(in: searchContext).activeConversations + let activeConversations = ZMUser.selfUser(in: contextProvider.searchContext).activeConversations let activeContacts = Set(activeConversations.flatMap(\.localParticipants)) - let selfUser = ZMUser.selfUser(in: searchContext) + let selfUser = ZMUser.selfUser(in: contextProvider.searchContext) return members.filter { guard let user = $0.user else { return false } @@ -315,8 +308,8 @@ extension SearchTask { if searchOptions.contains(.excludeNonActivePartners) { let query = query.strippingLeadingAtSign() - let selfUser = ZMUser.selfUser(in: searchContext) - let activeConversations = ZMUser.selfUser(in: searchContext).activeConversations + let selfUser = ZMUser.selfUser(in: contextProvider.searchContext) + let activeConversations = ZMUser.selfUser(in: contextProvider.searchContext).activeConversations let activeContacts = Set(activeConversations.flatMap(\.localParticipants)) result = result.filter { membership in @@ -342,7 +335,7 @@ extension SearchTask { ZMUser.sortedFetchRequest(with: ZMUser.predicateForConnectedUsers(withSearch: query)) } - return searchContext.fetchOrAssert(request: fetchRequest) as? [ZMUser] ?? [] + return contextProvider.searchContext.fetchOrAssert(request: fetchRequest) as? [ZMUser] ?? [] } func conversations(matchingQuery query: SearchRequest.Query, selfUser: ZMUser) -> [ZMConversation] { @@ -354,7 +347,7 @@ extension SearchTask { )) fetchRequest.sortDescriptors = [NSSortDescriptor(key: ZMNormalizedUserDefinedNameKey, ascending: true)] - var conversations = searchContext.fetchOrAssert(request: fetchRequest) as? [ZMConversation] ?? [] + var conversations = contextProvider.searchContext.fetchOrAssert(request: fetchRequest) as? [ZMConversation] ?? [] if query.isHandleQuery { // if we are searching for a username only include conversations with matching displayName @@ -389,7 +382,7 @@ extension SearchTask { tasksRemaining += 1 - searchContext.performGroupedBlock { [self] in + contextProvider.searchContext.performGroupedBlock { [self] in let request = type(of: self).searchRequestForUser(qualifiedID: qualifiedID, apiVersion: apiVersion) request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in defer { @@ -411,7 +404,7 @@ extension SearchTask { } }) - request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in + request.add(ZMTaskCreatedHandler(on: contextProvider.searchContext) { [weak self] taskIdentifier in self?.userLookupTaskIdentifier = taskIdentifier }) @@ -449,7 +442,7 @@ extension SearchTask { tasksRemaining += 1 - searchContext.performGroupedBlock { [self] in + contextProvider.searchContext.performGroupedBlock { [self] in let request = Self.searchRequestInDirectory(withRequest: searchRequest, apiVersion: apiVersion) request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in @@ -476,7 +469,7 @@ extension SearchTask { } }) - request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in + request.add(ZMTaskCreatedHandler(on: contextProvider.searchContext) { [weak self] taskIdentifier in self?.directoryTaskIdentifier = taskIdentifier }) @@ -524,7 +517,7 @@ extension SearchTask { }) - request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in + request.add(ZMTaskCreatedHandler(on: contextProvider.searchContext) { [weak self] taskIdentifier in self?.teamMembershipTaskIdentifier = taskIdentifier }) @@ -599,7 +592,7 @@ extension SearchTask { tasksRemaining += 1 - searchContext.performGroupedBlock { [self] in + contextProvider.searchContext.performGroupedBlock { [self] in let request = type(of: self).searchRequestInDirectory( withHandle: searchRequest.query.string, apiVersion: apiVersion @@ -659,7 +652,7 @@ extension SearchTask { } }) - request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in + request.add(ZMTaskCreatedHandler(on: contextProvider.searchContext) { [weak self] taskIdentifier in self?.handleTaskIdentifier = taskIdentifier }) @@ -685,7 +678,9 @@ extension SearchTask { extension SearchTask { func performRemoteSearchForServices() { - let teamIdentifier = searchContext.performAndWait { ZMUser.selfUser(in: searchContext).team?.remoteIdentifier } + let teamIdentifier = contextProvider.searchContext.performAndWait { + ZMUser.selfUser(in: contextProvider.searchContext).team?.remoteIdentifier + } guard let apiVersion, let teamIdentifier, @@ -696,7 +691,7 @@ extension SearchTask { tasksRemaining += 1 - searchContext.performGroupedBlock { [self] in + contextProvider.searchContext.performGroupedBlock { [self] in let request = type(of: self).servicesSearchRequest( teamIdentifier: teamIdentifier, @@ -728,7 +723,7 @@ extension SearchTask { } }) - request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in + request.add(ZMTaskCreatedHandler(on: contextProvider.searchContext) { [weak self] taskIdentifier in self?.servicesTaskIdentifier = taskIdentifier }) @@ -752,14 +747,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/DatabaseTest.swift b/wire-ios-sync-engine/Tests/Source/DatabaseTest.swift index 2c752135e72..864004a27b6 100644 --- a/wire-ios-sync-engine/Tests/Source/DatabaseTest.swift +++ b/wire-ios-sync-engine/Tests/Source/DatabaseTest.swift @@ -40,10 +40,6 @@ class DatabaseTest: ZMTBaseTest { coreDataStack!.syncContext } - var searchMOC: NSManagedObjectContext { - coreDataStack!.searchContext - } - var sharedContainerURL: URL? { let bundleIdentifier = Bundle.main.bundleIdentifier let groupIdentifier = "group." + bundleIdentifier! diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift index 9e23de73b85..764c210fff1 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift @@ -61,7 +61,6 @@ final class SearchDirectoryTests: DatabaseTest { private func makeSearchDirectory(apiVersion: APIVersion) -> SearchDirectory { SearchDirectory( - searchContext: searchMOC, contextProvider: coreDataStack!, transportSession: mockTransport, searchUsersCache: mockCache, diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift index bea95e9daac..e18330ccb84 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift @@ -1419,7 +1419,6 @@ final class SearchTaskTests: DatabaseTest { ) -> SearchTask { SearchTask( request: request, - searchContext: searchMOC, contextProvider: coreDataStack!, transportSession: mockTransportSession, searchUsersCache: mockCache, @@ -1434,7 +1433,6 @@ final class SearchTaskTests: DatabaseTest { ) -> SearchTask { SearchTask( qualifiedID: QualifiedID(uuid: lookupUserId, domain: domain), - searchContext: searchMOC, contextProvider: coreDataStack!, transportSession: mockTransportSession, searchUsersCache: mockCache, From 09ebe6c3683e42fee363565e4bc77990b75ba007 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Wed, 28 Jan 2026 09:51:49 +0000 Subject: [PATCH 02/71] clean up unused code --- .../Source/UserSession/Search/SearchDirectory.swift | 13 ++++++------- .../Source/UserSession/SearchDirectoryTests.swift | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift index ef3654c8d00..4c5b02cfc8d 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 { - 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 @@ -104,7 +103,7 @@ public class SearchDirectory: NSObject { return task } - func observeSearchUsers(_ result: SearchResult) { + private func observeSearchUsers(_ result: SearchResult) { let searchUserObserverCenter = contextProvider.viewContext.searchUserObserverCenter result.directory.forEach(searchUserObserverCenter.addSearchUser) result.services.compactMap { $0 as? ZMSearchUser }.forEach(searchUserObserverCenter.addSearchUser) @@ -116,7 +115,7 @@ public class SearchDirectory: NSObject { } } -extension SearchDirectory: TearDownCapable { +extension SearchDirectory { /// Tear down the SearchDirectory. /// diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift index 764c210fff1..9d1bcef3683 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 From 5843473f30d4994010ce122885498f9b3b589a8c Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Wed, 28 Jan 2026 10:38:40 +0000 Subject: [PATCH 03/71] some renaming --- .../UserSession/Search/SearchDirectory.swift | 4 +-- .../UserSession/Search/SearchTask.swift | 34 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift index 4c5b02cfc8d..86bb23505e2 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift @@ -68,7 +68,7 @@ public final class SearchDirectory { /// Returns a SearchTask which should be retained until the results arrive. public func perform(_ request: SearchRequest) -> SearchTask { let task = SearchTask( - task: .search(searchRequest: request), + type: .search(searchRequest: request), contextProvider: contextProvider, transportSession: transportSession, searchUsersCache: searchUsersCache, @@ -89,7 +89,7 @@ public final class SearchDirectory { /// Returns a SearchTask which should be retained until the results arrive. public func lookup(qualifiedID: QualifiedID) -> SearchTask { let task = SearchTask( - task: .lookup(qualifiedID: qualifiedID), + type: .lookup(qualifiedID: qualifiedID), contextProvider: contextProvider, transportSession: transportSession, 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 2f03a32d59f..5dfefa9b259 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -19,9 +19,9 @@ 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) } @@ -33,7 +33,7 @@ public class SearchTask { private let contextProvider: ContextProvider private let searchUsersCache: SearchUsersCache? - private let task: Task + private let type: `Type` private var userLookupTaskIdentifier: ZMTaskIdentifier? private var directoryTaskIdentifier: ZMTaskIdentifier? private var teamMembershipTaskIdentifier: ZMTaskIdentifier? @@ -84,7 +84,7 @@ public class SearchTask { apiVersion: WireTransport.APIVersion? ) { self.init( - task: .search(searchRequest: request), + type: .search(searchRequest: request), contextProvider: contextProvider, transportSession: transportSession, searchUsersCache: searchUsersCache, @@ -100,7 +100,7 @@ public class SearchTask { apiVersion: WireTransport.APIVersion? ) { self.init( - task: .lookup(qualifiedID: qualifiedID), + type: .lookup(qualifiedID: qualifiedID), contextProvider: contextProvider, transportSession: transportSession, searchUsersCache: searchUsersCache, @@ -109,13 +109,13 @@ public class SearchTask { } public init( - task: Task, + 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 @@ -162,7 +162,7 @@ extension SearchTask { /// look up a user ID from contacts and teamMembers locally. private func performLocalLookup() { - guard case let .lookup(qualifiedID) = task else { return } + guard case let .lookup(qualifiedID) = type else { return } tasksRemaining += 1 @@ -216,7 +216,7 @@ extension SearchTask { } func performLocalSearch() { - guard case let .search(request) = task else { return } + guard case let .search(request) = type else { return } tasksRemaining += 1 @@ -376,14 +376,14 @@ extension SearchTask { func performUserLookup() { guard - case let .lookup(qualifiedID) = task, + case let .lookup(qualifiedID) = type, let apiVersion else { return } tasksRemaining += 1 contextProvider.searchContext.performGroupedBlock { [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 @@ -433,7 +433,7 @@ extension SearchTask { guard let apiVersion, apiVersion >= .v1, - case let .search(searchRequest) = task, + case let .search(searchRequest) = type, !searchRequest.searchOptions.contains(.localResultsOnly), !searchRequest.searchOptions.isDisjoint(with: [.directory, .teamMembers, .federated]) else { @@ -489,7 +489,7 @@ extension SearchTask { return } - let request = type(of: self).fetchTeamMembershipRequest( + let request = Self.fetchTeamMembershipRequest( teamID: teamID, teamMemberIDs: teamMembersIDs, apiVersion: apiVersion @@ -585,7 +585,7 @@ extension SearchTask { 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 } @@ -593,7 +593,7 @@ extension SearchTask { tasksRemaining += 1 contextProvider.searchContext.performGroupedBlock { [self] in - let request = type(of: self).searchRequestInDirectory( + let request = Self.searchRequestInDirectory( withHandle: searchRequest.query.string, apiVersion: apiVersion ) @@ -684,7 +684,7 @@ extension SearchTask { 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 } @@ -693,7 +693,7 @@ extension SearchTask { contextProvider.searchContext.performGroupedBlock { [self] in - let request = type(of: self).servicesSearchRequest( + let request = Self.servicesSearchRequest( teamIdentifier: teamIdentifier, query: searchRequest.query.string, apiVersion: apiVersion From 00eb258f5032e3271559f6d8568a425ae512521c Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Wed, 28 Jan 2026 11:01:06 +0000 Subject: [PATCH 04/71] prepare for refactoring --- .../UserSession/Search/SearchTask.swift | 64 +++++-------------- .../Source/UserSession/SearchTaskTests.swift | 7 +- 2 files changed, 20 insertions(+), 51 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 5dfefa9b259..02fdb31d8cb 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -26,7 +26,7 @@ public final class SearchTask { case lookup(qualifiedID: QualifiedID) } - public typealias ResultHandler = (_ result: SearchResult, _ isCompleted: Bool) -> Void + public typealias ResultHandler = (_ incrementalResult: SearchResult, _ isCompleted: Bool) -> Void private let apiVersion: WireTransport.APIVersion? private let transportSession: TransportSessionType @@ -76,38 +76,6 @@ public final class SearchTask { } } - convenience init( - request: SearchRequest, - contextProvider: ContextProvider, - transportSession: TransportSessionType, - searchUsersCache: SearchUsersCache?, - apiVersion: WireTransport.APIVersion? - ) { - self.init( - type: .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( - type: .lookup(qualifiedID: qualifiedID), - contextProvider: contextProvider, - transportSession: transportSession, - searchUsersCache: searchUsersCache, - apiVersion: apiVersion - ) - } - public init( type: `Type`, contextProvider: ContextProvider, @@ -215,7 +183,7 @@ extension SearchTask { } } - func performLocalSearch() { + /*private*/ func performLocalSearch() { // TODO: make private guard case let .search(request) = type else { return } tasksRemaining += 1 @@ -299,7 +267,7 @@ extension SearchTask { } } - func teamMembers(matchingQuery query: String, team: Team?, searchOptions: SearchOptions) -> [Member] { + private func teamMembers(matchingQuery query: String, team: Team?, searchOptions: SearchOptions) -> [Member] { var result = team?.members(matchingQuery: query) ?? [] if searchOptions.contains(.excludeNonActiveTeamMembers) { @@ -325,7 +293,7 @@ extension SearchTask { return result } - func connectedUsers(matchingQuery query: String, hostedOnDomain: String?) -> [ZMUser] { + private func connectedUsers(matchingQuery query: String, hostedOnDomain: String?) -> [ZMUser] { let fetchRequest: NSFetchRequest = if let hostedOnDomain { ZMUser.sortedFetchRequest(with: ZMUser.predicateForConnectedUsers( withSearch: query, @@ -338,7 +306,7 @@ extension SearchTask { return contextProvider.searchContext.fetchOrAssert(request: fetchRequest) as? [ZMUser] ?? [] } - func conversations(matchingQuery query: SearchRequest.Query, selfUser: ZMUser) -> [ZMConversation] { + private func conversations(matchingQuery query: SearchRequest.Query, selfUser: ZMUser) -> [ZMConversation] { // swiftlint:disable:next todo_requires_jira_link // TODO: use the interface with team param? let fetchRequest = ZMConversation.sortedFetchRequest(with: ZMConversation.predicate( @@ -374,7 +342,7 @@ extension SearchTask { extension SearchTask { - func performUserLookup() { + /*private*/ func performUserLookup() { // TODO: make private guard case let .lookup(qualifiedID) = type, let apiVersion @@ -416,7 +384,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( @@ -429,7 +397,7 @@ extension SearchTask { extension SearchTask { - func performRemoteSearch() { + /*private*/ func performRemoteSearch() { // TODO: make private guard let apiVersion, apiVersion >= .v1, @@ -477,7 +445,7 @@ extension SearchTask { } } - func performTeamMembershipLookup(on searchResult: SearchResult, searchRequest: SearchRequest) { + private func performTeamMembershipLookup(on searchResult: SearchResult, searchRequest: SearchRequest) { let teamMembersIDs = searchResult.teamMembers.compactMap(\.remoteIdentifier) guard @@ -524,7 +492,7 @@ extension SearchTask { transportSession.enqueueOneTime(request) } - func completeRemoteSearch(searchResult: SearchResult? = nil) { + private func completeRemoteSearch(searchResult: SearchResult? = nil) { defer { tasksRemaining -= 1 } @@ -534,7 +502,7 @@ extension SearchTask { } } - static func searchRequestInDirectory( + private static func searchRequestInDirectory( withRequest searchRequest: SearchRequest, fetchLimit: Int = 10, apiVersion: APIVersion @@ -556,7 +524,7 @@ extension SearchTask { return ZMTransportRequest(getFromPath: path, apiVersion: apiVersion.rawValue) } - static func fetchTeamMembershipRequest( + private static func fetchTeamMembershipRequest( teamID: UUID, teamMemberIDs: [UUID], apiVersion: APIVersion @@ -581,7 +549,7 @@ extension SearchTask { extension SearchTask { - func performRemoteSearchForTeamUser() { + /*private*/ func performRemoteSearchForTeamUser() { // TODO: make private guard let apiVersion, apiVersion <= .v1, @@ -660,7 +628,7 @@ extension SearchTask { } } - 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("@") { @@ -677,7 +645,7 @@ extension SearchTask { extension SearchTask { - func performRemoteSearchForServices() { + /*private*/ func performRemoteSearchForServices() { // TODO: make private let teamIdentifier = contextProvider.searchContext.performAndWait { ZMUser.selfUser(in: contextProvider.searchContext).team?.remoteIdentifier } @@ -731,7 +699,7 @@ extension SearchTask { } } - static func servicesSearchRequest( + /*private*/ static func servicesSearchRequest( // TODO: make private teamIdentifier: UUID, query: String, apiVersion: APIVersion diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift index e18330ccb84..1ee427e00b5 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift @@ -1418,7 +1418,7 @@ final class SearchTaskTests: DatabaseTest { apiVersion: APIVersion = .v0 ) -> SearchTask { SearchTask( - request: request, + type: .search(searchRequest: request), contextProvider: coreDataStack!, transportSession: mockTransportSession, searchUsersCache: mockCache, @@ -1431,8 +1431,9 @@ 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, From 96fc35afb764999d6c952350482d59ac985de082 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Wed, 28 Jan 2026 12:22:39 +0000 Subject: [PATCH 05/71] remove unused code --- .../Sources/WireNetwork/APIs/Rest/Search/SearchAPIV1.swift | 2 +- .../Sources/WireNetwork/APIs/Rest/Search/SearchAPIV15.swift | 2 +- .../Source/UserSession/Search/SearchTask.swift | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV1.swift b/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV1.swift index 3c85a21f005..ff4eea61bac 100644 --- a/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV1.swift +++ b/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV1.swift @@ -55,7 +55,7 @@ class SearchAPIV1: SearchAPIV0 { urlComponents.path = "\(pathPrefix)\(basePath)" urlComponents.queryItems = queryItems - guard let path = urlComponents.string?.replacingOccurrences(of: "+", with: "%2B") else { + guard let path = urlComponents.string else { throw SearchAPIError.invalidRequest } diff --git a/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV15.swift b/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV15.swift index 2a97b47b597..5e3c76ed321 100644 --- a/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV15.swift +++ b/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV15.swift @@ -51,7 +51,7 @@ final class SearchAPIV15: SearchAPIV14 { urlComponents.path = "\(pathPrefix)\(basePath)" urlComponents.queryItems = queryItems - guard let path = urlComponents.string?.replacingOccurrences(of: "+", with: "%2B") else { + guard let path = urlComponents.string else { // TODO: manually verify the change is correct throw SearchAPIError.invalidRequest } diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 02fdb31d8cb..78d27ec7990 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -520,7 +520,7 @@ extension SearchTask { url.path = "/search/contacts" url.queryItems = queryItems - let path = url.string?.replacingOccurrences(of: "+", with: "%2B") ?? "" + let path = url.string ?? "" return ZMTransportRequest(getFromPath: path, apiVersion: apiVersion.rawValue) } @@ -638,7 +638,7 @@ extension SearchTask { var url = URLComponents() url.path = "/users" url.queryItems = [URLQueryItem(name: "handles", value: handle)] - let urlStr = url.string?.replacingOccurrences(of: "+", with: "%2B") ?? "" + let urlStr = url.string ?? "" // TODO: manually verify it's correct return ZMTransportRequest(getFromPath: urlStr, apiVersion: apiVersion.rawValue) } } @@ -711,7 +711,7 @@ extension SearchTask { if !trimmedQuery.isEmpty { url.queryItems = [URLQueryItem(name: "prefix", value: trimmedQuery)] } - let urlStr = url.string?.replacingOccurrences(of: "+", with: "%2B") ?? "" + let urlStr = url.string ?? "" return ZMTransportRequest(getFromPath: urlStr, apiVersion: apiVersion.rawValue) } } From 4e4cf14c94e403f8b3f3a8cf362aa27dfca393fb Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Wed, 28 Jan 2026 14:14:51 +0000 Subject: [PATCH 06/71] start migrating SearchTask to Swift Concurrency --- .../Source/Use cases/SearchUsersUseCase.swift | 22 ++---- .../UserSession/Search/SearchDirectory.swift | 5 +- .../UserSession/Search/SearchResult.swift | 10 +++ .../UserSession/Search/SearchTask.swift | 72 ++++++++++++------- .../SearchUserViewController.swift | 26 ++++--- 5 files changed, 80 insertions(+), 55 deletions(-) diff --git a/wire-ios-sync-engine/Source/Use cases/SearchUsersUseCase.swift b/wire-ios-sync-engine/Source/Use cases/SearchUsersUseCase.swift index 6bfc9eb9eaf..9638e67cca3 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.createTask(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 86bb23505e2..13638a9f21f 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift @@ -63,10 +63,7 @@ public final class SearchDirectory { self.refreshConversationsMissingMetadataAction = refreshConversationsMissingMetadataAction } - /// Perform a search request. - /// - /// Returns a SearchTask which should be retained until the results arrive. - public func perform(_ request: SearchRequest) -> SearchTask { + public func createTask(with request: SearchRequest) -> SearchTask { let task = SearchTask( type: .search(searchRequest: request), contextProvider: contextProvider, diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchResult.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchResult.swift index cead556662c..949117cd3ab 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 = [] + searchUsersCache = nil + } + public init?( payload: [AnyHashable: Any], query: SearchRequest.Query, diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 78d27ec7990..26997e19d9d 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -26,6 +26,15 @@ public final class SearchTask { case lookup(qualifiedID: QualifiedID) } + enum Status { + case pending + case running + case cancelled + case completed + } + + private var status = Status.pending + public typealias ResultHandler = (_ incrementalResult: SearchResult, _ isCompleted: Bool) -> Void private let apiVersion: WireTransport.APIVersion? @@ -40,15 +49,7 @@ public final class SearchTask { private var handleTaskIdentifier: ZMTaskIdentifier? private var servicesTaskIdentifier: ZMTaskIdentifier? private var resultHandlers: [ResultHandler] = [] - private var result = SearchResult( - context: .init(concurrencyType: .privateQueueConcurrencyType), - contacts: [], - teamMembers: [], - directory: [], - conversations: [], - services: [], - searchUsersCache: nil - ) + private var result = SearchResult() private let tasksRemainingLock = NSRecursiveLock() private var _tasksRemaining = 0 @@ -76,7 +77,7 @@ public final class SearchTask { } } - public init( + init( type: `Type`, contextProvider: ContextProvider, transportSession: TransportSessionType, @@ -96,7 +97,11 @@ public final class SearchTask { /// Cancel a previously started task public func cancel() { - resultHandlers.removeAll() + guard status == .running else { return assertionFailure() } + + status = .cancelled + + resultHandlers.removeAll() // TODO: delete teamMembershipTaskIdentifier.map(transportSession.cancelTask) userLookupTaskIdentifier.map(transportSession.cancelTask) @@ -107,22 +112,41 @@ public final class SearchTask { tasksRemaining = 0 } - /// 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 status == .pending else { + assertionFailure() + return SearchResult() + } + + status = .running + defer { status = .completed } + + // 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. - // search People or groups - performLocalLookup() - performLocalSearch() + return await withCheckedContinuation { continuation in - // v1 - performRemoteSearchForTeamUser() + // search services + performRemoteSearchForServices() - // v2+ - performRemoteSearch() - performUserLookup() + // search People or groups + performLocalLookup() + performLocalSearch() + + // v1 + performRemoteSearchForTeamUser() + + // v2+ + performRemoteSearch() + performUserLookup() + + addResultHandler { incrementalResult, isCompleted in + if isCompleted { + continuation.resume(returning: SearchResult()) // TODO: fix, aggregate results! + } + } + + } } } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/SearchUserViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/SearchUserViewController.swift index 7ba88339e99..17ea2be16af 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,6 +99,19 @@ final class SearchUserViewController: UIViewController { // MARK: - Methods + private func startLookup() { + guard let searchDirectory else { return } + + Task { + let task = searchDirectory.lookup(qualifiedID: qualifiedID) + pendingSearchTask = task + let searchResult = await task.start() + pendingSearchTask = nil + activityIndicator.stop() + handleSearchResult(searchResult: searchResult, isCompleted: true) // TODO: remove isCompleted? + } + } + private func handleSearchResult(searchResult: SearchResult, isCompleted: Bool) { guard !resultHandled, isCompleted else { return } guard let selfUser = ZMUser.selfUser() else { @@ -137,7 +141,7 @@ final class SearchUserViewController: UIViewController { navigationController?.setViewControllers([profileViewController], animated: true) resultHandled = true - } else if isCompleted { + } else if isCompleted { // TODO: ?? let alert = UIAlertController( title: L10n.Localizable.UrlAction.InvalidUser.title, message: L10n.Localizable.UrlAction.InvalidUser.message, From 93c66875a6098ad434b1ef9dc0bb1a33f31c1b9d Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Wed, 28 Jan 2026 14:29:15 +0000 Subject: [PATCH 07/71] don't send empty search requests --- .../Source/UserSession/Search/SearchRequest.swift | 5 ++++- .../Source/UserSession/Search/SearchTask.swift | 1 + .../UserProfile/SearchUserViewController.swift | 8 ++++---- 3 files changed, 9 insertions(+), 5 deletions(-) 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/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 26997e19d9d..54e02ee2550 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -426,6 +426,7 @@ extension SearchTask { let apiVersion, apiVersion >= .v1, 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 { diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/SearchUserViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/SearchUserViewController.swift index 17ea2be16af..f39c64b3e3a 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/SearchUserViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/SearchUserViewController.swift @@ -108,12 +108,12 @@ final class SearchUserViewController: UIViewController { let searchResult = await task.start() pendingSearchTask = nil activityIndicator.stop() - handleSearchResult(searchResult: searchResult, isCompleted: true) // TODO: remove isCompleted? + handleSearchResult(searchResult: searchResult) // TODO: remove isCompleted? } } - private func handleSearchResult(searchResult: SearchResult, isCompleted: Bool) { - guard !resultHandled, isCompleted else { return } + private func handleSearchResult(searchResult: SearchResult) { + guard !resultHandled else { return } guard let selfUser = ZMUser.selfUser() else { assertionFailure("ZMUser.selfUser() is nil") return @@ -141,7 +141,7 @@ final class SearchUserViewController: UIViewController { navigationController?.setViewControllers([profileViewController], animated: true) resultHandled = true - } else if isCompleted { // TODO: ?? + } else { let alert = UIAlertController( title: L10n.Localizable.UrlAction.InvalidUser.title, message: L10n.Localizable.UrlAction.InvalidUser.message, From 2f72db4b0510790f7a924fcacb42909d6d727a5e Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Wed, 28 Jan 2026 14:48:26 +0000 Subject: [PATCH 08/71] some more cleanup --- .../Source/Use cases/SearchUsersUseCase.swift | 2 +- .../Source/UserSession/Search/SearchDirectory.swift | 6 ++---- .../Source/UserSession/Search/SearchTask.swift | 2 +- .../StartUI/StartUI/SearchResultsViewController.swift | 10 +++------- .../StartUI/StartUI/StartUIViewController.swift | 2 +- .../UserProfile/SearchUserViewController.swift | 4 ++-- 6 files changed, 10 insertions(+), 16 deletions(-) diff --git a/wire-ios-sync-engine/Source/Use cases/SearchUsersUseCase.swift b/wire-ios-sync-engine/Source/Use cases/SearchUsersUseCase.swift index 9638e67cca3..c6076f033b2 100644 --- a/wire-ios-sync-engine/Source/Use cases/SearchUsersUseCase.swift +++ b/wire-ios-sync-engine/Source/Use cases/SearchUsersUseCase.swift @@ -72,7 +72,7 @@ public final class SearchUsersUseCase: SearchUsersUseCaseProtocol { team: team ) - let task = searchDirectory.createTask(with: request) + let task = searchDirectory.createSearchTask(with: request) activeSearchTask = task defer { if activeSearchTask === task { diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift index 13638a9f21f..9258d223b94 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift @@ -63,7 +63,7 @@ public final class SearchDirectory { self.refreshConversationsMissingMetadataAction = refreshConversationsMissingMetadataAction } - public func createTask(with request: SearchRequest) -> SearchTask { + public func createSearchTask(with request: SearchRequest) -> SearchTask { let task = SearchTask( type: .search(searchRequest: request), contextProvider: contextProvider, @@ -82,9 +82,7 @@ public final class SearchDirectory { /// 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 { + public func createLookupTask(with qualifiedID: QualifiedID) -> SearchTask { let task = SearchTask( type: .lookup(qualifiedID: qualifiedID), contextProvider: contextProvider, diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 54e02ee2550..a1b63ecbbd4 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -91,7 +91,7 @@ public final class SearchTask { self.apiVersion = apiVersion } - public func addResultHandler(_ resultHandler: @escaping ResultHandler) { + /*private*/ func addResultHandler(_ resultHandler: @escaping ResultHandler) { // TODO: make private resultHandlers.append(resultHandler) } 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 f39c64b3e3a..ce5d387cb42 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/SearchUserViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/SearchUserViewController.swift @@ -103,12 +103,12 @@ final class SearchUserViewController: UIViewController { guard let searchDirectory else { return } Task { - let task = searchDirectory.lookup(qualifiedID: qualifiedID) + let task = searchDirectory.createLookupTask(with: qualifiedID) pendingSearchTask = task let searchResult = await task.start() pendingSearchTask = nil activityIndicator.stop() - handleSearchResult(searchResult: searchResult) // TODO: remove isCompleted? + handleSearchResult(searchResult: searchResult) } } From f7a594f01f9aa970a368f39214cff9492b2f5b9d Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Wed, 28 Jan 2026 16:32:19 +0000 Subject: [PATCH 09/71] add note --- .../Source/Notifications/SearchUserObserverCenter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wire-ios-data-model/Source/Notifications/SearchUserObserverCenter.swift b/wire-ios-data-model/Source/Notifications/SearchUserObserverCenter.swift index 32e0177c5e7..fdce0a5eb7c 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 { // TODO: what is it needed for? /// Keys that we want to be notified for static let observableKeys: [String] = [ From 93c2a547927b6528089636590d57d406644a17bf Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Thu, 29 Jan 2026 08:43:27 +0000 Subject: [PATCH 10/71] refactoring --- .../UserSession/Search/SearchDirectory.swift | 22 ++---------------- .../UserSession/Search/SearchTask.swift | 23 +++++++++++-------- 2 files changed, 16 insertions(+), 29 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift index 9258d223b94..f6e157a5556 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift @@ -64,44 +64,26 @@ public final class SearchDirectory { } public func createSearchTask(with request: SearchRequest) -> SearchTask { - let task = 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. public func createLookupTask(with qualifiedID: QualifiedID) -> SearchTask { - let task = 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 - } - - private 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/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index a1b63ecbbd4..d424387851b 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -91,7 +91,7 @@ public final class SearchTask { self.apiVersion = apiVersion } - /*private*/ func addResultHandler(_ resultHandler: @escaping ResultHandler) { // TODO: make private + private func addResultHandler(_ resultHandler: @escaping ResultHandler) { resultHandlers.append(resultHandler) } @@ -122,10 +122,21 @@ public final class SearchTask { status = .running defer { status = .completed } - // 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. - return await withCheckedContinuation { continuation in + addResultHandler { result, isCompleted in + + // 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) + + if isCompleted { + continuation.resume(returning: self.result) + } + + } + // search services performRemoteSearchForServices() @@ -140,12 +151,6 @@ public final class SearchTask { performRemoteSearch() performUserLookup() - addResultHandler { incrementalResult, isCompleted in - if isCompleted { - continuation.resume(returning: SearchResult()) // TODO: fix, aggregate results! - } - } - } } } From f5b2e8709dcb506077dc1d1636f72ad2c15c5aeb Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Thu, 29 Jan 2026 09:27:13 +0000 Subject: [PATCH 11/71] minor fix --- .../Source/UserSession/Search/SearchTask.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index d424387851b..1fbe5dafde9 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -101,15 +101,13 @@ public final class SearchTask { status = .cancelled - resultHandlers.removeAll() // TODO: delete - teamMembershipTaskIdentifier.map(transportSession.cancelTask) userLookupTaskIdentifier.map(transportSession.cancelTask) directoryTaskIdentifier.map(transportSession.cancelTask) servicesTaskIdentifier.map(transportSession.cancelTask) handleTaskIdentifier.map(transportSession.cancelTask) - tasksRemaining = 0 + // tasksRemaining is supposed to reach 0 eventually } /// Start the search task. Errors will not be thrown. From 6ac778fdcb532f0558406c4fcc61f4aaeca2d946 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Fri, 30 Jan 2026 11:00:24 +0100 Subject: [PATCH 12/71] note --- .../Source/UserSession/Search/SearchTask.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 1fbe5dafde9..34064bfbbd9 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -107,7 +107,7 @@ public final class SearchTask { servicesTaskIdentifier.map(transportSession.cancelTask) handleTaskIdentifier.map(transportSession.cancelTask) - // tasksRemaining is supposed to reach 0 eventually + // tasksRemaining is supposed to reach 0 eventually and trigger the continuation of `start`. } /// Start the search task. Errors will not be thrown. @@ -135,6 +135,8 @@ public final class SearchTask { } + manually test + // search services performRemoteSearchForServices() From a287b76e90c2e099d28c4c21014e9ec3b80c7711 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Fri, 30 Jan 2026 15:03:28 +0100 Subject: [PATCH 13/71] prepare for isolating search subtasks --- .../UserSession/Search/SearchTask.swift | 57 ++++++++++++------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 34064bfbbd9..4597af82384 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -120,37 +120,52 @@ public final class SearchTask { status = .running defer { status = .completed } - return await withCheckedContinuation { continuation in + /// Updates the passed search result. + typealias Aggregator = (inout SearchResult) -> Void + return await withTaskGroup(of: Aggregator.self, returning: SearchResult.self) { taskGroup in - addResultHandler { result, isCompleted in + taskGroup.addTask { + { $0 = 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) + taskGroup.addTask { + await withCheckedContinuation { continuation in - if isCompleted { - continuation.resume(returning: self.result) - } + self.addResultHandler { result, isCompleted in - } + // 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) - manually test + if isCompleted { + continuation.resume(returning: { _ in result }) + } + + } - // search services - performRemoteSearchForServices() + // search services + self.performRemoteSearchForServices() // TODO: test manually - // search People or groups - performLocalLookup() - performLocalSearch() + // search People or groups + self.performLocalLookup() // TODO: test manually + self.performLocalSearch() // TODO: test manually - // v1 - performRemoteSearchForTeamUser() + // v1 + self.performRemoteSearchForTeamUser() // TODO: test manually - // v2+ - performRemoteSearch() - performUserLookup() + // v2+ + self.performRemoteSearch() // TODO: test manually + self.performUserLookup() // TODO: test manually + } + } + + while let aggregator = await taskGroup.next() { + aggregator(&self.result) + } + + return SearchResult() } } } From 1e0cb6f236de58157849c4fff2d372cf3418f3e3 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Fri, 30 Jan 2026 15:23:36 +0100 Subject: [PATCH 14/71] migrate performRemoteSearchForServices --- .../UserSession/Search/SearchTask.swift | 56 +++++++++---------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 4597af82384..fe4a74b71b0 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -37,6 +37,14 @@ public final class SearchTask { public typealias ResultHandler = (_ incrementalResult: SearchResult, _ isCompleted: Bool) -> Void + /// 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 three different ways of aggregating search results: + /// - union(withLocalResult:) + /// - union(withServiceResult:) + /// - union(withDirectoryResult:) + /*private*/ typealias SearchResultAggregator = (inout SearchResult) -> Void // TODO: make private + private let apiVersion: WireTransport.APIVersion? private let transportSession: TransportSessionType private let contextProvider: ContextProvider @@ -47,12 +55,12 @@ public final class SearchTask { private var directoryTaskIdentifier: ZMTaskIdentifier? private var teamMembershipTaskIdentifier: ZMTaskIdentifier? private var handleTaskIdentifier: ZMTaskIdentifier? - private var servicesTaskIdentifier: ZMTaskIdentifier? private var resultHandlers: [ResultHandler] = [] private var result = SearchResult() private let tasksRemainingLock = NSRecursiveLock() private var _tasksRemaining = 0 + @available(*, deprecated, message: "to be deleted!") private var tasksRemaining: Int { get { tasksRemainingLock.withLock { @@ -104,14 +112,13 @@ public final class SearchTask { teamMembershipTaskIdentifier.map(transportSession.cancelTask) userLookupTaskIdentifier.map(transportSession.cancelTask) directoryTaskIdentifier.map(transportSession.cancelTask) - servicesTaskIdentifier.map(transportSession.cancelTask) handleTaskIdentifier.map(transportSession.cancelTask) // tasksRemaining is supposed to reach 0 eventually and trigger the continuation of `start`. } /// Start the search task. Errors will not be thrown. - public func start() async -> SearchResult { + public func start() async -> SearchResult { // TODO: test manually with two clients, develop and this code guard status == .pending else { assertionFailure() return SearchResult() @@ -120,12 +127,11 @@ public final class SearchTask { status = .running defer { status = .completed } - /// Updates the passed search result. - typealias Aggregator = (inout SearchResult) -> Void - return await withTaskGroup(of: Aggregator.self, returning: SearchResult.self) { taskGroup in + return await withTaskGroup(of: SearchResultAggregator.self, returning: SearchResult.self) { taskGroup in + // search services taskGroup.addTask { - { $0 = SearchResult() } + await self.performRemoteSearchForServices() } taskGroup.addTask { @@ -139,14 +145,11 @@ public final class SearchTask { result.services.compactMap { $0 as? ZMSearchUser }.forEach(searchUserObserverCenter.addSearchUser) if isCompleted { - continuation.resume(returning: { _ in result }) + continuation.resume(returning: { _ in }) } } - // search services - self.performRemoteSearchForServices() // TODO: test manually - // search People or groups self.performLocalLookup() // TODO: test manually self.performLocalSearch() // TODO: test manually @@ -690,21 +693,22 @@ extension SearchTask { extension SearchTask { - /*private*/ func performRemoteSearchForServices() { // TODO: make private - let teamIdentifier = contextProvider.searchContext.performAndWait { - ZMUser.selfUser(in: contextProvider.searchContext).team?.remoteIdentifier + /*private*/ func performRemoteSearchForServices() async -> SearchResultAggregator { // TODO: create subtask struct + + let searchContext = contextProvider.searchContext + let teamIdentifier = await searchContext.perform { + ZMUser.selfUser(in: searchContext).team?.remoteIdentifier } + guard let apiVersion, let teamIdentifier, case let .search(searchRequest) = type, !searchRequest.searchOptions.contains(.localResultsOnly), searchRequest.searchOptions.contains(.services) - else { return } + else { return { _ in } } - tasksRemaining += 1 - - contextProvider.searchContext.performGroupedBlock { [self] in + return await withCheckedContinuation { continuation in let request = Self.servicesSearchRequest( teamIdentifier: teamIdentifier, @@ -714,10 +718,6 @@ extension SearchTask { request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in - defer { - self?.tasksRemaining -= 1 - } - guard let contextProvider = self?.contextProvider, let payload = response.payload?.asDictionary(), @@ -727,20 +727,14 @@ extension SearchTask { contextProvider: contextProvider, searchUsersCache: self?.searchUsersCache ) - else { - return - } + else { return continuation.resume(returning: { _ in }) } - if let updatedResult = self?.result.union(withServiceResult: result) { - self?.result = updatedResult - } - }) + continuation.resume { $0 = $0.union(withServiceResult: result) } - request.add(ZMTaskCreatedHandler(on: contextProvider.searchContext) { [weak self] taskIdentifier in - self?.servicesTaskIdentifier = taskIdentifier }) transportSession.enqueueOneTime(request) + } } From 7784c55aee4c7a315e2f07f2ea7b1817324863d5 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Fri, 30 Jan 2026 17:05:20 +0100 Subject: [PATCH 15/71] migrate performLocalLookup --- .../UserSession/Search/SearchTask.swift | 116 ++++++++++-------- 1 file changed, 68 insertions(+), 48 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index fe4a74b71b0..e925d287d85 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -16,6 +16,7 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // +import CoreData import Foundation import WireUtilities @@ -134,6 +135,15 @@ public final class SearchTask { await self.performRemoteSearchForServices() } + // search People or groups + taskGroup.addTask { + await self.performLocalLookup() + } + taskGroup.addTask { + // await self.performLocalSearch() // TODO: fix + fatalError() + } + taskGroup.addTask { await withCheckedContinuation { continuation in @@ -150,10 +160,6 @@ public final class SearchTask { } - // search People or groups - self.performLocalLookup() // TODO: test manually - self.performLocalSearch() // TODO: test manually - // v1 self.performRemoteSearchForTeamUser() // TODO: test manually @@ -175,67 +181,76 @@ public final class SearchTask { extension SearchTask { - /// look up a user ID from contacts and teamMembers locally. - private func performLocalLookup() { - guard case let .lookup(qualifiedID) = type else { return } + /// Look up a user ID from contacts and teamMembers locally. + private func performLocalLookup() async -> SearchResultAggregator { // TODO: create subtask struct - tasksRemaining += 1 + guard case let .lookup(qualifiedID) = type else { + return { _ in } + } - contextProvider.searchContext.performGroupedBlock { [self] in - let selfUser = ZMUser.selfUser(in: contextProvider.searchContext) + let searchContext = contextProvider.newBackgroundContext() + let (teamMemberIDs, connectedUserIDs) = await searchContext.perform { - var options = SearchOptions() + 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(matchingQuery: "", team: selfUser.team, searchOptions: options) - let teamMembers = activeMembers.filter { $0.remoteIdentifier == qualifiedID.uuid } - let connectedUsers = connectedUsers(matchingQuery: "", hostedOnDomain: nil) + let activeMembers = self.teamMembers(matchingQuery: "", team: selfUser.team, searchOptions: options) + 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)) - contextProvider.viewContext.performGroupedBlock { [self] in - - let copiedTeamMembers = teamMembers.compactMap(\.user) - .compactMap { contextProvider.viewContext.object(with: $0.objectID) as? Member } - let copiedConnectedUsers = connectedUsers - .compactMap { contextProvider.viewContext.object(with: $0.objectID) as? ZMUser } + } - let result = SearchResult( - context: contextProvider.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 + ) - self.result = self.result.union(withLocalResult: result.copy(on: contextProvider.viewContext)) + return { $0 = $0.union(withLocalResult: result.copy(on: viewContext)) } - tasksRemaining -= 1 - } } + } /*private*/ func performLocalSearch() { // TODO: make private guard case let .search(request) = type else { return } + let searchContext = contextProvider.searchContext + tasksRemaining += 1 - contextProvider.searchContext.performGroupedBlock { [self] in + searchContext.performGroupedBlock { [self] in var team: Team? if let teamObjectID = request.team?.objectID { @@ -246,7 +261,8 @@ extension SearchTask { let connectedUsers = request.searchOptions .contains(.contacts) ? connectedUsers( matchingQuery: request.normalizedQuery, - hostedOnDomain: request.searchDomain + hostedOnDomain: request.searchDomain, + in: searchContext ) : [] let teamMembers = request.searchOptions.contains(.teamMembers) ? teamMembers( matchingQuery: request.normalizedQuery, @@ -340,7 +356,11 @@ extension SearchTask { return result } - private func connectedUsers(matchingQuery query: String, hostedOnDomain: String?) -> [ZMUser] { + private func connectedUsers( + matchingQuery query: String, + hostedOnDomain: String?, + in context: NSManagedObjectContext + ) -> [ZMUser] { let fetchRequest: NSFetchRequest = if let hostedOnDomain { ZMUser.sortedFetchRequest(with: ZMUser.predicateForConnectedUsers( withSearch: query, @@ -350,7 +370,7 @@ extension SearchTask { ZMUser.sortedFetchRequest(with: ZMUser.predicateForConnectedUsers(withSearch: query)) } - return contextProvider.searchContext.fetchOrAssert(request: fetchRequest) as? [ZMUser] ?? [] + return context.fetchOrAssert(request: fetchRequest) as? [ZMUser] ?? [] } private func conversations(matchingQuery query: SearchRequest.Query, selfUser: ZMUser) -> [ZMConversation] { From 23c3923a502921532598ec92bf068be58c1848ed Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Fri, 30 Jan 2026 17:17:45 +0100 Subject: [PATCH 16/71] migrate performLocalSearch --- .../UserSession/Search/SearchTask.swift | 97 ++++++++++--------- 1 file changed, 51 insertions(+), 46 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index e925d287d85..4d5b36b2c8f 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -140,8 +140,7 @@ public final class SearchTask { await self.performLocalLookup() } taskGroup.addTask { - // await self.performLocalSearch() // TODO: fix - fatalError() + await self.performLocalSearch() } taskGroup.addTask { @@ -243,21 +242,20 @@ extension SearchTask { } - /*private*/ func performLocalSearch() { // TODO: make private - guard case let .search(request) = type else { return } + /*private*/ func performLocalSearch() async -> SearchResultAggregator { // TODO: make private + guard case let .search(request) = type else { + return { _ in } + } let searchContext = contextProvider.searchContext - - tasksRemaining += 1 - - searchContext.performGroupedBlock { [self] in + let (connectedUserIDs, teamMemberIDs, conversationIDs) = await searchContext.perform { [self] in var team: Team? if let teamObjectID = request.team?.objectID { - team = (try? contextProvider.searchContext.existingObject(with: teamObjectID)) as? Team + team = (try? searchContext.existingObject(with: teamObjectID)) as? Team } - let selfUser = ZMUser.selfUser(in: contextProvider.searchContext) + let selfUser = ZMUser.selfUser(in: searchContext) let connectedUsers = request.searchOptions .contains(.contacts) ? connectedUsers( matchingQuery: request.normalizedQuery, @@ -275,47 +273,54 @@ extension SearchTask { selfUser: selfUser ) : [] - contextProvider.viewContext.performGroupedBlock { [self] in - - let copiedConnectedUsers = connectedUsers - .compactMap { contextProvider.viewContext.object(with: $0.objectID) as? ZMUser } - let searchConnectedUsers = copiedConnectedUsers - .map { - ZMSearchUser( - contextProvider: contextProvider, - user: $0, - searchUsersCache: searchUsersCache - ) - } - .filter { $0.name?.isEmpty == false } + return ( + connectedUsers.map(\.objectID), + teamMembers.map(\.objectID), + conversations.map(\.objectID) + ) - let copiedteamMembers = teamMembers.compactMap { - contextProvider.viewContext.object(with: $0.objectID) as? Member - } - let searchTeamMembers = copiedteamMembers - .compactMap(\.user) - .map { - ZMSearchUser( - contextProvider: contextProvider, - user: $0, - searchUsersCache: searchUsersCache - ) - } + } - let result = SearchResult( - context: contextProvider.viewContext, - contacts: searchConnectedUsers, - teamMembers: searchTeamMembers, - directory: [], - conversations: conversations, - services: [], - searchUsersCache: searchUsersCache - ) + let viewContext = contextProvider.viewContext + return await viewContext.perform { [self] in - self.result = self.result.union(withLocalResult: result.copy(on: contextProvider.viewContext)) + let copiedConnectedUsers = connectedUserIDs + .compactMap { viewContext.object(with: $0) as? ZMUser } + let searchConnectedUsers = copiedConnectedUsers + .map { + ZMSearchUser( + contextProvider: contextProvider, + user: $0, + searchUsersCache: searchUsersCache + ) + } + .filter { $0.name?.isEmpty == false } - tasksRemaining -= 1 + let copiedteamMembers = teamMemberIDs.compactMap { + contextProvider.viewContext.object(with: $0) as? Member } + let searchTeamMembers = copiedteamMembers + .compactMap(\.user) + .map { + ZMSearchUser( + contextProvider: contextProvider, + user: $0, + searchUsersCache: searchUsersCache + ) + } + + 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)) } + } } From b5903601b30c73a5955231483601d01d4be5ea8f Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Fri, 30 Jan 2026 17:25:42 +0100 Subject: [PATCH 17/71] migrate performRemoteSearchForTeamUser --- .../UserSession/Search/SearchTask.swift | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 4d5b36b2c8f..8a4867fca63 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -143,7 +143,19 @@ public final class SearchTask { await self.performLocalSearch() } + // v1 taskGroup.addTask { + await self.performRemoteSearchForTeamUser() + } + + // v2+ + taskGroup.addTask { +// self.performRemoteSearch() +// self.performUserLookup() + fatalError() // TODO: fix + } + + taskGroup.addTask { // TODO: delete await withCheckedContinuation { continuation in self.addResultHandler { result, isCompleted in @@ -159,13 +171,6 @@ public final class SearchTask { } - // v1 - self.performRemoteSearchForTeamUser() // TODO: test manually - - // v2+ - self.performRemoteSearch() // TODO: test manually - self.performUserLookup() // TODO: test manually - } } @@ -622,18 +627,17 @@ extension SearchTask { extension SearchTask { - /*private*/ func performRemoteSearchForTeamUser() { // TODO: make private + /*private*/ func performRemoteSearchForTeamUser() async -> SearchResultAggregator { // TODO: make private guard let apiVersion, apiVersion <= .v1, case let .search(searchRequest) = type, !searchRequest.searchOptions.contains(.localResultsOnly), searchRequest.searchOptions.contains(.directory) - else { return } + else { return { _ in } } - tasksRemaining += 1 + return await withCheckedContinuation { continuation in - contextProvider.searchContext.performGroupedBlock { [self] in let request = Self.searchRequestInDirectory( withHandle: searchRequest.query.string, apiVersion: apiVersion @@ -641,16 +645,12 @@ extension SearchTask { request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in - defer { - self?.tasksRemaining -= 1 - } - guard let contextProvider = self?.contextProvider, let payload = response.payload?.asArray(), let userPayload = (payload.first as? ZMTransportData)?.asDictionary() else { - return + return continuation.resume(returning: { _ in }) } guard @@ -658,7 +658,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] @@ -670,14 +670,14 @@ extension SearchTask { contextProvider: contextProvider, searchUsersCache: self?.searchUsersCache ) else { - return + return continuation.resume(returning: { _ in }) } if let user = result.directory.first, !user.isSelfUser { if let prevResult = self?.result { // prepend result to prevResult only if it doesn't contain it if !prevResult.directory.contains(user) { - self?.result = SearchResult( + let result = SearchResult( context: prevResult.context, contacts: prevResult.contacts, teamMembers: prevResult.teamMembers, @@ -686,22 +686,26 @@ extension SearchTask { services: prevResult.services, searchUsersCache: self?.searchUsersCache ) + continuation.resume(returning: { $0 = result }) + } else { + continuation.resume(returning: { _ in }) } } else { - self?.result = result + continuation.resume(returning: { $0 = result }) } + } else { + continuation.resume(returning: { _ in }) } }) - request.add(ZMTaskCreatedHandler(on: contextProvider.searchContext) { [weak self] taskIdentifier in - self?.handleTaskIdentifier = taskIdentifier - }) - transportSession.enqueueOneTime(request) } } - private 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("@") { From 14627e35ac82a32db817590f3b7fa1e61afb1f64 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Fri, 30 Jan 2026 17:46:17 +0100 Subject: [PATCH 18/71] migrate performTeamMembershipLookup --- .../UserSession/Search/SearchTask.swift | 86 +++++++++---------- 1 file changed, 41 insertions(+), 45 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 8a4867fca63..c8f251aa72a 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -150,7 +150,9 @@ public final class SearchTask { // v2+ taskGroup.addTask { -// self.performRemoteSearch() + await self.performRemoteSearch() + } + taskGroup.addTask { // self.performUserLookup() fatalError() // TODO: fix } @@ -474,7 +476,7 @@ extension SearchTask { extension SearchTask { - /*private*/ func performRemoteSearch() { // TODO: make private + /*private*/ func performRemoteSearch() async -> SearchResultAggregator { // TODO: make private guard let apiVersion, apiVersion >= .v1, @@ -483,12 +485,11 @@ extension SearchTask { !searchRequest.searchOptions.contains(.localResultsOnly), !searchRequest.searchOptions.isDisjoint(with: [.directory, .teamMembers, .federated]) else { - return + return { _ in } } - tasksRemaining += 1 + return await withCheckedContinuation { continuation in - contextProvider.searchContext.performGroupedBlock { [self] in let request = Self.searchRequestInDirectory(withRequest: searchRequest, apiVersion: apiVersion) request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in @@ -504,26 +505,34 @@ extension SearchTask { searchUsersCache: self?.searchUsersCache ) else { - self?.completeRemoteSearch() - return + return continuation.resume(returning: { _ in }) } if searchRequest.searchOptions.contains(.teamMembers) { - self?.performTeamMembershipLookup(on: result, searchRequest: searchRequest) + Task { + if let aggregator = await self?.performTeamMembershipLookup( + on: result, + searchRequest: searchRequest + ) { + continuation.resume(returning: aggregator) + } else { + continuation.resume(returning: { _ in }) + } + } } else { - self?.completeRemoteSearch(searchResult: result) + continuation.resume(returning: { $0 = $0.union(withDirectoryResult: result) }) } }) - request.add(ZMTaskCreatedHandler(on: contextProvider.searchContext) { [weak self] taskIdentifier in - self?.directoryTaskIdentifier = taskIdentifier - }) - transportSession.enqueueOneTime(request) } } - private func performTeamMembershipLookup(on searchResult: SearchResult, searchRequest: SearchRequest) { + private func performTeamMembershipLookup( + on searchResult: SearchResult, + searchRequest: SearchRequest + ) async -> SearchResultAggregator { + let teamMembersIDs = searchResult.teamMembers.compactMap(\.remoteIdentifier) guard @@ -531,8 +540,7 @@ extension SearchTask { let teamID = ZMUser.selfUser(in: contextProvider.viewContext).team?.remoteIdentifier, !teamMembersIDs.isEmpty else { - completeRemoteSearch(searchResult: searchResult) - return + return { $0 = $0.union(withDirectoryResult: searchResult) } } let request = Self.fetchTeamMembershipRequest( @@ -541,43 +549,31 @@ extension SearchTask { apiVersion: apiVersion ) - request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in - guard - let contextProvider = self?.contextProvider, - let rawData = response.rawData, - let payload = MembershipListPayload(rawData) - else { - self?.completeRemoteSearch() - return - } + return await withCheckedContinuation { continuation in - var updatedResult = searchResult - updatedResult.extendWithMembershipPayload(payload: payload) - updatedResult.filterBy( - searchOptions: searchRequest.searchOptions, - query: searchRequest.query.string, - contextProvider: contextProvider - ) + request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in + guard + let contextProvider = self?.contextProvider, + let rawData = response.rawData, + let payload = MembershipListPayload(rawData) + else { return continuation.resume(returning: { _ in }) } - self?.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: searchResult) }) - request.add(ZMTaskCreatedHandler(on: contextProvider.searchContext) { [weak self] taskIdentifier in - self?.teamMembershipTaskIdentifier = taskIdentifier - }) + }) - transportSession.enqueueOneTime(request) - } + transportSession.enqueueOneTime(request) - private func completeRemoteSearch(searchResult: SearchResult? = nil) { - defer { - tasksRemaining -= 1 } - if let searchResult { - result = result.union(withDirectoryResult: searchResult) - } } private static func searchRequestInDirectory( From 1790d7b8cdb5a856044dda2ed1e020221f9fc4cd Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Fri, 30 Jan 2026 17:48:20 +0100 Subject: [PATCH 19/71] migrate performUserLookup --- .../UserSession/Search/SearchTask.swift | 23 +++++++------------ 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index c8f251aa72a..d80859f8951 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -153,8 +153,7 @@ public final class SearchTask { await self.performRemoteSearch() } taskGroup.addTask { -// self.performUserLookup() - fatalError() // TODO: fix + await self.performUserLookup() } taskGroup.addTask { // TODO: delete @@ -421,20 +420,16 @@ extension SearchTask { extension SearchTask { - /*private*/ func performUserLookup() { // TODO: make private + /*private*/ func performUserLookup() async -> SearchResultAggregator { // TODO: make private guard case let .lookup(qualifiedID) = type, let apiVersion - else { return } + else { return { _ in } } - tasksRemaining += 1 + return await withCheckedContinuation { continuation in - contextProvider.searchContext.performGroupedBlock { [self] in let request = Self.searchRequestForUser(qualifiedID: qualifiedID, apiVersion: apiVersion) request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in - defer { - self?.tasksRemaining -= 1 - } guard let contextProvider = self?.contextProvider, @@ -444,17 +439,15 @@ extension SearchTask { contextProvider: contextProvider, searchUsersCache: self?.searchUsersCache ) - else { return } + else { return continuation.resume(returning: { _ in }) } if let updatedResult = self?.result.union(withDirectoryResult: result) { - self?.result = updatedResult + continuation.resume(returning: { $0 = $0.union(withDirectoryResult: result) }) + } else { + continuation.resume(returning: { _ in }) } }) - request.add(ZMTaskCreatedHandler(on: contextProvider.searchContext) { [weak self] taskIdentifier in - self?.userLookupTaskIdentifier = taskIdentifier - }) - transportSession.enqueueOneTime(request) } From a381efdeb01ed3d2a8f19eb50780fa71591db1b2 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Fri, 30 Jan 2026 17:49:30 +0100 Subject: [PATCH 20/71] minor fix --- .../Source/UserSession/Search/SearchTask.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index d80859f8951..8cac155effc 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -441,11 +441,7 @@ extension SearchTask { ) else { return continuation.resume(returning: { _ in }) } - if let updatedResult = self?.result.union(withDirectoryResult: result) { - continuation.resume(returning: { $0 = $0.union(withDirectoryResult: result) }) - } else { - continuation.resume(returning: { _ in }) - } + continuation.resume(returning: { $0 = $0.union(withDirectoryResult: result) }) }) transportSession.enqueueOneTime(request) From b80176469fff96ddb0824e0b603a51baeaf7979e Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Fri, 30 Jan 2026 17:51:21 +0100 Subject: [PATCH 21/71] some cleanup --- .../UserSession/Search/SearchTask.swift | 57 ++----------------- 1 file changed, 6 insertions(+), 51 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 8cac155effc..1002077b747 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -59,33 +59,6 @@ public final class SearchTask { private var resultHandlers: [ResultHandler] = [] private var result = SearchResult() - private let tasksRemainingLock = NSRecursiveLock() - private var _tasksRemaining = 0 - @available(*, deprecated, message: "to be deleted!") - 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(result, isCompleted) } - - if isCompleted { - resultHandlers.removeAll() - } - } - } - } - init( type: `Type`, contextProvider: ContextProvider, @@ -100,10 +73,6 @@ public final class SearchTask { self.apiVersion = apiVersion } - private func addResultHandler(_ resultHandler: @escaping ResultHandler) { - resultHandlers.append(resultHandler) - } - /// Cancel a previously started task public func cancel() { guard status == .running else { return assertionFailure() } @@ -156,30 +125,16 @@ public final class SearchTask { await self.performUserLookup() } - taskGroup.addTask { // TODO: delete - await withCheckedContinuation { continuation in - - self.addResultHandler { result, isCompleted in - - // 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) - - if isCompleted { - continuation.resume(returning: { _ in }) - } - - } - - } - } - while let aggregator = await taskGroup.next() { aggregator(&self.result) } - 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 } } } From 22a43b34ccc4de3d13681bbd35662e355860e58d Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Fri, 30 Jan 2026 19:22:27 +0100 Subject: [PATCH 22/71] some cleanup, add notes --- .../UserSession/Search/SearchTask.swift | 72 ++++++++----------- 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 1002077b747..f99d6c30ba9 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -36,8 +36,6 @@ public final class SearchTask { private var status = Status.pending - public typealias ResultHandler = (_ incrementalResult: SearchResult, _ isCompleted: Bool) -> Void - /// 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 three different ways of aggregating search results: @@ -52,12 +50,6 @@ public final class SearchTask { private let searchUsersCache: SearchUsersCache? private let type: `Type` - private var userLookupTaskIdentifier: ZMTaskIdentifier? - private var directoryTaskIdentifier: ZMTaskIdentifier? - private var teamMembershipTaskIdentifier: ZMTaskIdentifier? - private var handleTaskIdentifier: ZMTaskIdentifier? - private var resultHandlers: [ResultHandler] = [] - private var result = SearchResult() init( type: `Type`, @@ -75,16 +67,11 @@ public final class SearchTask { /// Cancel a previously started task public func cancel() { - guard status == .running else { return assertionFailure() } + guard status == .running else { + return assertionFailure() + } status = .cancelled - - teamMembershipTaskIdentifier.map(transportSession.cancelTask) - userLookupTaskIdentifier.map(transportSession.cancelTask) - directoryTaskIdentifier.map(transportSession.cancelTask) - handleTaskIdentifier.map(transportSession.cancelTask) - - // tasksRemaining is supposed to reach 0 eventually and trigger the continuation of `start`. } /// Start the search task. Errors will not be thrown. @@ -99,34 +86,35 @@ public final class SearchTask { return await withTaskGroup(of: SearchResultAggregator.self, returning: SearchResult.self) { taskGroup in - // search services + // search services taskGroup.addTask { - await self.performRemoteSearchForServices() + await self.performRemoteSearchForServices() manually test each call } // search People or groups taskGroup.addTask { - await self.performLocalLookup() + await self.performLocalLookup() manually test each call } taskGroup.addTask { - await self.performLocalSearch() + await self.performLocalSearch() manually test each call } // v1 taskGroup.addTask { - await self.performRemoteSearchForTeamUser() + await self.performRemoteSearchForTeamUser() manually test each call } // v2+ taskGroup.addTask { - await self.performRemoteSearch() + await self.performRemoteSearch() manually test each call } taskGroup.addTask { - await self.performUserLookup() + await self.performUserLookup() manually test each call } + var result = SearchResult() while let aggregator = await taskGroup.next() { - aggregator(&self.result) + aggregator(&result) } // add to search users cache @@ -586,7 +574,8 @@ extension SearchTask { request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in guard - let contextProvider = self?.contextProvider, + let self, + let contextProvider = contextProvider, let payload = response.payload?.asArray(), let userPayload = (payload.first as? ZMTransportData)?.asDictionary() else { @@ -608,30 +597,27 @@ extension SearchTask { query: searchRequest.query, searchOptions: searchRequest.searchOptions, contextProvider: contextProvider, - searchUsersCache: self?.searchUsersCache + searchUsersCache: searchUsersCache ) else { return continuation.resume(returning: { _ in }) } if let user = result.directory.first, !user.isSelfUser { - if let prevResult = self?.result { - // prepend result to prevResult only if it doesn't contain it - if !prevResult.directory.contains(user) { - let result = SearchResult( - context: prevResult.context, - contacts: prevResult.contacts, - teamMembers: prevResult.teamMembers, - directory: result.directory + prevResult.directory, - conversations: prevResult.conversations, - services: prevResult.services, - searchUsersCache: self?.searchUsersCache - ) - continuation.resume(returning: { $0 = result }) - } else { - continuation.resume(returning: { _ in }) - } - } else { + let prevResult = self.result TODO: check behavior on develop + // prepend result to prevResult only if it doesn't contain it + if !prevResult.directory.contains(user) { + let result = SearchResult( + context: prevResult.context, + contacts: prevResult.contacts, + teamMembers: prevResult.teamMembers, + directory: result.directory + prevResult.directory, + conversations: prevResult.conversations, + services: prevResult.services, + searchUsersCache: searchUsersCache + ) continuation.resume(returning: { $0 = result }) + } else { + continuation.resume(returning: { _ in }) } } else { continuation.resume(returning: { _ in }) From 8ddbf74c46514d61048a4ca667c87b90e8c1173f Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 09:42:52 +0100 Subject: [PATCH 23/71] add guard let self --- .../UserSession/Search/SearchTask.swift | 66 +++++++++---------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index a8775cad5e9..83cb9d50921 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -392,23 +392,22 @@ extension SearchTask { searchContext.performGroupedBlock { [self] in let request = type(of: self).searchRequestForUser(qualifiedID: qualifiedID, apiVersion: apiVersion) request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in + defer { self?.tasksRemaining -= 1 } guard - let contextProvider = self?.contextProvider, + let self, let payload = response.payload?.asDictionary(), let result = SearchResult( userLookupPayload: payload, contextProvider: contextProvider, - searchUsersCache: self?.searchUsersCache + searchUsersCache: searchUsersCache ) else { return } - if let updatedResult = self?.result.union(withDirectoryResult: result) { - self?.result = updatedResult - } + self.result = self.result.union(withDirectoryResult: result) }) request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in @@ -453,26 +452,26 @@ extension SearchTask { let request = Self.searchRequestInDirectory(withRequest: searchRequest, apiVersion: apiVersion) request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in + guard let self else { return } guard - let contextProvider = self?.contextProvider, let payload = response.payload?.asDictionary(), let result = SearchResult( payload: payload, query: searchRequest.query, searchOptions: searchRequest.searchOptions, contextProvider: contextProvider, - searchUsersCache: self?.searchUsersCache + searchUsersCache: searchUsersCache ) else { - self?.completeRemoteSearch() + completeRemoteSearch() return } if searchRequest.searchOptions.contains(.teamMembers) { - self?.performTeamMembershipLookup(on: result, searchRequest: searchRequest) + performTeamMembershipLookup(on: result, searchRequest: searchRequest) } else { - self?.completeRemoteSearch(searchResult: result) + completeRemoteSearch(searchResult: result) } }) @@ -503,12 +502,13 @@ extension SearchTask { ) request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in + guard let self else { return } + guard - let contextProvider = self?.contextProvider, let rawData = response.rawData, let payload = MembershipListPayload(rawData) else { - self?.completeRemoteSearch() + completeRemoteSearch() return } @@ -520,7 +520,7 @@ extension SearchTask { contextProvider: contextProvider ) - self?.completeRemoteSearch(searchResult: updatedResult) + completeRemoteSearch(searchResult: updatedResult) }) @@ -612,7 +612,7 @@ extension SearchTask { } guard - let contextProvider = self?.contextProvider, + let self, let payload = response.payload?.asArray(), let userPayload = (payload.first as? ZMTransportData)?.asDictionary() else { @@ -634,27 +634,24 @@ extension SearchTask { query: searchRequest.query, searchOptions: searchRequest.searchOptions, contextProvider: contextProvider, - searchUsersCache: self?.searchUsersCache + searchUsersCache: searchUsersCache ) else { return } if let user = result.directory.first, !user.isSelfUser { - if let prevResult = self?.result { - // prepend result to prevResult only if it doesn't contain it - if !prevResult.directory.contains(user) { - self?.result = SearchResult( - context: prevResult.context, - contacts: prevResult.contacts, - teamMembers: prevResult.teamMembers, - directory: result.directory + prevResult.directory, - conversations: prevResult.conversations, - services: prevResult.services, - searchUsersCache: self?.searchUsersCache - ) - } - } else { - self?.result = result + let prevResult = self.result + // prepend result to prevResult only if it doesn't contain it + if !prevResult.directory.contains(user) { + self.result = SearchResult( + context: prevResult.context, + contacts: prevResult.contacts, + teamMembers: prevResult.teamMembers, + directory: result.directory + prevResult.directory, + conversations: prevResult.conversations, + services: prevResult.services, + searchUsersCache: searchUsersCache + ) } } }) @@ -711,21 +708,20 @@ extension SearchTask { } guard - let contextProvider = self?.contextProvider, + let self, let payload = response.payload?.asDictionary(), let result = SearchResult( servicesPayload: payload, query: searchRequest.query.string, contextProvider: contextProvider, - searchUsersCache: self?.searchUsersCache + searchUsersCache: searchUsersCache ) else { return } - if let updatedResult = self?.result.union(withServiceResult: result) { - self?.result = updatedResult - } + let updatedResult = self.result.union(withServiceResult: result) + self.result = updatedResult }) request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in From 7f2baf7ce53c88c41ffce9812791f8c3496c8c61 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 09:49:17 +0100 Subject: [PATCH 24/71] rename local result variables --- .../UserSession/Search/SearchTask.swift | 94 ++++++++++--------- 1 file changed, 49 insertions(+), 45 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 83cb9d50921..278fc7e58f7 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -26,7 +26,7 @@ public class SearchTask { case lookup(qualifiedID: QualifiedID) } - public typealias ResultHandler = (_ result: SearchResult, _ isCompleted: Bool) -> Void + public typealias ResultHandler = (_ partialResult: SearchResult, _ isCompleted: Bool) -> Void private let apiVersion: WireTransport.APIVersion? private let transportSession: TransportSessionType @@ -193,7 +193,7 @@ extension SearchTask { let copiedConnectedUsers = connectedUsers .compactMap { contextProvider.viewContext.object(with: $0.objectID) as? ZMUser } - let result = SearchResult( + let partialResult = SearchResult( context: contextProvider.viewContext, contacts: copiedConnectedUsers.map { ZMSearchUser( @@ -215,7 +215,7 @@ extension SearchTask { searchUsersCache: searchUsersCache ) - self.result = self.result.union(withLocalResult: result.copy(on: contextProvider.viewContext)) + self.result = self.result.union(withLocalResult: partialResult.copy(on: contextProvider.viewContext)) tasksRemaining -= 1 } @@ -278,7 +278,7 @@ extension SearchTask { ) } - let result = SearchResult( + let partialResult = SearchResult( context: contextProvider.viewContext, contacts: searchConnectedUsers, teamMembers: searchTeamMembers, @@ -288,7 +288,7 @@ extension SearchTask { searchUsersCache: searchUsersCache ) - self.result = self.result.union(withLocalResult: result.copy(on: contextProvider.viewContext)) + self.result = self.result.union(withLocalResult: partialResult.copy(on: contextProvider.viewContext)) tasksRemaining -= 1 } @@ -307,10 +307,10 @@ extension SearchTask { } func teamMembers(matchingQuery query: String, team: Team?, searchOptions: SearchOptions) -> [Member] { - var result = team?.members(matchingQuery: query) ?? [] + var partialResult = team?.members(matchingQuery: query) ?? [] if searchOptions.contains(.excludeNonActiveTeamMembers) { - result = filterNonActiveTeamMembers(members: result) + partialResult = filterNonActiveTeamMembers(members: partialResult) } if searchOptions.contains(.excludeNonActivePartners) { @@ -319,7 +319,7 @@ extension SearchTask { let activeConversations = ZMUser.selfUser(in: searchContext).activeConversations let activeContacts = Set(activeConversations.flatMap(\.localParticipants)) - result = result.filter { membership in + partialResult = partialResult.filter { membership in if let user = membership.user { user.teamRole != .partner || user.handle == query || membership .createdBy == selfUser || activeContacts.contains(user) @@ -329,7 +329,7 @@ extension SearchTask { } } - return result + return partialResult } func connectedUsers(matchingQuery query: String, hostedOnDomain: String?) -> [ZMUser] { @@ -392,22 +392,23 @@ extension SearchTask { searchContext.performGroupedBlock { [self] in let request = type(of: self).searchRequestForUser(qualifiedID: qualifiedID, apiVersion: apiVersion) request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in - defer { self?.tasksRemaining -= 1 } guard - let self, + let contextProvider = self?.contextProvider, let payload = response.payload?.asDictionary(), - let result = SearchResult( + let partialResult = SearchResult( userLookupPayload: payload, contextProvider: contextProvider, - searchUsersCache: searchUsersCache + searchUsersCache: self?.searchUsersCache ) else { return } - self.result = self.result.union(withDirectoryResult: result) + if let updatedResult = self?.result.union(withDirectoryResult: partialResult) { + self?.result = updatedResult + } }) request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in @@ -452,26 +453,26 @@ extension SearchTask { let request = Self.searchRequestInDirectory(withRequest: searchRequest, apiVersion: apiVersion) request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in - guard let self else { return } guard + let contextProvider = self?.contextProvider, let payload = response.payload?.asDictionary(), - let result = SearchResult( + let partialResult = SearchResult( payload: payload, query: searchRequest.query, searchOptions: searchRequest.searchOptions, contextProvider: contextProvider, - searchUsersCache: searchUsersCache + searchUsersCache: self?.searchUsersCache ) else { - completeRemoteSearch() + self?.completeRemoteSearch() return } if searchRequest.searchOptions.contains(.teamMembers) { - performTeamMembershipLookup(on: result, searchRequest: searchRequest) + self?.performTeamMembershipLookup(on: partialResult, searchRequest: searchRequest) } else { - completeRemoteSearch(searchResult: result) + self?.completeRemoteSearch(searchResult: partialResult) } }) @@ -502,13 +503,12 @@ extension SearchTask { ) request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in - guard let self else { return } - guard + let contextProvider = self?.contextProvider, let rawData = response.rawData, let payload = MembershipListPayload(rawData) else { - completeRemoteSearch() + self?.completeRemoteSearch() return } @@ -520,7 +520,7 @@ extension SearchTask { contextProvider: contextProvider ) - completeRemoteSearch(searchResult: updatedResult) + self?.completeRemoteSearch(searchResult: updatedResult) }) @@ -612,7 +612,7 @@ extension SearchTask { } guard - let self, + let contextProvider = self?.contextProvider, let payload = response.payload?.asArray(), let userPayload = (payload.first as? ZMTransportData)?.asDictionary() else { @@ -629,29 +629,32 @@ extension SearchTask { let document = ["handle": handle, "name": name, "id": id] let documentPayload = ["documents": [document]] - guard let result = SearchResult( + guard let partialResult = SearchResult( payload: documentPayload, query: searchRequest.query, searchOptions: searchRequest.searchOptions, contextProvider: contextProvider, - searchUsersCache: searchUsersCache + searchUsersCache: self?.searchUsersCache ) else { return } - if let user = result.directory.first, !user.isSelfUser { - let prevResult = self.result - // prepend result to prevResult only if it doesn't contain it - if !prevResult.directory.contains(user) { - self.result = SearchResult( - context: prevResult.context, - contacts: prevResult.contacts, - teamMembers: prevResult.teamMembers, - directory: result.directory + prevResult.directory, - conversations: prevResult.conversations, - services: prevResult.services, - searchUsersCache: searchUsersCache - ) + if let user = partialResult.directory.first, !user.isSelfUser { + if let prevResult = self?.result { + // prepend result to prevResult only if it doesn't contain it + if !prevResult.directory.contains(user) { + self?.result = SearchResult( + context: prevResult.context, + contacts: prevResult.contacts, + teamMembers: prevResult.teamMembers, + directory: partialResult.directory + prevResult.directory, + conversations: prevResult.conversations, + services: prevResult.services, + searchUsersCache: self?.searchUsersCache + ) + } + } else { + self?.result = partialResult } } }) @@ -708,20 +711,21 @@ extension SearchTask { } guard - let self, + let contextProvider = self?.contextProvider, let payload = response.payload?.asDictionary(), - let result = SearchResult( + let partialResult = SearchResult( servicesPayload: payload, query: searchRequest.query.string, contextProvider: contextProvider, - searchUsersCache: searchUsersCache + searchUsersCache: self?.searchUsersCache ) else { return } - let updatedResult = self.result.union(withServiceResult: result) - self.result = updatedResult + if let updatedResult = self?.result.union(withServiceResult: partialResult) { + self?.result = updatedResult + } }) request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in From e14a0c58b911566b4a26b32ddd7fd29ad9be16b2 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 09:49:55 +0100 Subject: [PATCH 25/71] rename member variable result --- .../UserSession/Search/SearchTask.swift | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 278fc7e58f7..8e323bf4b0c 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -41,7 +41,7 @@ public class SearchTask { private var handleTaskIdentifier: ZMTaskIdentifier? private var servicesTaskIdentifier: ZMTaskIdentifier? private var resultHandlers: [ResultHandler] = [] - private var result = SearchResult( + private var aggregatedResult = SearchResult( context: .init(concurrencyType: .privateQueueConcurrencyType), contacts: [], teamMembers: [], @@ -68,7 +68,7 @@ public class SearchTask { // only trigger handles if decrement to 0 if oldValue > newValue { let isCompleted = newValue == 0 - resultHandlers.forEach { $0(result, isCompleted) } + resultHandlers.forEach { $0(aggregatedResult, isCompleted) } if isCompleted { resultHandlers.removeAll() @@ -215,7 +215,7 @@ extension SearchTask { searchUsersCache: searchUsersCache ) - self.result = self.result.union(withLocalResult: partialResult.copy(on: contextProvider.viewContext)) + self.aggregatedResult = self.aggregatedResult.union(withLocalResult: partialResult.copy(on: contextProvider.viewContext)) tasksRemaining -= 1 } @@ -288,7 +288,7 @@ extension SearchTask { searchUsersCache: searchUsersCache ) - self.result = self.result.union(withLocalResult: partialResult.copy(on: contextProvider.viewContext)) + self.aggregatedResult = self.aggregatedResult.union(withLocalResult: partialResult.copy(on: contextProvider.viewContext)) tasksRemaining -= 1 } @@ -406,8 +406,8 @@ extension SearchTask { ) else { return } - if let updatedResult = self?.result.union(withDirectoryResult: partialResult) { - self?.result = updatedResult + if let updatedResult = self?.aggregatedResult.union(withDirectoryResult: partialResult) { + self?.aggregatedResult = updatedResult } }) @@ -537,7 +537,7 @@ extension SearchTask { } if let searchResult { - result = result.union(withDirectoryResult: searchResult) + aggregatedResult = aggregatedResult.union(withDirectoryResult: searchResult) } } @@ -640,10 +640,10 @@ extension SearchTask { } if let user = partialResult.directory.first, !user.isSelfUser { - if let prevResult = self?.result { + if let prevResult = self?.aggregatedResult { // prepend result to prevResult only if it doesn't contain it if !prevResult.directory.contains(user) { - self?.result = SearchResult( + self?.aggregatedResult = SearchResult( context: prevResult.context, contacts: prevResult.contacts, teamMembers: prevResult.teamMembers, @@ -654,7 +654,7 @@ extension SearchTask { ) } } else { - self?.result = partialResult + self?.aggregatedResult = partialResult } } }) @@ -723,8 +723,8 @@ extension SearchTask { return } - if let updatedResult = self?.result.union(withServiceResult: partialResult) { - self?.result = updatedResult + if let updatedResult = self?.aggregatedResult.union(withServiceResult: partialResult) { + self?.aggregatedResult = updatedResult } }) From 2d8616f75848d1979dfc7346014e7d61520ad181 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 09:53:04 +0100 Subject: [PATCH 26/71] add guard let self --- .../UserSession/Search/SearchTask.swift | 48 +++++++++---------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 8e323bf4b0c..dc2c63d8980 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -397,18 +397,17 @@ extension SearchTask { } guard - let contextProvider = self?.contextProvider, + let self, let payload = response.payload?.asDictionary(), let partialResult = SearchResult( userLookupPayload: payload, contextProvider: contextProvider, - searchUsersCache: self?.searchUsersCache + searchUsersCache: self.searchUsersCache ) else { return } - if let updatedResult = self?.aggregatedResult.union(withDirectoryResult: partialResult) { - self?.aggregatedResult = updatedResult - } + let updatedResult = self.aggregatedResult.union(withDirectoryResult: partialResult) + self.aggregatedResult = updatedResult }) request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in @@ -453,26 +452,26 @@ extension SearchTask { let request = Self.searchRequestInDirectory(withRequest: searchRequest, apiVersion: apiVersion) request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in + guard let self else { return } guard - let contextProvider = self?.contextProvider, let payload = response.payload?.asDictionary(), let partialResult = SearchResult( payload: payload, query: searchRequest.query, searchOptions: searchRequest.searchOptions, contextProvider: contextProvider, - searchUsersCache: self?.searchUsersCache + searchUsersCache: self.searchUsersCache ) else { - self?.completeRemoteSearch() + self.completeRemoteSearch() return } if searchRequest.searchOptions.contains(.teamMembers) { - self?.performTeamMembershipLookup(on: partialResult, searchRequest: searchRequest) + self.performTeamMembershipLookup(on: partialResult, searchRequest: searchRequest) } else { - self?.completeRemoteSearch(searchResult: partialResult) + self.completeRemoteSearch(searchResult: partialResult) } }) @@ -503,12 +502,13 @@ extension SearchTask { ) request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in + guard let self else { return } + guard - let contextProvider = self?.contextProvider, let rawData = response.rawData, let payload = MembershipListPayload(rawData) else { - self?.completeRemoteSearch() + self.completeRemoteSearch() return } @@ -520,7 +520,7 @@ extension SearchTask { contextProvider: contextProvider ) - self?.completeRemoteSearch(searchResult: updatedResult) + self.completeRemoteSearch(searchResult: updatedResult) }) @@ -612,7 +612,7 @@ extension SearchTask { } guard - let contextProvider = self?.contextProvider, + let self, let payload = response.payload?.asArray(), let userPayload = (payload.first as? ZMTransportData)?.asDictionary() else { @@ -634,28 +634,25 @@ extension SearchTask { query: searchRequest.query, searchOptions: searchRequest.searchOptions, contextProvider: contextProvider, - searchUsersCache: self?.searchUsersCache + searchUsersCache: self.searchUsersCache ) else { return } if let user = partialResult.directory.first, !user.isSelfUser { - if let prevResult = self?.aggregatedResult { + let prevResult = self.aggregatedResult // prepend result to prevResult only if it doesn't contain it if !prevResult.directory.contains(user) { - self?.aggregatedResult = SearchResult( + self.aggregatedResult = SearchResult( context: prevResult.context, contacts: prevResult.contacts, teamMembers: prevResult.teamMembers, directory: partialResult.directory + prevResult.directory, conversations: prevResult.conversations, services: prevResult.services, - searchUsersCache: self?.searchUsersCache + searchUsersCache: self.searchUsersCache ) } - } else { - self?.aggregatedResult = partialResult - } } }) @@ -711,21 +708,20 @@ extension SearchTask { } guard - let contextProvider = self?.contextProvider, + let self, let payload = response.payload?.asDictionary(), let partialResult = SearchResult( servicesPayload: payload, query: searchRequest.query.string, contextProvider: contextProvider, - searchUsersCache: self?.searchUsersCache + searchUsersCache: self.searchUsersCache ) else { return } - if let updatedResult = self?.aggregatedResult.union(withServiceResult: partialResult) { - self?.aggregatedResult = updatedResult - } + let updatedResult = self.aggregatedResult.union(withServiceResult: partialResult) + self.aggregatedResult = updatedResult }) request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in From fe96e5b33200a14a7fba23dbaad22798b12bb28e Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 09:53:22 +0100 Subject: [PATCH 27/71] format code --- .../UserSession/Search/SearchTask.swift | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index dc2c63d8980..3cc96228a18 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -215,7 +215,8 @@ extension SearchTask { searchUsersCache: searchUsersCache ) - self.aggregatedResult = self.aggregatedResult.union(withLocalResult: partialResult.copy(on: contextProvider.viewContext)) + aggregatedResult = aggregatedResult + .union(withLocalResult: partialResult.copy(on: contextProvider.viewContext)) tasksRemaining -= 1 } @@ -288,7 +289,8 @@ extension SearchTask { searchUsersCache: searchUsersCache ) - self.aggregatedResult = self.aggregatedResult.union(withLocalResult: partialResult.copy(on: contextProvider.viewContext)) + aggregatedResult = aggregatedResult + .union(withLocalResult: partialResult.copy(on: contextProvider.viewContext)) tasksRemaining -= 1 } @@ -402,12 +404,12 @@ extension SearchTask { let partialResult = SearchResult( userLookupPayload: payload, contextProvider: contextProvider, - searchUsersCache: self.searchUsersCache + searchUsersCache: searchUsersCache ) else { return } - let updatedResult = self.aggregatedResult.union(withDirectoryResult: partialResult) - self.aggregatedResult = updatedResult + let updatedResult = aggregatedResult.union(withDirectoryResult: partialResult) + aggregatedResult = updatedResult }) request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in @@ -461,17 +463,17 @@ extension SearchTask { query: searchRequest.query, searchOptions: searchRequest.searchOptions, contextProvider: contextProvider, - searchUsersCache: self.searchUsersCache + searchUsersCache: searchUsersCache ) else { - self.completeRemoteSearch() + completeRemoteSearch() return } if searchRequest.searchOptions.contains(.teamMembers) { - self.performTeamMembershipLookup(on: partialResult, searchRequest: searchRequest) + performTeamMembershipLookup(on: partialResult, searchRequest: searchRequest) } else { - self.completeRemoteSearch(searchResult: partialResult) + completeRemoteSearch(searchResult: partialResult) } }) @@ -508,7 +510,7 @@ extension SearchTask { let rawData = response.rawData, let payload = MembershipListPayload(rawData) else { - self.completeRemoteSearch() + completeRemoteSearch() return } @@ -520,7 +522,7 @@ extension SearchTask { contextProvider: contextProvider ) - self.completeRemoteSearch(searchResult: updatedResult) + completeRemoteSearch(searchResult: updatedResult) }) @@ -634,25 +636,25 @@ extension SearchTask { query: searchRequest.query, searchOptions: searchRequest.searchOptions, contextProvider: contextProvider, - searchUsersCache: self.searchUsersCache + searchUsersCache: searchUsersCache ) else { return } if let user = partialResult.directory.first, !user.isSelfUser { - let prevResult = self.aggregatedResult - // prepend result to prevResult only if it doesn't contain it - if !prevResult.directory.contains(user) { - self.aggregatedResult = SearchResult( - context: prevResult.context, - contacts: prevResult.contacts, - teamMembers: prevResult.teamMembers, - directory: partialResult.directory + prevResult.directory, - conversations: prevResult.conversations, - services: prevResult.services, - searchUsersCache: self.searchUsersCache - ) - } + 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 + ) + } } }) @@ -714,14 +716,14 @@ extension SearchTask { servicesPayload: payload, query: searchRequest.query.string, contextProvider: contextProvider, - searchUsersCache: self.searchUsersCache + searchUsersCache: searchUsersCache ) else { return } - let updatedResult = self.aggregatedResult.union(withServiceResult: partialResult) - self.aggregatedResult = updatedResult + let updatedResult = aggregatedResult.union(withServiceResult: partialResult) + aggregatedResult = updatedResult }) request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in From f6969a1809f3b7038359f1d33c594a4ea6660d32 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 10:18:13 +0100 Subject: [PATCH 28/71] attempt to fix build errors --- .../UserSession/Search/SearchTask.swift | 37 +++++++++---------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 743c4c805e9..7062353b4c9 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -88,28 +88,28 @@ public final class SearchTask { // search services taskGroup.addTask { - await self.performRemoteSearchForServices() manually test each call + await self.performRemoteSearchForServices() // TODO: manually test each call } // search People or groups taskGroup.addTask { - await self.performLocalLookup() manually test each call + await self.performLocalLookup() // TODO: manually test each call } taskGroup.addTask { - await self.performLocalSearch() manually test each call + await self.performLocalSearch() // TODO: manually test each call } // v1 taskGroup.addTask { - await self.performRemoteSearchForTeamUser() manually test each call + await self.performRemoteSearchForTeamUser() // TODO: manually test each call } // v2+ taskGroup.addTask { - await self.performRemoteSearch() manually test each call + await self.performRemoteSearch() // TODO: manually test each call } taskGroup.addTask { - await self.performUserLookup() manually test each call + await self.performUserLookup() // TODO: manually test each call } var result = SearchResult() @@ -285,7 +285,7 @@ extension SearchTask { } private func teamMembers(matchingQuery query: String, team: Team?, searchOptions: SearchOptions) -> [Member] { - var result = team?.members(matchingQuery: query) ?? [] + var partialResult = team?.members(matchingQuery: query) ?? [] if searchOptions.contains(.excludeNonActiveTeamMembers) { partialResult = filterNonActiveTeamMembers(members: partialResult) @@ -384,7 +384,7 @@ extension SearchTask { ) else { return continuation.resume(returning: { _ in }) } - continuation.resume(returning: { $0 = $0.union(withDirectoryResult: result) }) + continuation.resume(returning: { $0 = $0.union(withDirectoryResult: partialResult) }) }) transportSession.enqueueOneTime(request) @@ -442,17 +442,14 @@ extension SearchTask { if searchRequest.searchOptions.contains(.teamMembers) { Task { - if let aggregator = await self?.performTeamMembershipLookup( - on: result, + let aggregator = await self.performTeamMembershipLookup( + on: partialResult, searchRequest: searchRequest - ) { - continuation.resume(returning: aggregator) - } else { - continuation.resume(returning: { _ in }) - } + ) + continuation.resume(returning: aggregator) } } else { - continuation.resume(returning: { $0 = $0.union(withDirectoryResult: result) }) + continuation.resume(returning: { $0 = $0.union(withDirectoryResult: partialResult) }) } }) @@ -602,15 +599,15 @@ extension SearchTask { return continuation.resume(returning: { _ in }) } - if let user = result.directory.first, !user.isSelfUser { - let prevResult = self.result TODO: check behavior on develop + 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) { let result = SearchResult( context: prevResult.context, contacts: prevResult.contacts, teamMembers: prevResult.teamMembers, - directory: result.directory + prevResult.directory, + directory: partialResult.directory + prevResult.directory, conversations: prevResult.conversations, services: prevResult.services, searchUsersCache: searchUsersCache @@ -684,7 +681,7 @@ extension SearchTask { ) else { return continuation.resume(returning: { _ in }) } - continuation.resume { $0 = $0.union(withServiceResult: result) } + continuation.resume { $0 = $0.union(withServiceResult: partialResult) } }) From a6d81e12d314770a18093e9f617077da613f37d4 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 10:50:30 +0100 Subject: [PATCH 29/71] minor fix --- .../UserSession/Search/SearchResult.swift | 12 +++++++ .../UserSession/Search/SearchTask.swift | 31 +++++++++---------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchResult.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchResult.swift index 949117cd3ab..2ac2a2365f2 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchResult.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchResult.swift @@ -233,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 7062353b4c9..a139e5d48f6 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -572,7 +572,6 @@ extension SearchTask { guard let self, - let contextProvider = contextProvider, let payload = response.payload?.asArray(), let userPayload = (payload.first as? ZMTransportData)?.asDictionary() else { @@ -600,22 +599,20 @@ extension SearchTask { } 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) { - let result = SearchResult( - context: prevResult.context, - contacts: prevResult.contacts, - teamMembers: prevResult.teamMembers, - directory: partialResult.directory + prevResult.directory, - conversations: prevResult.conversations, - services: prevResult.services, - searchUsersCache: searchUsersCache - ) - continuation.resume(returning: { $0 = result }) - } else { - continuation.resume(returning: { _ in }) - } + let partialResult = SearchResult( + context: contextProvider.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 }) } From 63f647abcecda9290f8ba55dcd5b63ace0c529f0 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 10:53:51 +0100 Subject: [PATCH 30/71] remove unused code --- .../Sources/WireNetwork/APIs/Rest/Search/SearchAPIV1.swift | 2 +- .../Sources/WireNetwork/APIs/Rest/Search/SearchAPIV15.swift | 2 +- .../Source/UserSession/Search/SearchTask.swift | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV1.swift b/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV1.swift index 3c85a21f005..ff4eea61bac 100644 --- a/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV1.swift +++ b/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV1.swift @@ -55,7 +55,7 @@ class SearchAPIV1: SearchAPIV0 { urlComponents.path = "\(pathPrefix)\(basePath)" urlComponents.queryItems = queryItems - guard let path = urlComponents.string?.replacingOccurrences(of: "+", with: "%2B") else { + guard let path = urlComponents.string else { throw SearchAPIError.invalidRequest } diff --git a/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV15.swift b/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV15.swift index 2a97b47b597..76e05b1e931 100644 --- a/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV15.swift +++ b/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV15.swift @@ -51,7 +51,7 @@ final class SearchAPIV15: SearchAPIV14 { urlComponents.path = "\(pathPrefix)\(basePath)" urlComponents.queryItems = queryItems - guard let path = urlComponents.string?.replacingOccurrences(of: "+", with: "%2B") else { + guard let path = urlComponents.string else { throw SearchAPIError.invalidRequest } diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 3cc96228a18..956b7de331a 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -561,7 +561,7 @@ extension SearchTask { url.path = "/search/contacts" url.queryItems = queryItems - let path = url.string?.replacingOccurrences(of: "+", with: "%2B") ?? "" + let path = url.string ?? "" return ZMTransportRequest(getFromPath: path, apiVersion: apiVersion.rawValue) } @@ -676,7 +676,7 @@ extension SearchTask { var url = URLComponents() url.path = "/users" url.queryItems = [URLQueryItem(name: "handles", value: handle)] - let urlStr = url.string?.replacingOccurrences(of: "+", with: "%2B") ?? "" + let urlStr = url.string ?? "" return ZMTransportRequest(getFromPath: urlStr, apiVersion: apiVersion.rawValue) } } @@ -746,7 +746,7 @@ extension SearchTask { if !trimmedQuery.isEmpty { url.queryItems = [URLQueryItem(name: "prefix", value: trimmedQuery)] } - let urlStr = url.string?.replacingOccurrences(of: "+", with: "%2B") ?? "" + let urlStr = url.string ?? "" return ZMTransportRequest(getFromPath: urlStr, apiVersion: apiVersion.rawValue) } } From 596b7d16e37e7a3401b19f562f4e041bf59d8845 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 10:58:10 +0100 Subject: [PATCH 31/71] minor change --- .../Source/UserSession/Search/SearchTask.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index a139e5d48f6..a4337cbbbbb 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -561,6 +561,7 @@ extension SearchTask { searchRequest.searchOptions.contains(.directory) else { return { _ in } } + let viewContext = contextProvider.viewContext return await withCheckedContinuation { continuation in let request = Self.searchRequestInDirectory( @@ -568,7 +569,7 @@ extension SearchTask { apiVersion: apiVersion ) - request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in + request.add(ZMCompletionHandler(on: viewContext) { [weak self] response in guard let self, @@ -600,7 +601,7 @@ extension SearchTask { if let user = partialResult.directory.first, !user.isSelfUser { let partialResult = SearchResult( - context: contextProvider.viewContext, + context: viewContext, contacts: [], teamMembers: [], directory: partialResult.directory, From 2aa37e6de9ba22095eaa4bd5730c766ef3eefe54 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 11:04:07 +0100 Subject: [PATCH 32/71] remove unused code --- .../UserInterface/UserProfile/SearchUserViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/SearchUserViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/SearchUserViewController.swift index 7ba88339e99..8a809fa530c 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/SearchUserViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/SearchUserViewController.swift @@ -137,7 +137,7 @@ final class SearchUserViewController: UIViewController { navigationController?.setViewControllers([profileViewController], animated: true) resultHandled = true - } else if isCompleted { + } else { let alert = UIAlertController( title: L10n.Localizable.UrlAction.InvalidUser.title, message: L10n.Localizable.UrlAction.InvalidUser.message, From 60e932a74f5a27f70af208d37812130adbf06076 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 11:06:07 +0100 Subject: [PATCH 33/71] remove comment --- .../Source/Notifications/SearchUserObserverCenter.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wire-ios-data-model/Source/Notifications/SearchUserObserverCenter.swift b/wire-ios-data-model/Source/Notifications/SearchUserObserverCenter.swift index fdce0a5eb7c..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 final class SearchUserSnapshot { // TODO: what is it needed for? +public final class SearchUserSnapshot { /// Keys that we want to be notified for static let observableKeys: [String] = [ From 290b40be40c3818b9ba9b2957861cbb98fb4cfee Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 11:39:52 +0100 Subject: [PATCH 34/71] revert some changes --- .../Sources/WireNetwork/APIs/Rest/Search/SearchAPIV1.swift | 2 +- .../Sources/WireNetwork/APIs/Rest/Search/SearchAPIV15.swift | 2 +- .../Source/UserSession/Search/SearchTask.swift | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV1.swift b/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV1.swift index ff4eea61bac..3c85a21f005 100644 --- a/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV1.swift +++ b/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV1.swift @@ -55,7 +55,7 @@ class SearchAPIV1: SearchAPIV0 { urlComponents.path = "\(pathPrefix)\(basePath)" urlComponents.queryItems = queryItems - guard let path = urlComponents.string else { + guard let path = urlComponents.string?.replacingOccurrences(of: "+", with: "%2B") else { throw SearchAPIError.invalidRequest } diff --git a/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV15.swift b/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV15.swift index 76e05b1e931..2a97b47b597 100644 --- a/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV15.swift +++ b/WireNetwork/Sources/WireNetwork/APIs/Rest/Search/SearchAPIV15.swift @@ -51,7 +51,7 @@ final class SearchAPIV15: SearchAPIV14 { urlComponents.path = "\(pathPrefix)\(basePath)" urlComponents.queryItems = queryItems - guard let path = urlComponents.string else { + guard let path = urlComponents.string?.replacingOccurrences(of: "+", with: "%2B") else { throw SearchAPIError.invalidRequest } diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 956b7de331a..3cc96228a18 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -561,7 +561,7 @@ extension SearchTask { url.path = "/search/contacts" url.queryItems = queryItems - let path = url.string ?? "" + let path = url.string?.replacingOccurrences(of: "+", with: "%2B") ?? "" return ZMTransportRequest(getFromPath: path, apiVersion: apiVersion.rawValue) } @@ -676,7 +676,7 @@ extension SearchTask { var url = URLComponents() url.path = "/users" url.queryItems = [URLQueryItem(name: "handles", value: handle)] - let urlStr = url.string ?? "" + let urlStr = url.string?.replacingOccurrences(of: "+", with: "%2B") ?? "" return ZMTransportRequest(getFromPath: urlStr, apiVersion: apiVersion.rawValue) } } @@ -746,7 +746,7 @@ extension SearchTask { if !trimmedQuery.isEmpty { url.queryItems = [URLQueryItem(name: "prefix", value: trimmedQuery)] } - let urlStr = url.string ?? "" + let urlStr = url.string?.replacingOccurrences(of: "+", with: "%2B") ?? "" return ZMTransportRequest(getFromPath: urlStr, apiVersion: apiVersion.rawValue) } } From 9ad054140fb16a301e23830b903a604612a004f1 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 11:47:54 +0100 Subject: [PATCH 35/71] escape plus character in query items --- .../Sources/WireNetwork/APIs/Rest/TeamsAPI/TeamsAPIV5.swift | 1 + .../WireNetworkTests/APIs/Rest/Search/SearchAPITests.swift | 4 ++-- ...onse_200_V15AndAbove_Then_Verify_Request.request-0-v15.txt | 2 +- ...sResponse_200_V1ToV14_Then_Verify_Request.request-0-v1.txt | 2 +- ...Response_200_V1ToV14_Then_Verify_Request.request-0-v10.txt | 2 +- ...Response_200_V1ToV14_Then_Verify_Request.request-0-v11.txt | 2 +- ...Response_200_V1ToV14_Then_Verify_Request.request-0-v12.txt | 2 +- ...Response_200_V1ToV14_Then_Verify_Request.request-0-v13.txt | 2 +- ...Response_200_V1ToV14_Then_Verify_Request.request-0-v14.txt | 2 +- ...sResponse_200_V1ToV14_Then_Verify_Request.request-0-v2.txt | 2 +- ...sResponse_200_V1ToV14_Then_Verify_Request.request-0-v3.txt | 2 +- ...sResponse_200_V1ToV14_Then_Verify_Request.request-0-v4.txt | 2 +- ...sResponse_200_V1ToV14_Then_Verify_Request.request-0-v5.txt | 2 +- ...sResponse_200_V1ToV14_Then_Verify_Request.request-0-v6.txt | 2 +- ...sResponse_200_V1ToV14_Then_Verify_Request.request-0-v7.txt | 2 +- ...sResponse_200_V1ToV14_Then_Verify_Request.request-0-v8.txt | 2 +- ...sResponse_200_V1ToV14_Then_Verify_Request.request-0-v9.txt | 2 +- 17 files changed, 18 insertions(+), 17 deletions(-) diff --git a/WireNetwork/Sources/WireNetwork/APIs/Rest/TeamsAPI/TeamsAPIV5.swift b/WireNetwork/Sources/WireNetwork/APIs/Rest/TeamsAPI/TeamsAPIV5.swift index 6f4ea19e58f..638edd4cf98 100644 --- a/WireNetwork/Sources/WireNetwork/APIs/Rest/TeamsAPI/TeamsAPIV5.swift +++ b/WireNetwork/Sources/WireNetwork/APIs/Rest/TeamsAPI/TeamsAPIV5.swift @@ -132,6 +132,7 @@ class TeamsAPIV5: TeamsAPIV4 { return .init(element: [], hasMore: false, nextStart: "") } + let prefix = prefix.replacingOccurrences(of: "+", with: "%2B") var requestBuilder = try URLRequestBuilder(path: path) .withMethod(.get) .withQueryItem(name: "prefix", value: prefix) diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/SearchAPITests.swift b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/SearchAPITests.swift index e7b93d810d8..16fe0ac1caf 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/SearchAPITests.swift +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/SearchAPITests.swift @@ -54,7 +54,7 @@ final class SearchAPITests: XCTestCase { // When try await apiSnapshotHelper.verifyRequest(for: [apiVersion], apiService: apiService) { sut in - let contacts = try await sut.searchContacts(query: "lorem", domain: "", type: .app).documents + let contacts = try await sut.searchContacts(query: "lorem ipsum", domain: "", type: .app).documents // Then XCTAssertEqual( @@ -104,7 +104,7 @@ final class SearchAPITests: XCTestCase { // When try await apiSnapshotHelper.verifyRequest(for: [apiVersion], apiService: apiService) { sut in - let contacts = try await sut.searchContacts(query: "lorem", domain: "", type: .app).documents + let contacts = try await sut.searchContacts(query: "lorem ipsum", domain: "", type: .app).documents // Then XCTAssertEqual( diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V15AndAbove_Then_Verify_Request.request-0-v15.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V15AndAbove_Then_Verify_Request.request-0-v15.txt index 15b8ff34aef..e76d960796a 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V15AndAbove_Then_Verify_Request.request-0-v15.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V15AndAbove_Then_Verify_Request.request-0-v15.txt @@ -1,2 +1,2 @@ curl \ - "/v15/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v15/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v1.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v1.txt index 93fb0c5e1f3..f31e86061d1 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v1.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v1.txt @@ -1,2 +1,2 @@ curl \ - "/v1/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v1/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v10.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v10.txt index a93d2470260..a2800018905 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v10.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v10.txt @@ -1,2 +1,2 @@ curl \ - "/v10/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v10/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v11.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v11.txt index cc7bfa96558..e66736bc6e0 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v11.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v11.txt @@ -1,2 +1,2 @@ curl \ - "/v11/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v11/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v12.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v12.txt index 31700959b85..773979cf4d7 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v12.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v12.txt @@ -1,2 +1,2 @@ curl \ - "/v12/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v12/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v13.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v13.txt index 3af46055707..16c392d6222 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v13.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v13.txt @@ -1,2 +1,2 @@ curl \ - "/v13/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v13/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v14.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v14.txt index 8694f0d82dc..04506edd4a8 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v14.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v14.txt @@ -1,2 +1,2 @@ curl \ - "/v14/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v14/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v2.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v2.txt index 238a87dfc36..062138da1c8 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v2.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v2.txt @@ -1,2 +1,2 @@ curl \ - "/v2/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v2/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v3.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v3.txt index d2b193d278e..cc512c3c5cc 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v3.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v3.txt @@ -1,2 +1,2 @@ curl \ - "/v3/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v3/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v4.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v4.txt index dc2128df749..4c5d4513ea9 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v4.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v4.txt @@ -1,2 +1,2 @@ curl \ - "/v4/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v4/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v5.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v5.txt index 97ef8d501d2..3915e9697c5 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v5.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v5.txt @@ -1,2 +1,2 @@ curl \ - "/v5/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v5/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v6.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v6.txt index 75494be62b5..5c2195497d0 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v6.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v6.txt @@ -1,2 +1,2 @@ curl \ - "/v6/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v6/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v7.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v7.txt index 67077bb28ff..9a1cc47786c 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v7.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v7.txt @@ -1,2 +1,2 @@ curl \ - "/v7/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v7/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v8.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v8.txt index 8ebec712193..61a8ec35a45 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v8.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v8.txt @@ -1,2 +1,2 @@ curl \ - "/v8/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v8/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v9.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v9.txt index 798a3567dd2..e0458aa5ab6 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v9.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v9.txt @@ -1,2 +1,2 @@ curl \ - "/v9/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v9/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file From 333369ce3961b9d467b4ba0bca268001d1b8fb04 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 12:05:23 +0100 Subject: [PATCH 36/71] fix one test --- .../Source/UserSession/SearchTaskTests.swift | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift index 1ee427e00b5..2fa673a3434 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift @@ -17,9 +17,9 @@ // import XCTest - import WireMockTransport import WireTransport + @testable import WireSyncEngine final class SearchTaskTests: DatabaseTest { @@ -85,11 +85,9 @@ final class SearchTaskTests: DatabaseTest { return conversation } - func testThatItFindsASingleUnconnectedUserByHandle() { + func testThatItFindsASingleUnconnectedUserByHandle() async throws { // given - let remoteResultArrived = customExpectation(description: "received remote result") - mockTransportSession.performRemoteChanges { remoteChanges in let mockUser = remoteChanges.insertUser(withName: "Dale Cooper") mockUser.handle = "bob" @@ -98,18 +96,16 @@ 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") } @@ -1440,4 +1436,5 @@ final class SearchTaskTests: DatabaseTest { apiVersion: apiVersion ) } + } From 7a9b596c4dcb6944d4b2d3680e9f6e71e58d3ae0 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 12:25:41 +0100 Subject: [PATCH 37/71] add async throws --- .../UserSession/Search/SearchTask.swift | 5 +- .../Source/UserSession/SearchTaskTests.swift | 110 +++++++++--------- 2 files changed, 56 insertions(+), 59 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index dd715c1549f..c4777a998f0 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -67,10 +67,7 @@ public final class SearchTask { /// Cancel a previously started task public func cancel() { - guard status == .running else { - return assertionFailure() - } - + guard status == .running else { return assertionFailure() } status = .cancelled } diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift index 2fa673a3434..875a4f69bef 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift @@ -109,7 +109,7 @@ final class SearchTaskTests: DatabaseTest { } - func testThatItReturnsNothingWhenSearchingForSelfUserByHandle() { + func testThatItReturnsNothingWhenSearchingForSelfUserByHandle() async throws { // given var selfUserID: UUID! @@ -144,7 +144,7 @@ final class SearchTaskTests: DatabaseTest { // MARK: Contacts Search - func testThatItFindsASingleUser() { + func testThatItFindsASingleUser() async throws { // given let resultArrived = customExpectation(description: "received result") @@ -164,7 +164,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItDoesFindUsersContainingButNotBeginningWithSearchString() { + func testThatItDoesFindUsersContainingButNotBeginningWithSearchString() async throws { // given let resultArrived = customExpectation(description: "received result") _ = createConnectedUser(withName: "userA") @@ -183,7 +183,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItFindsUsersBeginningWithSearchString() { + func testThatItFindsUsersBeginningWithSearchString() async throws { // given let resultArrived = customExpectation(description: "received result") let user = createConnectedUser(withName: "userA") @@ -202,7 +202,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItUsesAllQueryComponentsToFindAUser() { + func testThatItUsesAllQueryComponentsToFindAUser() async throws { // given let resultArrived = customExpectation(description: "received result") let user1 = createConnectedUser(withName: "Some Body") @@ -223,7 +223,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItFindsSeveralUsers() { + func testThatItFindsSeveralUsers() async throws { // given let resultArrived = customExpectation(description: "received result") let user1 = createConnectedUser(withName: "Grant") @@ -244,7 +244,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatUserSearchIsCaseInsensitive() { + func testThatUserSearchIsCaseInsensitive() async throws { // given let resultArrived = customExpectation(description: "received result") let user1 = createConnectedUser(withName: "Somebody") @@ -263,7 +263,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatUserSearchIsInsensitiveToDiacritics() { + func testThatUserSearchIsInsensitiveToDiacritics() async throws { // given let resultArrived = customExpectation(description: "received result") let user1 = createConnectedUser(withName: "Sömëbodÿ") @@ -282,7 +282,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatUserSearchOnlyReturnsConnectedUsers() { + func testThatUserSearchOnlyReturnsConnectedUsers() async throws { // given let resultArrived = customExpectation(description: "received result") let user1 = createConnectedUser(withName: "Somebody Blocked") @@ -305,7 +305,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItDoesNotReturnTheSelfUser() { + func testThatItDoesNotReturnTheSelfUser() async throws { // given let resultArrived = customExpectation(description: "received result") let selfUser = ZMUser.selfUser(in: uiMOC) @@ -326,7 +326,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItDoesNotFindUsersWithOtherDomainsIfSearchDomainIsRequired() { + func testThatItDoesNotFindUsersWithOtherDomainsIfSearchDomainIsRequired() async throws { // given let resultArrived = customExpectation(description: "received result") let user = createConnectedUser(withName: "userA", domain: "bella.com") @@ -345,7 +345,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItFindsUsersWithSameDomainAsSelfUser() { + func testThatItFindsUsersWithSameDomainAsSelfUser() async throws { // given let resultArrived = customExpectation(description: "received result") let user = createConnectedUser(withName: "userA", domain: "anta.com") @@ -364,7 +364,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItFindsUsersWithOtherDomainsIfSearchDomainIsNotRequired() { + func testThatItFindsUsersWithOtherDomainsIfSearchDomainIsNotRequired() async throws { // given let resultArrived = customExpectation(description: "received result") let user = createConnectedUser(withName: "userA", domain: "bella.com") @@ -385,7 +385,7 @@ final class SearchTaskTests: DatabaseTest { // MARK: Team member local search - func testThatItCanSearchForTeamMembersLocally() { + func testThatItCanSearchForTeamMembersLocally() async throws { // given let resultArrived = customExpectation(description: "received result") let team = Team.insertNewObject(in: uiMOC) @@ -413,7 +413,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItCanExcludeNonActiveTeamMembersLocally() { + func testThatItCanExcludeNonActiveTeamMembersLocally() async throws { // given let resultArrived = customExpectation(description: "received result") let team = Team.insertNewObject(in: uiMOC) @@ -455,7 +455,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItIncludesNonActiveTeamMembersLocally_WhenSelfUserWasCreatedByThem() { + func testThatItIncludesNonActiveTeamMembersLocally_WhenSelfUserWasCreatedByThem() async throws { // given let resultArrived = customExpectation(description: "received result") let team = Team.insertNewObject(in: uiMOC) @@ -489,7 +489,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItCanExcludeNonActivePartnersLocally() { + func testThatItCanExcludeNonActivePartnersLocally() async throws { // given let resultArrived = customExpectation(description: "received result") let team = Team.insertNewObject(in: uiMOC) @@ -540,7 +540,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItIncludesNonActivePartnersLocally_WhenSearchingWithExactHandle() { + func testThatItIncludesNonActivePartnersLocally_WhenSearchingWithExactHandle() async throws { // given let resultArrived = customExpectation(description: "received result") let team = Team.insertNewObject(in: uiMOC) @@ -570,7 +570,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItIncludesNonActivePartnersLocally_WhenSelfUserCreatedPartner() { + func testThatItIncludesNonActivePartnersLocally_WhenSelfUserCreatedPartner() async throws { // given let resultArrived = customExpectation(description: "received result") let team = Team.insertNewObject(in: uiMOC) @@ -603,7 +603,7 @@ final class SearchTaskTests: DatabaseTest { // MARK: Conversation Search - func testThatItFindsASingleConversation() { + func testThatItFindsASingleConversation() async throws { // given let resultArrived = customExpectation(description: "received result") let conversation = createGroupConversation(withName: "Somebody") @@ -622,7 +622,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItDoesFindConversationsUsingPartialNames() { + func testThatItDoesFindConversationsUsingPartialNames() async throws { // given let resultArrived = customExpectation(description: "received result") let conversation = createGroupConversation(withName: "Somebody") @@ -641,7 +641,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItFindsSeveralConversations() { + func testThatItFindsSeveralConversations() async throws { // given let resultArrived = customExpectation(description: "received result") let conversation1 = createGroupConversation(withName: "Candy Apple Records") @@ -662,7 +662,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatConversationSearchIsCaseInsensitive() { + func testThatConversationSearchIsCaseInsensitive() async throws { // given let resultArrived = customExpectation(description: "received result") let conversation = createGroupConversation(withName: "SoMEBody") @@ -681,7 +681,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatConversationSearchIsInsensitiveToDiacritics() { + func testThatConversationSearchIsInsensitiveToDiacritics() async throws { // given let resultArrived = customExpectation(description: "received result") let conversation = createGroupConversation(withName: "Sömëbodÿ") @@ -700,7 +700,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItOnlyFindsGroupConversations() { + func testThatItOnlyFindsGroupConversations() async throws { // given let resultArrived = customExpectation(description: "received result") let groupConversation = createGroupConversation(withName: "Group Conversation") @@ -725,7 +725,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItFindsConversationsThatDoNotHaveAUserDefinedName() { + func testThatItFindsConversationsThatDoNotHaveAUserDefinedName() async throws { // given let resultArrived = customExpectation(description: "received result") let conversation = ZMConversation.insertNewObject(in: uiMOC) @@ -754,7 +754,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItFindsConversationsThatContainsSearchTermOnlyInParticipantName() { + func testThatItFindsConversationsThatContainsSearchTermOnlyInParticipantName() async throws { // given let resultArrived = customExpectation(description: "received result") let conversation = createGroupConversation(withName: "Summertime") @@ -777,7 +777,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItOrdersConversationsByUserDefinedName() { + func testThatItOrdersConversationsByUserDefinedName() async throws { // given let resultArrived = customExpectation(description: "received result") let conversation1 = createGroupConversation(withName: "FooA") @@ -798,7 +798,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItOrdersConversationsByUserDefinedNameFirstAndByParticipantNameSecond() { + func testThatItOrdersConversationsByUserDefinedNameFirstAndByParticipantNameSecond() async throws { // given let resultArrived = customExpectation(description: "received result") let user1 = createConnectedUser(withName: "Bla") @@ -828,7 +828,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItFiltersConversationWhenTheQueryStartsWithAtSymbol() { + func testThatItFiltersConversationWhenTheQueryStartsWithAtSymbol() async throws { // given let resultArrived = customExpectation(description: "received result") _ = createGroupConversation(withName: "New Day Rising") @@ -848,7 +848,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItReturnsAllConversationsWhenPassingTeamParameter() { + func testThatItReturnsAllConversationsWhenPassingTeamParameter() async throws { // given let resultArrived = customExpectation(description: "received result") let team = Team.insertNewObject(in: uiMOC) @@ -875,7 +875,7 @@ final class SearchTaskTests: DatabaseTest { // 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) @@ -891,7 +891,7 @@ 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) @@ -904,7 +904,7 @@ final class SearchTaskTests: DatabaseTest { 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) @@ -917,7 +917,7 @@ final class SearchTaskTests: DatabaseTest { 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) @@ -933,7 +933,7 @@ final class SearchTaskTests: DatabaseTest { ) } - func testThatItEncodesUnsafeCharactersInRequest() { + func testThatItEncodesUnsafeCharactersInRequest() async throws { // RFC 3986 Section 3.4 "Query" // // @@ -954,7 +954,7 @@ final class SearchTaskTests: DatabaseTest { ) } - func testThatItCallsCompletionHandlerForDirectorySearch() { + func testThatItCallsCompletionHandlerForDirectorySearch() async throws { // given let resultArrived = customExpectation(description: "received result") let request = SearchRequest(query: "User", searchOptions: [.directory]) @@ -977,7 +977,7 @@ final class SearchTaskTests: DatabaseTest { // 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) @@ -1003,7 +1003,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) @@ -1024,7 +1024,7 @@ final class SearchTaskTests: DatabaseTest { 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]) @@ -1055,7 +1055,7 @@ final class SearchTaskTests: DatabaseTest { // 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) @@ -1073,7 +1073,7 @@ 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) @@ -1086,7 +1086,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(mockTransportSession.receivedRequests().isEmpty) } - func testThatItCallsCompletionHandlerForServicesSearch() { + func testThatItCallsCompletionHandlerForServicesSearch() async throws { // given let resultArrived = customExpectation(description: "received result") let request = SearchRequest(query: "Service", searchOptions: [.services]) @@ -1138,7 +1138,7 @@ final class SearchTaskTests: DatabaseTest { // MARK: User lookup - func testThatItSendsAUserLookupRequest() { + func testThatItSendsAUserLookupRequest() async throws { // given let userId = UUID() let task = makeSearchTask(lookupUserId: userId) @@ -1151,7 +1151,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertEqual(mockTransportSession.receivedRequests().first?.path, "/users/\(userId.transportString())") } - func testThatItSendsAUserLookupRequest_IfApiVersionIsV2AndAbove() { + func testThatItSendsAUserLookupRequest_IfApiVersionIsV2AndAbove() async throws { // given let userId = UUID() let domain = "wire.com" @@ -1168,7 +1168,7 @@ final class SearchTaskTests: DatabaseTest { ) } - func testThatItCallsCompletionHandlerForUserLookup() { + func testThatItCallsCompletionHandlerForUserLookup() async throws { // given let resultArrived = customExpectation(description: "received result") @@ -1192,7 +1192,7 @@ final class SearchTaskTests: DatabaseTest { // 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) @@ -1205,7 +1205,7 @@ final class SearchTaskTests: DatabaseTest { 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) @@ -1220,7 +1220,7 @@ 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") @@ -1246,7 +1246,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatItCallsCompletionHandlerForFederatedUserSearch_WhenUserDoesntExist() { + func testThatItCallsCompletionHandlerForFederatedUserSearch_WhenUserDoesntExist() async throws { // given let resultArrived = customExpectation(description: "received result") mockTransportSession.federatedDomains = ["example.com"] @@ -1267,7 +1267,7 @@ final class SearchTaskTests: DatabaseTest { // MARK: Combined results - func testThatRemoteResultsIncludePreviousLocalResults() { + func testThatRemoteResultsIncludePreviousLocalResults() async throws { // given let localResultArrived = customExpectation(description: "received local result") let user = createConnectedUser(withName: "userA") @@ -1303,7 +1303,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatLocalResultsIncludePreviousRemoteResults() { + func testThatLocalResultsIncludePreviousRemoteResults() async throws { // given let remoteResultArrived = customExpectation(description: "received remote result") _ = createConnectedUser(withName: "userA") @@ -1339,7 +1339,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatTaskIsCompletedAfterLocalResult() { + func testThatTaskIsCompletedAfterLocalResult() async throws { // given let localResultArrived = customExpectation(description: "received local result") let user = createConnectedUser(withName: "userA") @@ -1358,7 +1358,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatTaskIsCompletedAfterRemoteResults() { + func testThatTaskIsCompletedAfterRemoteResults() async throws { // given let remoteResultArrived = customExpectation(description: "received remote result") mockTransportSession.performRemoteChanges { remoteChanges in @@ -1380,7 +1380,7 @@ final class SearchTaskTests: DatabaseTest { XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } - func testThatTaskIsCompletedOnlyAfterFinalResultArrives() { + func testThatTaskIsCompletedOnlyAfterFinalResultArrives() async throws { // given let intermediateResultArrived = customExpectation(description: "received intermediate result") let finalResultsArrived = customExpectation(description: "received final result") From 5ff02d7c7d4ea4602247502273f2b8fd11e596fa Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 12:29:21 +0100 Subject: [PATCH 38/71] chore: improve code readability of SearchTask.swift - WPB-20362 (#4241) --- .../APIs/Rest/TeamsAPI/TeamsAPIV5.swift | 1 + .../APIs/Rest/Search/SearchAPITests.swift | 4 +- ...bove_Then_Verify_Request.request-0-v15.txt | 2 +- ...ToV14_Then_Verify_Request.request-0-v1.txt | 2 +- ...oV14_Then_Verify_Request.request-0-v10.txt | 2 +- ...oV14_Then_Verify_Request.request-0-v11.txt | 2 +- ...oV14_Then_Verify_Request.request-0-v12.txt | 2 +- ...oV14_Then_Verify_Request.request-0-v13.txt | 2 +- ...oV14_Then_Verify_Request.request-0-v14.txt | 2 +- ...ToV14_Then_Verify_Request.request-0-v2.txt | 2 +- ...ToV14_Then_Verify_Request.request-0-v3.txt | 2 +- ...ToV14_Then_Verify_Request.request-0-v4.txt | 2 +- ...ToV14_Then_Verify_Request.request-0-v5.txt | 2 +- ...ToV14_Then_Verify_Request.request-0-v6.txt | 2 +- ...ToV14_Then_Verify_Request.request-0-v7.txt | 2 +- ...ToV14_Then_Verify_Request.request-0-v8.txt | 2 +- ...ToV14_Then_Verify_Request.request-0-v9.txt | 2 +- .../UserSession/Search/SearchTask.swift | 102 +++++++++--------- .../SearchUserViewController.swift | 2 +- 19 files changed, 69 insertions(+), 70 deletions(-) diff --git a/WireNetwork/Sources/WireNetwork/APIs/Rest/TeamsAPI/TeamsAPIV5.swift b/WireNetwork/Sources/WireNetwork/APIs/Rest/TeamsAPI/TeamsAPIV5.swift index 6f4ea19e58f..638edd4cf98 100644 --- a/WireNetwork/Sources/WireNetwork/APIs/Rest/TeamsAPI/TeamsAPIV5.swift +++ b/WireNetwork/Sources/WireNetwork/APIs/Rest/TeamsAPI/TeamsAPIV5.swift @@ -132,6 +132,7 @@ class TeamsAPIV5: TeamsAPIV4 { return .init(element: [], hasMore: false, nextStart: "") } + let prefix = prefix.replacingOccurrences(of: "+", with: "%2B") var requestBuilder = try URLRequestBuilder(path: path) .withMethod(.get) .withQueryItem(name: "prefix", value: prefix) diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/SearchAPITests.swift b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/SearchAPITests.swift index e7b93d810d8..16fe0ac1caf 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/SearchAPITests.swift +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/SearchAPITests.swift @@ -54,7 +54,7 @@ final class SearchAPITests: XCTestCase { // When try await apiSnapshotHelper.verifyRequest(for: [apiVersion], apiService: apiService) { sut in - let contacts = try await sut.searchContacts(query: "lorem", domain: "", type: .app).documents + let contacts = try await sut.searchContacts(query: "lorem ipsum", domain: "", type: .app).documents // Then XCTAssertEqual( @@ -104,7 +104,7 @@ final class SearchAPITests: XCTestCase { // When try await apiSnapshotHelper.verifyRequest(for: [apiVersion], apiService: apiService) { sut in - let contacts = try await sut.searchContacts(query: "lorem", domain: "", type: .app).documents + let contacts = try await sut.searchContacts(query: "lorem ipsum", domain: "", type: .app).documents // Then XCTAssertEqual( diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V15AndAbove_Then_Verify_Request.request-0-v15.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V15AndAbove_Then_Verify_Request.request-0-v15.txt index 15b8ff34aef..e76d960796a 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V15AndAbove_Then_Verify_Request.request-0-v15.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V15AndAbove_Then_Verify_Request.request-0-v15.txt @@ -1,2 +1,2 @@ curl \ - "/v15/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v15/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v1.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v1.txt index 93fb0c5e1f3..f31e86061d1 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v1.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v1.txt @@ -1,2 +1,2 @@ curl \ - "/v1/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v1/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v10.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v10.txt index a93d2470260..a2800018905 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v10.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v10.txt @@ -1,2 +1,2 @@ curl \ - "/v10/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v10/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v11.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v11.txt index cc7bfa96558..e66736bc6e0 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v11.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v11.txt @@ -1,2 +1,2 @@ curl \ - "/v11/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v11/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v12.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v12.txt index 31700959b85..773979cf4d7 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v12.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v12.txt @@ -1,2 +1,2 @@ curl \ - "/v12/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v12/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v13.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v13.txt index 3af46055707..16c392d6222 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v13.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v13.txt @@ -1,2 +1,2 @@ curl \ - "/v13/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v13/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v14.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v14.txt index 8694f0d82dc..04506edd4a8 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v14.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v14.txt @@ -1,2 +1,2 @@ curl \ - "/v14/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v14/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v2.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v2.txt index 238a87dfc36..062138da1c8 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v2.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v2.txt @@ -1,2 +1,2 @@ curl \ - "/v2/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v2/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v3.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v3.txt index d2b193d278e..cc512c3c5cc 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v3.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v3.txt @@ -1,2 +1,2 @@ curl \ - "/v3/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v3/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v4.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v4.txt index dc2128df749..4c5d4513ea9 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v4.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v4.txt @@ -1,2 +1,2 @@ curl \ - "/v4/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v4/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v5.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v5.txt index 97ef8d501d2..3915e9697c5 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v5.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v5.txt @@ -1,2 +1,2 @@ curl \ - "/v5/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v5/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v6.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v6.txt index 75494be62b5..5c2195497d0 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v6.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v6.txt @@ -1,2 +1,2 @@ curl \ - "/v6/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v6/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v7.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v7.txt index 67077bb28ff..9a1cc47786c 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v7.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v7.txt @@ -1,2 +1,2 @@ curl \ - "/v7/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v7/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v8.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v8.txt index 8ebec712193..61a8ec35a45 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v8.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v8.txt @@ -1,2 +1,2 @@ curl \ - "/v8/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v8/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v9.txt b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v9.txt index 798a3567dd2..e0458aa5ab6 100644 --- a/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v9.txt +++ b/WireNetwork/Tests/WireNetworkTests/APIs/Rest/Search/__Snapshots__/SearchAPITests/testSearchContacts_SuccessResponse_200_V1ToV14_Then_Verify_Request.request-0-v9.txt @@ -1,2 +1,2 @@ curl \ - "/v9/search/contacts?q=lorem&type=app" \ No newline at end of file + "/v9/search/contacts?q=lorem%20ipsum&type=app" \ No newline at end of file diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index a8775cad5e9..3cc96228a18 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -26,7 +26,7 @@ public class SearchTask { case lookup(qualifiedID: QualifiedID) } - public typealias ResultHandler = (_ result: SearchResult, _ isCompleted: Bool) -> Void + public typealias ResultHandler = (_ partialResult: SearchResult, _ isCompleted: Bool) -> Void private let apiVersion: WireTransport.APIVersion? private let transportSession: TransportSessionType @@ -41,7 +41,7 @@ public class SearchTask { private var handleTaskIdentifier: ZMTaskIdentifier? private var servicesTaskIdentifier: ZMTaskIdentifier? private var resultHandlers: [ResultHandler] = [] - private var result = SearchResult( + private var aggregatedResult = SearchResult( context: .init(concurrencyType: .privateQueueConcurrencyType), contacts: [], teamMembers: [], @@ -68,7 +68,7 @@ public class SearchTask { // only trigger handles if decrement to 0 if oldValue > newValue { let isCompleted = newValue == 0 - resultHandlers.forEach { $0(result, isCompleted) } + resultHandlers.forEach { $0(aggregatedResult, isCompleted) } if isCompleted { resultHandlers.removeAll() @@ -193,7 +193,7 @@ extension SearchTask { let copiedConnectedUsers = connectedUsers .compactMap { contextProvider.viewContext.object(with: $0.objectID) as? ZMUser } - let result = SearchResult( + let partialResult = SearchResult( context: contextProvider.viewContext, contacts: copiedConnectedUsers.map { ZMSearchUser( @@ -215,7 +215,8 @@ extension SearchTask { searchUsersCache: searchUsersCache ) - self.result = self.result.union(withLocalResult: result.copy(on: contextProvider.viewContext)) + aggregatedResult = aggregatedResult + .union(withLocalResult: partialResult.copy(on: contextProvider.viewContext)) tasksRemaining -= 1 } @@ -278,7 +279,7 @@ extension SearchTask { ) } - let result = SearchResult( + let partialResult = SearchResult( context: contextProvider.viewContext, contacts: searchConnectedUsers, teamMembers: searchTeamMembers, @@ -288,7 +289,8 @@ extension SearchTask { searchUsersCache: searchUsersCache ) - self.result = self.result.union(withLocalResult: result.copy(on: contextProvider.viewContext)) + aggregatedResult = aggregatedResult + .union(withLocalResult: partialResult.copy(on: contextProvider.viewContext)) tasksRemaining -= 1 } @@ -307,10 +309,10 @@ extension SearchTask { } func teamMembers(matchingQuery query: String, team: Team?, searchOptions: SearchOptions) -> [Member] { - var result = team?.members(matchingQuery: query) ?? [] + var partialResult = team?.members(matchingQuery: query) ?? [] if searchOptions.contains(.excludeNonActiveTeamMembers) { - result = filterNonActiveTeamMembers(members: result) + partialResult = filterNonActiveTeamMembers(members: partialResult) } if searchOptions.contains(.excludeNonActivePartners) { @@ -319,7 +321,7 @@ extension SearchTask { let activeConversations = ZMUser.selfUser(in: searchContext).activeConversations let activeContacts = Set(activeConversations.flatMap(\.localParticipants)) - result = result.filter { membership in + partialResult = partialResult.filter { membership in if let user = membership.user { user.teamRole != .partner || user.handle == query || membership .createdBy == selfUser || activeContacts.contains(user) @@ -329,7 +331,7 @@ extension SearchTask { } } - return result + return partialResult } func connectedUsers(matchingQuery query: String, hostedOnDomain: String?) -> [ZMUser] { @@ -397,18 +399,17 @@ extension SearchTask { } guard - let contextProvider = self?.contextProvider, + let self, let payload = response.payload?.asDictionary(), - let result = SearchResult( + let partialResult = SearchResult( userLookupPayload: payload, contextProvider: contextProvider, - searchUsersCache: self?.searchUsersCache + searchUsersCache: searchUsersCache ) else { return } - if let updatedResult = self?.result.union(withDirectoryResult: result) { - self?.result = updatedResult - } + let updatedResult = aggregatedResult.union(withDirectoryResult: partialResult) + aggregatedResult = updatedResult }) request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in @@ -453,26 +454,26 @@ extension SearchTask { let request = Self.searchRequestInDirectory(withRequest: searchRequest, apiVersion: apiVersion) request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in + guard let self else { return } guard - let contextProvider = self?.contextProvider, let payload = response.payload?.asDictionary(), - let result = SearchResult( + let partialResult = SearchResult( payload: payload, query: searchRequest.query, searchOptions: searchRequest.searchOptions, contextProvider: contextProvider, - searchUsersCache: self?.searchUsersCache + searchUsersCache: searchUsersCache ) else { - self?.completeRemoteSearch() + completeRemoteSearch() return } if searchRequest.searchOptions.contains(.teamMembers) { - self?.performTeamMembershipLookup(on: result, searchRequest: searchRequest) + performTeamMembershipLookup(on: partialResult, searchRequest: searchRequest) } else { - self?.completeRemoteSearch(searchResult: result) + completeRemoteSearch(searchResult: partialResult) } }) @@ -503,12 +504,13 @@ extension SearchTask { ) request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in + guard let self else { return } + guard - let contextProvider = self?.contextProvider, let rawData = response.rawData, let payload = MembershipListPayload(rawData) else { - self?.completeRemoteSearch() + completeRemoteSearch() return } @@ -520,7 +522,7 @@ extension SearchTask { contextProvider: contextProvider ) - self?.completeRemoteSearch(searchResult: updatedResult) + completeRemoteSearch(searchResult: updatedResult) }) @@ -537,7 +539,7 @@ extension SearchTask { } if let searchResult { - result = result.union(withDirectoryResult: searchResult) + aggregatedResult = aggregatedResult.union(withDirectoryResult: searchResult) } } @@ -612,7 +614,7 @@ extension SearchTask { } guard - let contextProvider = self?.contextProvider, + let self, let payload = response.payload?.asArray(), let userPayload = (payload.first as? ZMTransportData)?.asDictionary() else { @@ -629,32 +631,29 @@ extension SearchTask { let document = ["handle": handle, "name": name, "id": id] let documentPayload = ["documents": [document]] - guard let result = SearchResult( + guard let partialResult = SearchResult( payload: documentPayload, query: searchRequest.query, searchOptions: searchRequest.searchOptions, contextProvider: contextProvider, - searchUsersCache: self?.searchUsersCache + searchUsersCache: searchUsersCache ) else { return } - if let user = result.directory.first, !user.isSelfUser { - if let prevResult = self?.result { - // prepend result to prevResult only if it doesn't contain it - if !prevResult.directory.contains(user) { - self?.result = SearchResult( - context: prevResult.context, - contacts: prevResult.contacts, - teamMembers: prevResult.teamMembers, - directory: result.directory + prevResult.directory, - conversations: prevResult.conversations, - services: prevResult.services, - searchUsersCache: self?.searchUsersCache - ) - } - } else { - self?.result = result + 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 + ) } } }) @@ -711,21 +710,20 @@ extension SearchTask { } guard - let contextProvider = self?.contextProvider, + let self, let payload = response.payload?.asDictionary(), - let result = SearchResult( + let partialResult = SearchResult( servicesPayload: payload, query: searchRequest.query.string, contextProvider: contextProvider, - searchUsersCache: self?.searchUsersCache + searchUsersCache: searchUsersCache ) else { return } - if let updatedResult = self?.result.union(withServiceResult: result) { - self?.result = updatedResult - } + let updatedResult = aggregatedResult.union(withServiceResult: partialResult) + aggregatedResult = updatedResult }) request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/SearchUserViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/SearchUserViewController.swift index 7ba88339e99..8a809fa530c 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/SearchUserViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/SearchUserViewController.swift @@ -137,7 +137,7 @@ final class SearchUserViewController: UIViewController { navigationController?.setViewControllers([profileViewController], animated: true) resultHandled = true - } else if isCompleted { + } else { let alert = UIAlertController( title: L10n.Localizable.UrlAction.InvalidUser.title, message: L10n.Localizable.UrlAction.InvalidUser.message, From a05c7ed53e1513e9fc5b97492503da1d468d46b4 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 12:35:10 +0100 Subject: [PATCH 39/71] fix another test --- .../Source/UserSession/SearchTaskTests.swift | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift index 875a4f69bef..330bcc03751 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift @@ -122,26 +122,23 @@ 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() async throws { @@ -1406,6 +1403,7 @@ final class SearchTaskTests: DatabaseTest { task.start() XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) } + */ // MARK: - Helpers From 89357c222c6f904b000c19bf8d81f5d20872797d Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 13:07:46 +0100 Subject: [PATCH 40/71] fix some tests --- .../Source/UserSession/SearchTaskTests.swift | 768 ++++++++---------- 1 file changed, 318 insertions(+), 450 deletions(-) diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift index 330bcc03751..7b3f3ac82a2 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift @@ -57,34 +57,40 @@ 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) - - uiMOC.saveOrRollback() - - return conversation + 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) + + try uiMOC.save() + + return conversation + } } + // MARK: - + func testThatItFindsASingleUnconnectedUserByHandle() async throws { // given @@ -138,253 +144,216 @@ final class SearchTaskTests: DatabaseTest { // then XCTAssertEqual(result.directory.count, 0) } - /* + // MARK: Contacts Search 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 - 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 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 - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.contacts.count, 1) } 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 - 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 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 - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.contacts.compactMap(\.user), [user1]) } 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 - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.contacts.compactMap(\.user), [user1, user2]) } 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 - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.contacts.compactMap(\.user), [user1]) } 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 - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.contacts.compactMap(\.user), [user1]) } func testThatUserSearchOnlyReturnsConnectedUsers() async throws { // given - let resultArrived = customExpectation(description: "received result") - let user1 = createConnectedUser(withName: "Somebody Blocked") + let user1 = try await createConnectedUser(withName: "Somebody Blocked") user1.connection?.status = .blocked - let user2 = createConnectedUser(withName: "Somebody Pending") + let user2 = try await createConnectedUser(withName: "Somebody Pending") user2.connection?.status = .pending - let user3 = createConnectedUser(withName: "Somebody") + 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 - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.contacts.compactMap(\.user), [user3]) } 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") + 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 - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.contacts.compactMap(\.user), [user]) } func testThatItDoesNotFindUsersWithOtherDomainsIfSearchDomainIsRequired() 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", searchDomain: "anta.com", searchOptions: [.contacts]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.contacts.count, 0) - } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.contacts.count, 0) } 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 - 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 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 - 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)) } // MARK: Team member local search func testThatItCanSearchForTeamMembersLocally() async throws { // 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) @@ -399,20 +368,17 @@ final class SearchTaskTests: DatabaseTest { let request = SearchRequest(query: "@member", searchOptions: [.teamMembers], team: team) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.teamMembers.compactMap(\.user), [user]) - } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.teamMembers.compactMap(\.user), [user]) } func testThatItCanExcludeNonActiveTeamMembersLocally() async throws { // 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) @@ -441,20 +407,17 @@ final class SearchTaskTests: DatabaseTest { let request = SearchRequest(query: "", searchOptions: [.teamMembers, .excludeNonActiveTeamMembers], team: team) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA]) - } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA]) } func testThatItIncludesNonActiveTeamMembersLocally_WhenSelfUserWasCreatedByThem() async throws { // 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 @@ -475,20 +438,17 @@ final class SearchTaskTests: DatabaseTest { let request = SearchRequest(query: "", searchOptions: [.teamMembers, .excludeNonActiveTeamMembers], team: team) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA]) - } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA]) } func testThatItCanExcludeNonActivePartnersLocally() async throws { // 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) @@ -526,20 +486,17 @@ final class SearchTaskTests: DatabaseTest { 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, userB]) - } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA, userB]) } func testThatItIncludesNonActivePartnersLocally_WhenSearchingWithExactHandle() async throws { // 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 @@ -556,20 +513,17 @@ final class SearchTaskTests: DatabaseTest { let request = SearchRequest(query: "@abc", searchOptions: [.teamMembers, .excludeNonActivePartners], team: team) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA]) - } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA]) } func testThatItIncludesNonActivePartnersLocally_WhenSelfUserCreatedPartner() async throws { // 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 @@ -587,60 +541,51 @@ final class SearchTaskTests: DatabaseTest { 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 - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA]) } // MARK: Conversation Search func testThatItFindsASingleConversation() async throws { // given - let resultArrived = customExpectation(description: "received result") let conversation = 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 - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.conversations, [conversation]) } func testThatItDoesFindConversationsUsingPartialNames() async throws { // given - let resultArrived = customExpectation(description: "received result") let conversation = 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 - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.conversations, [conversation]) } 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") @@ -648,58 +593,49 @@ final class SearchTaskTests: DatabaseTest { 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 - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.conversations, [conversation1, conversation2]) } func testThatConversationSearchIsCaseInsensitive() async throws { // given - let resultArrived = customExpectation(description: "received result") let conversation = 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 - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.conversations, [conversation]) } func testThatConversationSearchIsInsensitiveToDiacritics() async throws { // given - let resultArrived = customExpectation(description: "received result") let conversation = 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 - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.conversations, [conversation]) } 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 @@ -711,26 +647,23 @@ final class SearchTaskTests: DatabaseTest { let request = SearchRequest(query: "Conversation", searchOptions: [.conversations]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.conversations, [groupConversation]) - } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.conversations, [groupConversation]) } 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 user1 = try await createConnectedUser(withName: "Shinji") + let user2 = try await createConnectedUser(withName: "Asuka") + let user3 = try await createConnectedUser(withName: "Rëï") conversation.addParticipantsAndUpdateConversationState(users: [user1, user2, user3], role: nil) @@ -739,23 +672,20 @@ final class SearchTaskTests: DatabaseTest { 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 - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.conversations, [conversation]) + XCTAssertEqual(result.contacts.compactMap(\.user), [user3]) } func testThatItFindsConversationsThatContainsSearchTermOnlyInParticipantName() async throws { // given - let resultArrived = customExpectation(description: "received result") let conversation = createGroupConversation(withName: "Summertime") - let user = createConnectedUser(withName: "Rëï") + let user = try await createConnectedUser(withName: "Rëï") conversation.addParticipantAndUpdateConversationState(user: user, role: nil) uiMOC.saveOrRollback() @@ -763,20 +693,17 @@ final class SearchTaskTests: DatabaseTest { let request = SearchRequest(query: "Rei", searchOptions: [.conversations]) let task = makeSearchTask(request: request) - // expect - task.addResultHandler { result, _ in - resultArrived.fulfill() - XCTAssertEqual(result.conversations, [conversation]) - } - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.conversations, [conversation]) } 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") @@ -784,22 +711,19 @@ final class SearchTaskTests: DatabaseTest { 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 - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.conversations, [conversation1, conversation3, conversation2]) } 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") @@ -814,40 +738,34 @@ final class SearchTaskTests: DatabaseTest { 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 - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.conversations, [conversation1, conversation3, conversation4]) } func testThatItFiltersConversationWhenTheQueryStartsWithAtSymbol() async throws { // given - let resultArrived = customExpectation(description: "received result") _ = createGroupConversation(withName: "New Day Rising") _ = 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 - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(result.conversations, []) } 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") @@ -859,16 +777,15 @@ final class SearchTaskTests: DatabaseTest { 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 - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + var result = SearchResult() + let resultAggregator = await task.performLocalSearch() + resultAggregator(&result) + + // then + XCTAssertEqual(Set(result.conversations), Set([conversationInTeam, conversationNotInTeam])) } + /* // MARK: Directory Search @@ -953,7 +870,6 @@ final class SearchTaskTests: DatabaseTest { 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) @@ -961,15 +877,13 @@ 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 @@ -1023,7 +937,6 @@ final class SearchTaskTests: DatabaseTest { 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) @@ -1038,16 +951,14 @@ 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 @@ -1085,7 +996,6 @@ final class SearchTaskTests: DatabaseTest { func testThatItCallsCompletionHandlerForServicesSearch() async throws { // given - let resultArrived = customExpectation(description: "received result") let request = SearchRequest(query: "Service", searchOptions: [.services]) let task = makeSearchTask(request: request) @@ -1097,15 +1007,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 { @@ -1167,8 +1075,6 @@ final class SearchTaskTests: DatabaseTest { 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") @@ -1176,15 +1082,13 @@ 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 @@ -1220,7 +1124,6 @@ final class SearchTaskTests: DatabaseTest { func testThatItCallsCompletionHandlerForFederatedUserSearch_WhenUserExists() async throws { // given let federatedDomain = "example.com" - let resultArrived = customExpectation(description: "received result") mockTransportSession.federatedDomains = [federatedDomain] mockTransportSession.performRemoteChanges { remoteChanges in @@ -1232,42 +1135,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() 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() 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") @@ -1276,34 +1173,26 @@ 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 localResult = SearchResult() + let localResultAggregator = await task.performLocalSearch() + localResultAggregator(&localResult) - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then - local result contains user + XCTAssertTrue(localResult.contacts.compactMap(\.user).contains(user)) - // given - let remoteResultArrived = customExpectation(description: "received remote result") - - // expect - task.addResultHandler { result, _ in - remoteResultArrived.fulfill() - XCTAssertTrue(result.contacts.compactMap(\.user).contains(user)) - } + // when - perform remote search + var remoteResult = SearchResult() + let remoteResultAggregator = await task.performRemoteSearch() + remoteResultAggregator(&remoteResult) - // when - task.performRemoteSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then - remote result still contains local user + XCTAssertTrue(remoteResult.contacts.compactMap(\.user).contains(user)) } 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") @@ -1312,52 +1201,40 @@ 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 remoteResult = SearchResult() + let remoteResultAggregator = await task.performRemoteSearch() + remoteResultAggregator(&remoteResult) - // when - task.performRemoteSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) - - // given - let localResultArrived = customExpectation(description: "received local result") + // then - remote result contains directory user + XCTAssertEqual(remoteResult.directory.count, 1) - // expect - task.addResultHandler { result, _ in - localResultArrived.fulfill() - XCTAssertEqual(result.directory.count, 1) - } + // when - perform local search + var localResult = SearchResult() + let localResultAggregator = await task.performLocalSearch() + localResultAggregator(&localResult) - // when - task.performLocalSearch() - XCTAssertTrue(waitForCustomExpectations(withTimeout: 0.5)) + // then - local result still contains directory user + XCTAssertEqual(localResult.directory.count, 1) } 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() async throws { // given - let remoteResultArrived = customExpectation(description: "received remote result") mockTransportSession.performRemoteChanges { remoteChanges in remoteChanges.insertUser(withName: "UserB") } @@ -1365,23 +1242,18 @@ 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) + + // then + XCTAssertEqual(result.directory.count, 1) } func testThatTaskIsCompletedOnlyAfterFinalResultArrives() async throws { // given - let intermediateResultArrived = customExpectation(description: "received intermediate result") - let finalResultsArrived = customExpectation(description: "received final result") - _ = createConnectedUser(withName: "userA") + _ = try await createConnectedUser(withName: "userA") mockTransportSession.performRemoteChanges { remoteChanges in remoteChanges.insertUser(withName: "UserB") @@ -1390,18 +1262,14 @@ final class SearchTaskTests: DatabaseTest { 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)) + var result = SearchResult() + let resultAggregator = await task.start() + resultAggregator(&result) + + // then - verify both local and remote results are present + XCTAssertEqual(result.contacts.count, 1) + XCTAssertEqual(result.directory.count, 1) } */ From b615da12026a07cf54c11e7fb90d9ee264f3b305 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 13:37:32 +0100 Subject: [PATCH 41/71] partially fix SearchTaskTests --- .../Source/UserSession/SearchTaskTests.swift | 456 +++++++++++------- 1 file changed, 278 insertions(+), 178 deletions(-) diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift index 7b3f3ac82a2..600c4604b12 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift @@ -82,9 +82,9 @@ final class SearchTaskTests: DatabaseTest { conversation.userDefinedName = name conversation.conversationType = .group conversation.addParticipantAndUpdateConversationState(user: selfUser, role: nil) - + try uiMOC.save() - + return conversation } } @@ -158,7 +158,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertTrue(result.contacts.compactMap(\.user).contains(user)) @@ -174,7 +176,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.contacts.count, 1) @@ -190,7 +194,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertTrue(result.contacts.compactMap(\.user).contains(user)) @@ -208,7 +214,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.contacts.compactMap(\.user), [user1]) @@ -226,7 +234,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.contacts.compactMap(\.user), [user1, user2]) @@ -242,7 +252,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.contacts.compactMap(\.user), [user1]) @@ -258,7 +270,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.contacts.compactMap(\.user), [user1]) @@ -267,9 +281,13 @@ final class SearchTaskTests: DatabaseTest { func testThatUserSearchOnlyReturnsConnectedUsers() async throws { // given let user1 = try await createConnectedUser(withName: "Somebody Blocked") - user1.connection?.status = .blocked + await uiMOC.perform { + user1.connection?.status = .blocked + } let user2 = try await createConnectedUser(withName: "Somebody Pending") - user2.connection?.status = .pending + await uiMOC.perform { + user2.connection?.status = .pending + } let user3 = try await createConnectedUser(withName: "Somebody") let request = SearchRequest(query: "Some", searchOptions: [.contacts]) @@ -278,7 +296,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.contacts.compactMap(\.user), [user3]) @@ -286,17 +306,21 @@ final class SearchTaskTests: DatabaseTest { func testThatItDoesNotReturnTheSelfUser() async throws { // given - let selfUser = ZMUser.selfUser(in: uiMOC) - selfUser.name = "Some self user" - let user = try await 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) + let request = SearchRequest(query: "Some", searchOptions: [.contacts]) + let task = makeSearchTask(request: request) // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.contacts.compactMap(\.user), [user]) @@ -304,7 +328,7 @@ final class SearchTaskTests: DatabaseTest { func testThatItDoesNotFindUsersWithOtherDomainsIfSearchDomainIsRequired() async throws { // given - let user = try await 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) @@ -312,7 +336,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.contacts.count, 0) @@ -328,7 +354,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertTrue(result.contacts.compactMap(\.user).contains(user)) @@ -344,7 +372,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertTrue(result.contacts.compactMap(\.user).contains(user)) @@ -353,176 +383,203 @@ final class SearchTaskTests: DatabaseTest { // MARK: Team member local search func testThatItCanSearchForTeamMembersLocally() async throws { - // given - let team = Team.insertNewObject(in: uiMOC) - let user = ZMUser.insertNewObject(in: uiMOC) - let member = Member.insertNewObject(in: uiMOC) - - user.name = "Member A" - - member.team = team - member.user = user - - uiMOC.saveOrRollback() - - let request = SearchRequest(query: "@member", searchOptions: [.teamMembers], team: team) - let task = makeSearchTask(request: request) + 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) + + user.name = "Member A" + + member.team = team + member.user = user + + uiMOC.saveOrRollback() + + let request = SearchRequest(query: "@member", searchOptions: [.teamMembers], team: team) + let task = makeSearchTask(request: request) + return (user, task) + } // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.teamMembers.compactMap(\.user), [user]) } func testThatItCanExcludeNonActiveTeamMembersLocally() async throws { - // 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 - ) + 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) - userA.name = "Member A" - userB.name = "Member B" + 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() + + let request = SearchRequest(query: "", searchOptions: [.teamMembers, .excludeNonActiveTeamMembers], team: team) + let task = makeSearchTask(request: request) + return (userA, task) + } // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA]) } func testThatItIncludesNonActiveTeamMembersLocally_WhenSelfUserWasCreatedByThem() async throws { - // 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) + 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) - userA.name = "Member A" - userA.handle = "abc" + userA.name = "Member A" + userA.handle = "abc" - selfUser.membership?.permissions = .partner - selfUser.membership?.createdBy = userA + selfUser.membership?.permissions = .partner + selfUser.membership?.createdBy = userA - memberA.team = team - memberA.user = userA - memberA.permissions = .admin + memberA.team = team + memberA.user = userA + memberA.permissions = .admin - uiMOC.saveOrRollback() + uiMOC.saveOrRollback() - let request = SearchRequest(query: "", searchOptions: [.teamMembers, .excludeNonActiveTeamMembers], team: team) - let task = makeSearchTask(request: request) + let request = SearchRequest(query: "", searchOptions: [.teamMembers, .excludeNonActiveTeamMembers], team: team) + let task = makeSearchTask(request: request) + return (userA, task) + } // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA]) } func testThatItCanExcludeNonActivePartnersLocally() async throws { - // 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 - ) + 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) - userA.name = "Member A" - userB.name = "Member B" - userC.name = "Member C" + 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() + + let request = SearchRequest(query: "", searchOptions: [.teamMembers, .excludeNonActivePartners], team: team) + let task = makeSearchTask(request: request) + return (userA, userB, task) + } // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA, userB]) } func testThatItIncludesNonActivePartnersLocally_WhenSearchingWithExactHandle() async throws { - // given - let team = Team.insertNewObject(in: uiMOC) - let userA = ZMUser.insertNewObject(in: uiMOC) - let memberA = Member.insertNewObject(in: uiMOC) // non-active partner + 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 - userA.name = "Member A" - userA.handle = "abc" + userA.name = "Member A" + userA.handle = "abc" - memberA.team = team - memberA.user = userA - memberA.permissions = .partner + memberA.team = team + memberA.user = userA + memberA.permissions = .partner - uiMOC.saveOrRollback() + uiMOC.saveOrRollback() - let request = SearchRequest(query: "@abc", searchOptions: [.teamMembers, .excludeNonActivePartners], team: team) - let task = makeSearchTask(request: request) + let searchOptions: SearchOptions = [.teamMembers, .excludeNonActivePartners] + let request = SearchRequest(query: "@abc", searchOptions: searchOptions, team: team) + let task = makeSearchTask(request: request) + return (userA, task) + } // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA]) } 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) @@ -536,7 +593,10 @@ final class SearchTaskTests: DatabaseTest { memberA.permissions = .partner memberA.createdBy = ZMUser.selfUser(in: uiMOC) - uiMOC.saveOrRollback() + try uiMOC.save() + + return (userA, team) + } let request = SearchRequest(query: "", searchOptions: [.teamMembers, .excludeNonActivePartners], team: team) let task = makeSearchTask(request: request) @@ -544,7 +604,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.teamMembers.compactMap(\.user), [userA]) @@ -554,7 +616,7 @@ final class SearchTaskTests: DatabaseTest { func testThatItFindsASingleConversation() async throws { // given - let conversation = createGroupConversation(withName: "Somebody") + let conversation = try await createGroupConversation(withName: "Somebody") let request = SearchRequest(query: "Somebody", searchOptions: [.conversations]) let task = makeSearchTask(request: request) @@ -562,7 +624,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.conversations, [conversation]) @@ -570,7 +634,7 @@ final class SearchTaskTests: DatabaseTest { func testThatItDoesFindConversationsUsingPartialNames() async throws { // given - let conversation = createGroupConversation(withName: "Somebody") + let conversation = try await createGroupConversation(withName: "Somebody") let request = SearchRequest(query: "mebo", searchOptions: [.conversations]) let task = makeSearchTask(request: request) @@ -578,7 +642,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.conversations, [conversation]) @@ -586,9 +652,9 @@ final class SearchTaskTests: DatabaseTest { func testThatItFindsSeveralConversations() async throws { // given - 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) @@ -596,7 +662,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.conversations, [conversation1, conversation2]) @@ -604,7 +672,7 @@ final class SearchTaskTests: DatabaseTest { func testThatConversationSearchIsCaseInsensitive() async throws { // given - let conversation = createGroupConversation(withName: "SoMEBody") + let conversation = try await createGroupConversation(withName: "SoMEBody") let request = SearchRequest(query: "someBodY", searchOptions: [.conversations]) let task = makeSearchTask(request: request) @@ -612,15 +680,17 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) - + await uiMOC.perform { + resultAggregator(&result) + } + // then XCTAssertEqual(result.conversations, [conversation]) } func testThatConversationSearchIsInsensitiveToDiacritics() async throws { // given - 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) @@ -628,7 +698,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.conversations, [conversation]) @@ -636,13 +708,17 @@ final class SearchTaskTests: DatabaseTest { func testThatItOnlyFindsGroupConversations() async throws { // given - 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) @@ -650,7 +726,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.conversations, [groupConversation]) @@ -658,16 +736,20 @@ final class SearchTaskTests: DatabaseTest { func testThatItFindsConversationsThatDoNotHaveAUserDefinedName() async throws { // given - let conversation = ZMConversation.insertNewObject(in: uiMOC) - conversation.conversationType = .group + let conversation = await uiMOC.perform { [uiMOC] in + let conversation = ZMConversation.insertNewObject(in: uiMOC) + conversation.conversationType = .group + return conversation + } let user1 = try await createConnectedUser(withName: "Shinji") let user2 = try await createConnectedUser(withName: "Asuka") let user3 = try await createConnectedUser(withName: "Rëï") - conversation.addParticipantsAndUpdateConversationState(users: [user1, user2, user3], role: nil) - - 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) @@ -675,7 +757,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.conversations, [conversation]) @@ -684,11 +768,12 @@ final class SearchTaskTests: DatabaseTest { func testThatItFindsConversationsThatContainsSearchTermOnlyInParticipantName() async throws { // given - let conversation = createGroupConversation(withName: "Summertime") + let conversation = try await createGroupConversation(withName: "Summertime") let user = try await createConnectedUser(withName: "Rëï") - conversation.addParticipantAndUpdateConversationState(user: user, role: nil) - - uiMOC.saveOrRollback() + 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) @@ -696,7 +781,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.conversations, [conversation]) @@ -704,9 +791,9 @@ final class SearchTaskTests: DatabaseTest { func testThatItOrdersConversationsByUserDefinedName() async throws { // given - 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) @@ -714,7 +801,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.conversations, [conversation1, conversation3, conversation2]) @@ -725,15 +814,17 @@ final class SearchTaskTests: DatabaseTest { 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) - - uiMOC.saveOrRollback() + try await uiMOC.perform { [uiMOC] in + conversation2.addParticipantAndUpdateConversationState(user: user1, role: nil) + conversation4.addParticipantsAndUpdateConversationState(users: [user1, user2], role: nil) + + try uiMOC.save() + } let request = SearchRequest(query: "Foo", searchOptions: [.conversations]) let task = makeSearchTask(request: request) @@ -741,7 +832,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.conversations, [conversation1, conversation3, conversation4]) @@ -749,8 +842,8 @@ final class SearchTaskTests: DatabaseTest { func testThatItFiltersConversationWhenTheQueryStartsWithAtSymbol() async throws { // given - _ = 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) @@ -758,7 +851,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(result.conversations, []) @@ -766,13 +861,16 @@ final class SearchTaskTests: DatabaseTest { func testThatItReturnsAllConversationsWhenPassingTeamParameter() async throws { // given - 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) @@ -780,7 +878,9 @@ final class SearchTaskTests: DatabaseTest { // when var result = SearchResult() let resultAggregator = await task.performLocalSearch() - resultAggregator(&result) + await uiMOC.perform { + resultAggregator(&result) + } // then XCTAssertEqual(Set(result.conversations), Set([conversationInTeam, conversationNotInTeam])) From ebca3314ab263c59009e8ee3187971abd0b4536a Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 13:46:04 +0100 Subject: [PATCH 42/71] uncomment code --- .../Source/UserSession/SearchTaskTests.swift | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift index 600c4604b12..b863497dc8b 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift @@ -885,7 +885,6 @@ final class SearchTaskTests: DatabaseTest { // then XCTAssertEqual(Set(result.conversations), Set([conversationInTeam, conversationNotInTeam])) } - /* // MARK: Directory Search @@ -895,7 +894,7 @@ final class SearchTaskTests: DatabaseTest { let task = makeSearchTask(request: request, apiVersion: .v2) // when - task.performRemoteSearch() + _ = await task.performRemoteSearch() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then @@ -911,7 +910,7 @@ final class SearchTaskTests: DatabaseTest { let task = makeSearchTask(request: request) // when - task.performRemoteSearch() + _ = await task.performRemoteSearch() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then @@ -924,7 +923,7 @@ final class SearchTaskTests: DatabaseTest { let task = makeSearchTask(request: request) // when - task.performRemoteSearch() + _ = await task.performRemoteSearch() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then @@ -937,7 +936,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,7 +957,7 @@ final class SearchTaskTests: DatabaseTest { let task = makeSearchTask(request: request, apiVersion: .v2) // when - task.performRemoteSearch() + _ = await task.performRemoteSearch() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then @@ -1002,7 +1001,7 @@ final class SearchTaskTests: DatabaseTest { } // when - task.performRemoteSearch() + _ = await task.performRemoteSearch() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then @@ -1028,7 +1027,7 @@ final class SearchTaskTests: DatabaseTest { } // when - task.performRemoteSearch() + _ = await task.performRemoteSearch() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then @@ -1069,7 +1068,7 @@ final class SearchTaskTests: DatabaseTest { 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)) @@ -1087,7 +1086,7 @@ final class SearchTaskTests: DatabaseTest { let task = makeSearchTask(request: request) // when - task.performRemoteSearchForServices() + _ = await task.performRemoteSearchForServices() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then @@ -1149,7 +1148,7 @@ final class SearchTaskTests: DatabaseTest { let task = makeSearchTask(lookupUserId: userId) // when - task.performUserLookup() + _ = await task.performUserLookup() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then @@ -1163,7 +1162,7 @@ final class SearchTaskTests: DatabaseTest { let task = makeSearchTask(lookupUserId: userId, domain: domain, apiVersion: .v2) // when - task.performUserLookup() + _ = await task.performUserLookup() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then @@ -1199,7 +1198,7 @@ final class SearchTaskTests: DatabaseTest { let task = makeSearchTask(request: searchRequest, apiVersion: .v3) // when - task.performRemoteSearch() + _ = await task.performRemoteSearch() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then @@ -1212,7 +1211,7 @@ final class SearchTaskTests: DatabaseTest { let task = makeSearchTask(request: searchRequest, apiVersion: .v3) // when - task.performRemoteSearch() + _ = await task.performRemoteSearch() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then @@ -1363,15 +1362,12 @@ final class SearchTaskTests: DatabaseTest { let task = makeSearchTask(request: request) // when - var result = SearchResult() - let resultAggregator = await task.start() - resultAggregator(&result) - + let result = await task.start() + // then - verify both local and remote results are present XCTAssertEqual(result.contacts.count, 1) XCTAssertEqual(result.directory.count, 1) } - */ // MARK: - Helpers From 9ad2748a3aa1eb806c8864b95124536785e8c809 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 13:50:46 +0100 Subject: [PATCH 43/71] fix crashes --- .../Source/UserSession/Search/SearchTask.swift | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index c4777a998f0..1d4e8bbecb6 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -81,7 +81,10 @@ public final class SearchTask { status = .running defer { status = .completed } - return await withTaskGroup(of: SearchResultAggregator.self, returning: SearchResult.self) { taskGroup in + return await withTaskGroup( + of: SearchResultAggregator.self, + returning: SearchResult.self + ) { @MainActor taskGroup in // search services taskGroup.addTask { @@ -459,11 +462,16 @@ extension SearchTask { searchRequest: SearchRequest ) async -> SearchResultAggregator { - let teamMembersIDs = searchResult.teamMembers.compactMap(\.remoteIdentifier) + 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 { return { $0 = $0.union(withDirectoryResult: searchResult) } @@ -478,8 +486,9 @@ extension SearchTask { return await withCheckedContinuation { continuation in request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in + guard - let contextProvider = self?.contextProvider, + let self, let rawData = response.rawData, let payload = MembershipListPayload(rawData) else { return continuation.resume(returning: { _ in }) } From eae2185977c011738195e845b16b1b86fda792ca Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 13:59:57 +0100 Subject: [PATCH 44/71] fix more tests --- .../Source/UserSession/SearchTaskTests.swift | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift index b863497dc8b..7d1580d1a19 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift @@ -1273,20 +1273,19 @@ final class SearchTaskTests: DatabaseTest { let task = makeSearchTask(request: request, apiVersion: .v2) // when - perform local search - var localResult = SearchResult() + var result = SearchResult() let localResultAggregator = await task.performLocalSearch() - localResultAggregator(&localResult) + localResultAggregator(&result) // then - local result contains user - XCTAssertTrue(localResult.contacts.compactMap(\.user).contains(user)) + XCTAssertTrue(result.contacts.compactMap(\.user).contains(user)) // when - perform remote search - var remoteResult = SearchResult() let remoteResultAggregator = await task.performRemoteSearch() - remoteResultAggregator(&remoteResult) + remoteResultAggregator(&result) // then - remote result still contains local user - XCTAssertTrue(remoteResult.contacts.compactMap(\.user).contains(user)) + XCTAssertTrue(result.contacts.compactMap(\.user).contains(user)) } func testThatLocalResultsIncludePreviousRemoteResults() async throws { @@ -1301,20 +1300,19 @@ final class SearchTaskTests: DatabaseTest { let task = makeSearchTask(request: request, apiVersion: .v2) // when - perform remote search - var remoteResult = SearchResult() + var result = SearchResult() let remoteResultAggregator = await task.performRemoteSearch() - remoteResultAggregator(&remoteResult) + remoteResultAggregator(&result) // then - remote result contains directory user - XCTAssertEqual(remoteResult.directory.count, 1) + XCTAssertEqual(result.directory.count, 1) // when - perform local search - var localResult = SearchResult() let localResultAggregator = await task.performLocalSearch() - localResultAggregator(&localResult) + localResultAggregator(&result) // then - local result still contains directory user - XCTAssertEqual(localResult.directory.count, 1) + XCTAssertEqual(result.directory.count, 1) } func testThatTaskIsCompletedAfterLocalResult() async throws { From 70ea913da8bbc89a5d41697ef05a677f793db725 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 14:14:38 +0100 Subject: [PATCH 45/71] format code --- .../UserSession/Search/SearchDirectory.swift | 4 +- .../UserSession/Search/SearchResult.swift | 2 +- .../UserSession/Search/SearchTask.swift | 19 ++--- .../Source/UserSession/SearchTaskTests.swift | 77 ++++++++----------- 4 files changed, 46 insertions(+), 56 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift index f6e157a5556..c167a1a29d9 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift @@ -92,13 +92,13 @@ public final class SearchDirectory { } } -extension SearchDirectory { +public extension SearchDirectory { /// Tear down the SearchDirectory. /// /// NOTE: this must be called before releasing the instance - public func tearDown() { + func tearDown() { let tearDown = { [self] in // Evict all cached search users searchUsersCache?.removeAllObjects() diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchResult.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchResult.swift index 2ac2a2365f2..02d32b479a9 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchResult.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchResult.swift @@ -59,7 +59,7 @@ extension SearchResult { self.directory = [] self.conversations = [] self.services = [] - searchUsersCache = nil + self.searchUsersCache = nil } public init?( diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 1d4e8bbecb6..c229d6e1006 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -42,7 +42,7 @@ public final class SearchTask { /// - union(withLocalResult:) /// - union(withServiceResult:) /// - union(withDirectoryResult:) - /*private*/ typealias SearchResultAggregator = (inout SearchResult) -> Void // TODO: make private + /* private */ typealias SearchResultAggregator = (inout SearchResult) -> Void // TODO: make private private let apiVersion: WireTransport.APIVersion? private let transportSession: TransportSessionType @@ -52,7 +52,7 @@ public final class SearchTask { private let type: `Type` init( - type: `Type`, + type: Type, contextProvider: ContextProvider, transportSession: TransportSessionType, searchUsersCache: SearchUsersCache?, @@ -191,7 +191,7 @@ extension SearchTask { } - /*private*/ func performLocalSearch() async -> SearchResultAggregator { // TODO: make private + /* private */ func performLocalSearch() async -> SearchResultAggregator { // TODO: make private guard case let .search(request) = type else { return { _ in } } @@ -336,7 +336,8 @@ extension SearchTask { )) fetchRequest.sortDescriptors = [NSSortDescriptor(key: ZMNormalizedUserDefinedNameKey, ascending: true)] - var conversations = contextProvider.searchContext.fetchOrAssert(request: fetchRequest) as? [ZMConversation] ?? [] + var conversations = contextProvider.searchContext + .fetchOrAssert(request: fetchRequest) as? [ZMConversation] ?? [] if query.isHandleQuery { // if we are searching for a username only include conversations with matching displayName @@ -363,7 +364,7 @@ extension SearchTask { extension SearchTask { - /*private*/ func performUserLookup() async -> SearchResultAggregator { // TODO: make private + /* private */ func performUserLookup() async -> SearchResultAggregator { // TODO: make private guard case let .lookup(qualifiedID) = type, let apiVersion @@ -408,7 +409,7 @@ extension SearchTask { extension SearchTask { - /*private*/ func performRemoteSearch() async -> SearchResultAggregator { // TODO: make private + /* private */ func performRemoteSearch() async -> SearchResultAggregator { // TODO: make private guard let apiVersion, apiVersion >= .v1, @@ -558,7 +559,7 @@ extension SearchTask { extension SearchTask { - /*private*/ func performRemoteSearchForTeamUser() async -> SearchResultAggregator { // TODO: make private + /* private */ func performRemoteSearchForTeamUser() async -> SearchResultAggregator { // TODO: make private guard let apiVersion, apiVersion <= .v1, @@ -649,7 +650,7 @@ extension SearchTask { extension SearchTask { - /*private*/ func performRemoteSearchForServices() async -> SearchResultAggregator { // TODO: create subtask struct + /* private */ func performRemoteSearchForServices() async -> SearchResultAggregator { // TODO: create subtask struct let searchContext = contextProvider.searchContext let teamIdentifier = await searchContext.perform { @@ -694,7 +695,7 @@ extension SearchTask { } } - /*private*/ static func servicesSearchRequest( // TODO: make private + /* private */ static func servicesSearchRequest( // TODO: make private teamIdentifier: UUID, query: String, apiVersion: APIVersion diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift index 7d1580d1a19..91363afe1a1 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift @@ -16,9 +16,9 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // -import XCTest import WireMockTransport import WireTransport +import XCTest @testable import WireSyncEngine @@ -310,10 +310,10 @@ final class SearchTaskTests: DatabaseTest { let selfUser = ZMUser.selfUser(in: uiMOC) selfUser.name = "Some self user" } - let user = try await createConnectedUser(withName: "Somebody") + let user = try await createConnectedUser(withName: "Somebody") - let request = SearchRequest(query: "Some", searchOptions: [.contacts]) - let task = makeSearchTask(request: request) + let request = SearchRequest(query: "Some", searchOptions: [.contacts]) + let task = makeSearchTask(request: request) // when var result = SearchResult() @@ -388,14 +388,14 @@ final class SearchTaskTests: DatabaseTest { let team = Team.insertNewObject(in: uiMOC) let user = ZMUser.insertNewObject(in: uiMOC) let member = Member.insertNewObject(in: uiMOC) - + user.name = "Member A" - + member.team = team member.user = user - + uiMOC.saveOrRollback() - + let request = SearchRequest(query: "@member", searchOptions: [.teamMembers], team: team) let task = makeSearchTask(request: request) return (user, task) @@ -440,7 +440,11 @@ final class SearchTaskTests: DatabaseTest { uiMOC.saveOrRollback() - let request = SearchRequest(query: "", searchOptions: [.teamMembers, .excludeNonActiveTeamMembers], team: team) + let request = SearchRequest( + query: "", + searchOptions: [.teamMembers, .excludeNonActiveTeamMembers], + team: team + ) let task = makeSearchTask(request: request) return (userA, task) } @@ -476,7 +480,11 @@ final class SearchTaskTests: DatabaseTest { uiMOC.saveOrRollback() - let request = SearchRequest(query: "", searchOptions: [.teamMembers, .excludeNonActiveTeamMembers], team: team) + let request = SearchRequest( + query: "", + searchOptions: [.teamMembers, .excludeNonActiveTeamMembers], + team: team + ) let task = makeSearchTask(request: request) return (userA, task) } @@ -580,23 +588,23 @@ final class SearchTaskTests: DatabaseTest { 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 + // 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() + try uiMOC.save() return (userA, team) - } + } let request = SearchRequest(query: "", searchOptions: [.teamMembers, .excludeNonActivePartners], team: team) let task = makeSearchTask(request: request) @@ -683,7 +691,7 @@ final class SearchTaskTests: DatabaseTest { await uiMOC.perform { resultAggregator(&result) } - + // then XCTAssertEqual(result.conversations, [conversation]) } @@ -822,7 +830,7 @@ final class SearchTaskTests: DatabaseTest { try await uiMOC.perform { [uiMOC] in conversation2.addParticipantAndUpdateConversationState(user: user1, role: nil) conversation4.addParticipantsAndUpdateConversationState(users: [user1, user2], role: nil) - + try uiMOC.save() } @@ -1086,7 +1094,7 @@ final class SearchTaskTests: DatabaseTest { let task = makeSearchTask(request: request) // when - _ = await task.performRemoteSearchForServices() + _ = await task.performRemoteSearchForServices() XCTAssertTrue(waitForAllGroupsToBeEmpty(withTimeout: 0.5)) // then @@ -1348,25 +1356,6 @@ final class SearchTaskTests: DatabaseTest { XCTAssertEqual(result.directory.count, 1) } - func testThatTaskIsCompletedOnlyAfterFinalResultArrives() async throws { - // given - _ = try await createConnectedUser(withName: "userA") - - mockTransportSession.performRemoteChanges { remoteChanges in - remoteChanges.insertUser(withName: "UserB") - } - - let request = SearchRequest(query: "user", searchOptions: [.contacts, .directory]) - let task = makeSearchTask(request: request) - - // when - let result = await task.start() - - // then - verify both local and remote results are present - XCTAssertEqual(result.contacts.count, 1) - XCTAssertEqual(result.directory.count, 1) - } - // MARK: - Helpers private func makeSearchTask( From 05ed094bdaf46a341d63926802ceacf737141fb4 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 15:09:04 +0100 Subject: [PATCH 46/71] delete ContextProvider.searchContext --- .../ManagedObjectContext/CoreDataStack.swift | 1 - .../generated/AutoMockable.generated.swift | 9 --------- .../UserSession/Search/SearchTask.swift | 19 +++++++++++-------- 3 files changed, 11 insertions(+), 18 deletions(-) diff --git a/wire-ios-data-model/Source/ManagedObjectContext/CoreDataStack.swift b/wire-ios-data-model/Source/ManagedObjectContext/CoreDataStack.swift index d6d2bf5e19b..6bc6740d693 100644 --- a/wire-ios-data-model/Source/ManagedObjectContext/CoreDataStack.swift +++ b/wire-ios-data-model/Source/ManagedObjectContext/CoreDataStack.swift @@ -48,7 +48,6 @@ public protocol ContextProvider { var viewContext: NSManagedObjectContext { get } func newBackgroundContext() -> NSManagedObjectContext var syncContext: NSManagedObjectContext { get } - var searchContext: NSManagedObjectContext { get } // TODO: delete var eventContext: NSManagedObjectContext { get } } diff --git a/wire-ios-data-model/Support/Sourcery/generated/AutoMockable.generated.swift b/wire-ios-data-model/Support/Sourcery/generated/AutoMockable.generated.swift index 7a3a6d13cbd..3c6213be215 100644 --- a/wire-ios-data-model/Support/Sourcery/generated/AutoMockable.generated.swift +++ b/wire-ios-data-model/Support/Sourcery/generated/AutoMockable.generated.swift @@ -2137,15 +2137,6 @@ public class MockCoreDataStackProtocol: CoreDataStackProtocol { public var underlyingSyncContext: NSManagedObjectContext! - // MARK: - searchContext - - public var searchContext: NSManagedObjectContext { - get { return underlyingSearchContext } - set(value) { underlyingSearchContext = value } - } - - public var underlyingSearchContext: NSManagedObjectContext! - // MARK: - eventContext public var eventContext: NSManagedObjectContext { diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index c229d6e1006..74c706f00d5 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -42,7 +42,7 @@ public final class SearchTask { /// - union(withLocalResult:) /// - union(withServiceResult:) /// - union(withDirectoryResult:) - /* private */ typealias SearchResultAggregator = (inout SearchResult) -> Void // TODO: make private + typealias SearchResultAggregator = (inout SearchResult) -> Void private let apiVersion: WireTransport.APIVersion? private let transportSession: TransportSessionType @@ -196,7 +196,7 @@ extension SearchTask { return { _ in } } - let searchContext = contextProvider.searchContext + let searchContext = contextProvider.newBackgroundContext() let (connectedUserIDs, teamMemberIDs, conversationIDs) = await searchContext.perform { [self] in var team: Team? @@ -274,9 +274,10 @@ extension SearchTask { } private func filterNonActiveTeamMembers(members: [Member]) -> [Member] { - let activeConversations = ZMUser.selfUser(in: contextProvider.searchContext).activeConversations + let searchContext = contextProvider.newBackgroundContext() + let activeConversations = ZMUser.selfUser(in: searchContext).activeConversations let activeContacts = Set(activeConversations.flatMap(\.localParticipants)) - let selfUser = ZMUser.selfUser(in: contextProvider.searchContext) + let selfUser = ZMUser.selfUser(in: searchContext) return members.filter { guard let user = $0.user else { return false } @@ -293,8 +294,9 @@ extension SearchTask { if searchOptions.contains(.excludeNonActivePartners) { let query = query.strippingLeadingAtSign() - let selfUser = ZMUser.selfUser(in: contextProvider.searchContext) - let activeConversations = ZMUser.selfUser(in: contextProvider.searchContext).activeConversations + let searchContext = contextProvider.newBackgroundContext() // TODO: which context to work on actually? test also manually + let selfUser = ZMUser.selfUser(in: searchContext) + let activeConversations = ZMUser.selfUser(in: searchContext).activeConversations let activeContacts = Set(activeConversations.flatMap(\.localParticipants)) partialResult = partialResult.filter { membership in @@ -336,7 +338,8 @@ extension SearchTask { )) fetchRequest.sortDescriptors = [NSSortDescriptor(key: ZMNormalizedUserDefinedNameKey, ascending: true)] - var conversations = contextProvider.searchContext + let searchContext = contextProvider.newBackgroundContext() + var conversations = searchContext .fetchOrAssert(request: fetchRequest) as? [ZMConversation] ?? [] if query.isHandleQuery { @@ -652,7 +655,7 @@ extension SearchTask { /* private */ func performRemoteSearchForServices() async -> SearchResultAggregator { // TODO: create subtask struct - let searchContext = contextProvider.searchContext + let searchContext = contextProvider.newBackgroundContext() let teamIdentifier = await searchContext.perform { ZMUser.selfUser(in: searchContext).team?.remoteIdentifier } From 45ac179991f27626202a2cba33d3aecb5cfc86d9 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 15:52:17 +0100 Subject: [PATCH 47/71] fix build errors in WireSyncEngine --- .../ManagedObjectContext/CoreDataStack.swift | 18 --------------- .../NSManagedObjectContext+tests.h | 1 - .../NSManagedObjectContext+zmessaging.h | 4 ---- .../NSManagedObjectContext+zmessaging.m | 16 +------------- .../generated/AutoMockable.generated.swift | 9 -------- .../Source/Model/ZMBaseManagedObjectTest.m | 5 ----- .../SessionManager/UserSessionLoader.swift | 3 +-- .../UserSession/Search/SearchDirectory.swift | 6 ----- .../UserSession/Search/SearchTask.swift | 22 ++++++++++++------- .../ZMUserSession/ZMUserSession+Logs.swift | 16 +++++--------- .../ZMUserSession/ZMUserSession.swift | 9 -------- .../ZMUserSession/ZMUserSessionBuilder.swift | 3 +-- .../Tests/Source/DatabaseTest.swift | 4 ---- .../Tests/Source/MessagingTest.m | 5 ----- .../UserSession/SearchDirectoryTests.swift | 1 - .../Source/UserSession/SearchTaskTests.swift | 2 -- ...anagerEncryptionAtRestMigrationTests.swift | 3 +-- .../ZMUserSessionTests+EncryptionAtRest.swift | 3 +-- wire-ios/Tests/Mocks/UserSessionMock.swift | 1 - .../MockContextProvider.swift | 4 ---- 20 files changed, 25 insertions(+), 110 deletions(-) diff --git a/wire-ios-data-model/Source/ManagedObjectContext/CoreDataStack.swift b/wire-ios-data-model/Source/ManagedObjectContext/CoreDataStack.swift index 6d3876e4d76..f04e720ab64 100644 --- a/wire-ios-data-model/Source/ManagedObjectContext/CoreDataStack.swift +++ b/wire-ios-data-model/Source/ManagedObjectContext/CoreDataStack.swift @@ -48,7 +48,6 @@ public protocol ContextProvider { var viewContext: NSManagedObjectContext { get } func newBackgroundContext() -> NSManagedObjectContext var syncContext: NSManagedObjectContext { get } - var searchContext: NSManagedObjectContext { get } var eventContext: NSManagedObjectContext { get } } @@ -138,8 +137,6 @@ public final class CoreDataStack: NSObject, CoreDataStackProtocol, ContextProvid return context }() - public lazy var searchContext: NSManagedObjectContext = messagesContainer.newBackgroundContext() - public lazy var eventContext: NSManagedObjectContext = eventsContainer.newBackgroundContext() public let accountContainer: URL @@ -253,7 +250,6 @@ public final class CoreDataStack: NSObject, CoreDataStackProtocol, ContextProvid viewContext.tearDown() syncContext.tearDown() - searchContext.tearDown() eventContext.tearDown() closeStores() } @@ -311,7 +307,6 @@ public final class CoreDataStack: NSObject, CoreDataStackProtocol, ContextProvid await configureContextReferences() await configureViewContext(viewContext) await configureSyncContext(syncContext) - await configureSearchContext(searchContext) } catch { WireLogger.localStorage.critical( @@ -413,19 +408,6 @@ public final class CoreDataStack: NSObject, CoreDataStackProtocol, ContextProvid } } - func configureSearchContext(_ context: NSManagedObjectContext) async { - context.markAsSearch() - await context.perform { - context.localDomain = self.localDomain - context.isFederationEnabled = self.isFederationEnabled - context.createDispatchGroups() - self.dispatchGroup.map(context.addGroup(_:)) - context.setupLocalCachedSessionAndSelfUser() - context.undoManager = nil - context.mergePolicy = NSMergePolicy(merge: .rollbackMergePolicyType) - } - } - func configureEventContext(_ context: NSManagedObjectContext) async { await context.perform { context.createDispatchGroups() diff --git a/wire-ios-data-model/Source/ManagedObjectContext/NSManagedObjectContext+tests.h b/wire-ios-data-model/Source/ManagedObjectContext/NSManagedObjectContext+tests.h index b6145c6acea..c67ea7adeca 100644 --- a/wire-ios-data-model/Source/ManagedObjectContext/NSManagedObjectContext+tests.h +++ b/wire-ios-data-model/Source/ManagedObjectContext/NSManagedObjectContext+tests.h @@ -22,7 +22,6 @@ @interface NSManagedObjectContext (zmessagingTests) - (void)markAsSyncContext; -- (void)markAsSearchContext; - (void)markAsUIContext; - (void)disableObjectRefresh; diff --git a/wire-ios-data-model/Source/ManagedObjectContext/NSManagedObjectContext+zmessaging.h b/wire-ios-data-model/Source/ManagedObjectContext/NSManagedObjectContext+zmessaging.h index 2831cf81dfe..660a95c549a 100644 --- a/wire-ios-data-model/Source/ManagedObjectContext/NSManagedObjectContext+zmessaging.h +++ b/wire-ios-data-model/Source/ManagedObjectContext/NSManagedObjectContext+zmessaging.h @@ -25,7 +25,6 @@ extern NSString * _Nonnull const IsUserInterfaceContextKey; extern NSString * _Nonnull const IsSyncContextKey; -extern NSString * _Nonnull const IsSearchContextKey; extern NSString * _Nonnull const IsEventContextKey; @interface NSManagedObjectContext (zmessaging) @@ -37,9 +36,6 @@ extern NSString * _Nonnull const IsEventContextKey; /// Inverse of @c zm_isSyncContext @property (readonly) BOOL zm_isUserInterfaceContext; -/// Returns @c YES if the receiver is a context that is used for searching. -@property (readonly) BOOL zm_isSearchContext; - /// Returns @c YES if the context should refresh objects following the policy for the sync context @property (readonly) BOOL zm_shouldRefreshObjectsWithSyncContextPolicy; diff --git a/wire-ios-data-model/Source/ManagedObjectContext/NSManagedObjectContext+zmessaging.m b/wire-ios-data-model/Source/ManagedObjectContext/NSManagedObjectContext+zmessaging.m index 83afe8cbfa1..f98b697a473 100644 --- a/wire-ios-data-model/Source/ManagedObjectContext/NSManagedObjectContext+zmessaging.m +++ b/wire-ios-data-model/Source/ManagedObjectContext/NSManagedObjectContext+zmessaging.m @@ -31,7 +31,6 @@ #import NSString * const IsSyncContextKey = @"ZMIsSyncContext"; -NSString * const IsSearchContextKey = @"ZMIsSearchContext"; NSString * const IsUserInterfaceContextKey = @"ZMIsUserInterfaceContext"; NSString * const IsEventContextKey = @"ZMIsEventDecoderContext"; @@ -88,7 +87,7 @@ @implementation NSManagedObjectContext (zmessaging) - (BOOL)zm_isValidContext { - return self.zm_isSyncContext || self.zm_isUserInterfaceContext || self.zm_isSearchContext; + return self.zm_isSyncContext || self.zm_isUserInterfaceContext; } - (id)validUserInfoValueOfClass:(Class)class forKey:(NSString *)key @@ -128,11 +127,6 @@ - (BOOL)zm_isUserInterfaceContext return [[self validUserInfoValueOfClass:[NSNumber class] forKey:IsUserInterfaceContextKey] boolValue]; } -- (BOOL)zm_isSearchContext -{ - return [self.userInfo[IsSearchContextKey] boolValue]; -} - - (NSManagedObjectContext*)zm_syncContext { if (self.zm_isSyncContext) { @@ -539,13 +533,6 @@ - (void)markAsSyncContext; }]; } -- (void)markAsSearchContext; -{ - [self performBlockAndWait:^{ - self.userInfo[IsSearchContextKey] = @YES; - }]; -} - - (void)markAsUIContext { [self performBlockAndWait:^{ @@ -558,7 +545,6 @@ - (void)resetContextType [self performBlockAndWait:^{ self.userInfo[IsSyncContextKey] = @NO; self.userInfo[IsUserInterfaceContextKey] = @NO; - self.userInfo[IsSearchContextKey] = @NO; }]; } diff --git a/wire-ios-data-model/Support/Sourcery/generated/AutoMockable.generated.swift b/wire-ios-data-model/Support/Sourcery/generated/AutoMockable.generated.swift index 7a3a6d13cbd..3c6213be215 100644 --- a/wire-ios-data-model/Support/Sourcery/generated/AutoMockable.generated.swift +++ b/wire-ios-data-model/Support/Sourcery/generated/AutoMockable.generated.swift @@ -2137,15 +2137,6 @@ public class MockCoreDataStackProtocol: CoreDataStackProtocol { public var underlyingSyncContext: NSManagedObjectContext! - // MARK: - searchContext - - public var searchContext: NSManagedObjectContext { - get { return underlyingSearchContext } - set(value) { underlyingSearchContext = value } - } - - public var underlyingSearchContext: NSManagedObjectContext! - // MARK: - eventContext public var eventContext: NSManagedObjectContext { diff --git a/wire-ios-data-model/Tests/Source/Model/ZMBaseManagedObjectTest.m b/wire-ios-data-model/Tests/Source/Model/ZMBaseManagedObjectTest.m index fd593708298..d4ad4c80e59 100644 --- a/wire-ios-data-model/Tests/Source/Model/ZMBaseManagedObjectTest.m +++ b/wire-ios-data-model/Tests/Source/Model/ZMBaseManagedObjectTest.m @@ -138,11 +138,6 @@ - (NSManagedObjectContext *)syncMOC return self.coreDataStack.syncContext; } -- (NSManagedObjectContext *)searchMOC -{ - return self.coreDataStack.searchContext; -} - - (void)resetUIandSyncContextsAndResetPersistentStore:(BOOL)resetPersistentStore { self.coreDataStack = nil; diff --git a/wire-ios-sync-engine/Source/SessionManager/UserSessionLoader.swift b/wire-ios-sync-engine/Source/SessionManager/UserSessionLoader.swift index 2ac09b05294..d542c0ef8e7 100644 --- a/wire-ios-sync-engine/Source/SessionManager/UserSessionLoader.swift +++ b/wire-ios-sync-engine/Source/SessionManager/UserSessionLoader.swift @@ -469,8 +469,7 @@ final class UserSessionLoader { accountID: accountID, databaseContexts: [ coreDataStack.viewContext, - coreDataStack.syncContext, - coreDataStack.searchContext + coreDataStack.syncContext ], canPerformKeyMigration: true, sharedUserDefaults: sharedUserDefaults, diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift index 9462ef6a786..ef3654c8d00 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift @@ -21,7 +21,6 @@ import Foundation @objcMembers public class SearchDirectory: NSObject { - let searchContext: NSManagedObjectContext let contextProvider: ContextProvider let transportSession: TransportSessionType private let apiVersion: WireTransport.APIVersion? @@ -39,7 +38,6 @@ public class SearchDirectory: NSObject { public convenience init(userSession: ZMUserSession) { self.init( - searchContext: userSession.searchManagedObjectContext, contextProvider: userSession, transportSession: userSession.transportSession, searchUsersCache: userSession.searchUsersCache, @@ -50,7 +48,6 @@ public class SearchDirectory: NSObject { } init( - searchContext: NSManagedObjectContext, contextProvider: ContextProvider, transportSession: TransportSessionType, searchUsersCache: SearchUsersCache?, @@ -58,7 +55,6 @@ public class SearchDirectory: NSObject { refreshConversationsMissingMetadataAction: RecurringAction, apiVersion: WireTransport.APIVersion? ) { - self.searchContext = searchContext self.contextProvider = contextProvider self.transportSession = transportSession self.searchUsersCache = searchUsersCache @@ -74,7 +70,6 @@ public class SearchDirectory: NSObject { public func perform(_ request: SearchRequest) -> SearchTask { let task = SearchTask( task: .search(searchRequest: request), - searchContext: searchContext, contextProvider: contextProvider, transportSession: transportSession, searchUsersCache: searchUsersCache, @@ -96,7 +91,6 @@ public class SearchDirectory: NSObject { public func lookup(qualifiedID: QualifiedID) -> SearchTask { let task = SearchTask( task: .lookup(qualifiedID: qualifiedID), - searchContext: searchContext, contextProvider: contextProvider, transportSession: transportSession, 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 3cc96228a18..2699a3576fb 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -30,7 +30,6 @@ public class SearchTask { private let apiVersion: WireTransport.APIVersion? private let transportSession: TransportSessionType - private let searchContext: NSManagedObjectContext private let contextProvider: ContextProvider private let searchUsersCache: SearchUsersCache? @@ -79,7 +78,6 @@ public class SearchTask { convenience init( request: SearchRequest, - searchContext: NSManagedObjectContext, contextProvider: ContextProvider, transportSession: TransportSessionType, searchUsersCache: SearchUsersCache?, @@ -87,7 +85,6 @@ public class SearchTask { ) { self.init( task: .search(searchRequest: request), - searchContext: searchContext, contextProvider: contextProvider, transportSession: transportSession, searchUsersCache: searchUsersCache, @@ -97,7 +94,6 @@ public class SearchTask { convenience init( qualifiedID: QualifiedID, - searchContext: NSManagedObjectContext, contextProvider: ContextProvider, transportSession: TransportSessionType, searchUsersCache: SearchUsersCache?, @@ -105,7 +101,6 @@ public class SearchTask { ) { self.init( task: .lookup(qualifiedID: qualifiedID), - searchContext: searchContext, contextProvider: contextProvider, transportSession: transportSession, searchUsersCache: searchUsersCache, @@ -115,7 +110,6 @@ public class SearchTask { public init( task: Task, - searchContext: NSManagedObjectContext, contextProvider: ContextProvider, transportSession: TransportSessionType, searchUsersCache: SearchUsersCache?, @@ -123,7 +117,6 @@ public class SearchTask { ) { self.task = task self.transportSession = transportSession - self.searchContext = searchContext self.contextProvider = contextProvider self.searchUsersCache = searchUsersCache self.apiVersion = apiVersion @@ -173,6 +166,7 @@ extension SearchTask { tasksRemaining += 1 + let searchContext = contextProvider.newBackgroundContext() searchContext.performGroupedBlock { [self] in let selfUser = ZMUser.selfUser(in: searchContext) @@ -228,6 +222,7 @@ extension SearchTask { tasksRemaining += 1 + let searchContext = contextProvider.newBackgroundContext() searchContext.performGroupedBlock { [self] in var team: Team? @@ -298,6 +293,7 @@ extension SearchTask { } private func filterNonActiveTeamMembers(members: [Member]) -> [Member] { + let searchContext = contextProvider.newBackgroundContext() let activeConversations = ZMUser.selfUser(in: searchContext).activeConversations let activeContacts = Set(activeConversations.flatMap(\.localParticipants)) let selfUser = ZMUser.selfUser(in: searchContext) @@ -317,6 +313,7 @@ extension SearchTask { if searchOptions.contains(.excludeNonActivePartners) { let query = query.strippingLeadingAtSign() + let searchContext = contextProvider.newBackgroundContext() let selfUser = ZMUser.selfUser(in: searchContext) let activeConversations = ZMUser.selfUser(in: searchContext).activeConversations let activeContacts = Set(activeConversations.flatMap(\.localParticipants)) @@ -344,6 +341,7 @@ extension SearchTask { ZMUser.sortedFetchRequest(with: ZMUser.predicateForConnectedUsers(withSearch: query)) } + let searchContext = contextProvider.newBackgroundContext() return searchContext.fetchOrAssert(request: fetchRequest) as? [ZMUser] ?? [] } @@ -356,6 +354,7 @@ extension SearchTask { )) fetchRequest.sortDescriptors = [NSSortDescriptor(key: ZMNormalizedUserDefinedNameKey, ascending: true)] + let searchContext = contextProvider.newBackgroundContext() var conversations = searchContext.fetchOrAssert(request: fetchRequest) as? [ZMConversation] ?? [] if query.isHandleQuery { @@ -391,6 +390,7 @@ extension SearchTask { tasksRemaining += 1 + let searchContext = contextProvider.newBackgroundContext() searchContext.performGroupedBlock { [self] in let request = type(of: self).searchRequestForUser(qualifiedID: qualifiedID, apiVersion: apiVersion) request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in @@ -450,6 +450,7 @@ extension SearchTask { tasksRemaining += 1 + let searchContext = contextProvider.newBackgroundContext() searchContext.performGroupedBlock { [self] in let request = Self.searchRequestInDirectory(withRequest: searchRequest, apiVersion: apiVersion) @@ -526,6 +527,7 @@ extension SearchTask { }) + let searchContext = contextProvider.newBackgroundContext() request.add(ZMTaskCreatedHandler(on: searchContext) { [weak self] taskIdentifier in self?.teamMembershipTaskIdentifier = taskIdentifier }) @@ -601,6 +603,7 @@ extension SearchTask { tasksRemaining += 1 + let searchContext = contextProvider.newBackgroundContext() searchContext.performGroupedBlock { [self] in let request = type(of: self).searchRequestInDirectory( withHandle: searchRequest.query.string, @@ -684,7 +687,10 @@ extension SearchTask { extension SearchTask { func performRemoteSearchForServices() { - let teamIdentifier = searchContext.performAndWait { ZMUser.selfUser(in: searchContext).team?.remoteIdentifier } + let searchContext = contextProvider.newBackgroundContext() + let teamIdentifier = searchContext.performAndWait { + ZMUser.selfUser(in: searchContext).team?.remoteIdentifier + } guard let apiVersion, let teamIdentifier, diff --git a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+Logs.swift b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+Logs.swift index 6068de9be03..d90a35be5e8 100644 --- a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+Logs.swift +++ b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+Logs.swift @@ -22,25 +22,21 @@ import WireDataModel // MARK: - Error on context save debugging public enum ContextType: String { - case UI - case Sync - case Search - case Other + case ui + case sync + case other } extension NSManagedObjectContext { var type: ContextType { if zm_isSyncContext { - return .Sync + return .sync } if zm_isUserInterfaceContext { - return .UI + return .ui } - if zm_isSearchContext { - return .Search - } - return .Other + return .other } } diff --git a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession.swift b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession.swift index 6f7f3077798..125d3fa5ba1 100644 --- a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession.swift +++ b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession.swift @@ -230,11 +230,6 @@ public final class ZMUserSession: NSObject { coreDataStack.syncContext } - // swiftlint:disable:next todo_requires_jira_link - public var searchManagedObjectContext: NSManagedObjectContext { // TODO: jacob we don't want this to be public - coreDataStack.searchContext - } - // swiftlint:disable:next todo_requires_jira_link public var sharedContainerURL: URL { // TODO: jacob we don't want this to be public coreDataStack.applicationContainer @@ -1445,10 +1440,6 @@ extension ZMUserSession: ContextProvider { coreDataStack.syncContext } - public var searchContext: NSManagedObjectContext { - coreDataStack.searchContext - } - public var eventContext: NSManagedObjectContext { coreDataStack.eventContext } diff --git a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSessionBuilder.swift b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSessionBuilder.swift index 2b3bba2ef1d..dfb3f205d03 100644 --- a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSessionBuilder.swift +++ b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSessionBuilder.swift @@ -253,8 +253,7 @@ struct ZMUserSessionBuilder { accountID: coreDataStack.account.userIdentifier, databaseContexts: [ coreDataStack.viewContext, - coreDataStack.syncContext, - coreDataStack.searchContext + coreDataStack.syncContext ], canPerformKeyMigration: true, sharedUserDefaults: sharedUserDefaults, diff --git a/wire-ios-sync-engine/Tests/Source/DatabaseTest.swift b/wire-ios-sync-engine/Tests/Source/DatabaseTest.swift index 2c752135e72..864004a27b6 100644 --- a/wire-ios-sync-engine/Tests/Source/DatabaseTest.swift +++ b/wire-ios-sync-engine/Tests/Source/DatabaseTest.swift @@ -40,10 +40,6 @@ class DatabaseTest: ZMTBaseTest { coreDataStack!.syncContext } - var searchMOC: NSManagedObjectContext { - coreDataStack!.searchContext - } - var sharedContainerURL: URL? { let bundleIdentifier = Bundle.main.bundleIdentifier let groupIdentifier = "group." + bundleIdentifier! diff --git a/wire-ios-sync-engine/Tests/Source/MessagingTest.m b/wire-ios-sync-engine/Tests/Source/MessagingTest.m index 5ecc8f7eaa7..a2ad59ccee3 100644 --- a/wire-ios-sync-engine/Tests/Source/MessagingTest.m +++ b/wire-ios-sync-engine/Tests/Source/MessagingTest.m @@ -211,11 +211,6 @@ - (NSManagedObjectContext *)syncMOC return self.coreDataStack.syncContext; } -- (NSManagedObjectContext *)searchMOC -{ - return self.coreDataStack.searchContext; -} - - (NSManagedObjectContext *)eventMOC { return self.coreDataStack.eventContext; diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift index 9e23de73b85..764c210fff1 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift @@ -61,7 +61,6 @@ final class SearchDirectoryTests: DatabaseTest { private func makeSearchDirectory(apiVersion: APIVersion) -> SearchDirectory { SearchDirectory( - searchContext: searchMOC, contextProvider: coreDataStack!, transportSession: mockTransport, searchUsersCache: mockCache, diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift index bea95e9daac..e18330ccb84 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift @@ -1419,7 +1419,6 @@ final class SearchTaskTests: DatabaseTest { ) -> SearchTask { SearchTask( request: request, - searchContext: searchMOC, contextProvider: coreDataStack!, transportSession: mockTransportSession, searchUsersCache: mockCache, @@ -1434,7 +1433,6 @@ final class SearchTaskTests: DatabaseTest { ) -> SearchTask { SearchTask( qualifiedID: QualifiedID(uuid: lookupUserId, domain: domain), - searchContext: searchMOC, contextProvider: coreDataStack!, transportSession: mockTransportSession, searchUsersCache: mockCache, diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SessionManagerEncryptionAtRestMigrationTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SessionManagerEncryptionAtRestMigrationTests.swift index fa81199455f..a66ba158860 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/SessionManagerEncryptionAtRestMigrationTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/SessionManagerEncryptionAtRestMigrationTests.swift @@ -56,8 +56,7 @@ final class SessionManagerEncryptionAtRestMigrationTests: ZMUserSessionTestsBase accountID: coreDataStack.account.userIdentifier, databaseContexts: [ coreDataStack.viewContext, - coreDataStack.syncContext, - coreDataStack.searchContext + coreDataStack.syncContext ], canPerformKeyMigration: true, sharedUserDefaults: sharedUserDefaults, diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/ZMUserSessionTests+EncryptionAtRest.swift b/wire-ios-sync-engine/Tests/Source/UserSession/ZMUserSessionTests+EncryptionAtRest.swift index c97b6a0b2d8..4a256cd6d72 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/ZMUserSessionTests+EncryptionAtRest.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/ZMUserSessionTests+EncryptionAtRest.swift @@ -47,8 +47,7 @@ final class ZMUserSessionTests_EncryptionAtRest: ZMUserSessionTestsBase { accountID: coreDataStack.account.userIdentifier, databaseContexts: [ coreDataStack.viewContext, - coreDataStack.syncContext, - coreDataStack.searchContext + coreDataStack.syncContext ], canPerformKeyMigration: true, sharedUserDefaults: sharedUserDefaults, diff --git a/wire-ios/Tests/Mocks/UserSessionMock.swift b/wire-ios/Tests/Mocks/UserSessionMock.swift index 6cd90322d55..803ee6aafaf 100644 --- a/wire-ios/Tests/Mocks/UserSessionMock.swift +++ b/wire-ios/Tests/Mocks/UserSessionMock.swift @@ -470,7 +470,6 @@ extension UserSessionMock: ContextProvider { } var syncContext: NSManagedObjectContext { contextProvider.syncContext } - var searchContext: NSManagedObjectContext { contextProvider.searchContext } var eventContext: NSManagedObjectContext { contextProvider.eventContext } } diff --git a/wire-ios/Wire-iOS Tests/DeviceManagement/MockContextProvider.swift b/wire-ios/Wire-iOS Tests/DeviceManagement/MockContextProvider.swift index ec3840563f2..578a8285c8e 100644 --- a/wire-ios/Wire-iOS Tests/DeviceManagement/MockContextProvider.swift +++ b/wire-ios/Wire-iOS Tests/DeviceManagement/MockContextProvider.swift @@ -38,10 +38,6 @@ final class MockContextProvider: ContextProvider { NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) } - var searchContext: NSManagedObjectContext { - NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) - } - var eventContext: NSManagedObjectContext { NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) } From 9e2359741ada32bddb1ed38c48d5c95692c91a03 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 15:59:42 +0100 Subject: [PATCH 48/71] add notes --- wire-ios-data-model/Source/Model/User/ZMSearchUser.swift | 2 +- wire-ios-data-model/Source/Model/User/ZMUser.m | 1 + wire-ios-data-model/Source/Public/ZMUser.h | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/wire-ios-data-model/Source/Model/User/ZMSearchUser.swift b/wire-ios-data-model/Source/Model/User/ZMSearchUser.swift index 93762445803..e31260fe65a 100644 --- a/wire-ios-data-model/Source/Model/User/ZMSearchUser.swift +++ b/wire-ios-data-model/Source/Model/User/ZMSearchUser.swift @@ -105,7 +105,7 @@ public class ZMSearchUser: NSObject, UserType { user?.objectId ?? remoteIdentifier! } - fileprivate weak var contextProvider: ContextProvider? + fileprivate weak var contextProvider: ContextProvider? // TODO: remove, we seem to only access the viewContext private let searchUsersCache: SearchUsersCache? fileprivate var internalDomain: String? diff --git a/wire-ios-data-model/Source/Model/User/ZMUser.m b/wire-ios-data-model/Source/Model/User/ZMUser.m index 52e3ac4f8a6..548b12fe0e1 100644 --- a/wire-ios-data-model/Source/Model/User/ZMUser.m +++ b/wire-ios-data-model/Source/Model/User/ZMUser.m @@ -719,6 +719,7 @@ + (instancetype)selfUserInContext:(NSManagedObjectContext *)moc; @implementation ZMUser (Utilities) +// TODO: consider deleting + (ZMUser *)selfUserInUserSession:(id)session { VerifyReturnNil(session != nil); diff --git a/wire-ios-data-model/Source/Public/ZMUser.h b/wire-ios-data-model/Source/Public/ZMUser.h index 9698d32a57a..f9d51022ea0 100644 --- a/wire-ios-data-model/Source/Public/ZMUser.h +++ b/wire-ios-data-model/Source/Public/ZMUser.h @@ -87,6 +87,7 @@ typedef NS_ENUM(int16_t, ZMBlockState) { @interface ZMUser (Utilities) +// TODO: consider deleting + (ZMUser *_Nonnull)selfUserInUserSession:(id _Nonnull)session; @end From 3c94a0883bbb2e1e57dce22722c27b2fd26ceee4 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Mon, 2 Feb 2026 16:15:00 +0100 Subject: [PATCH 49/71] several fixes --- .../UserSession/Search/SearchTask.swift | 85 ++++++++++++------- 1 file changed, 55 insertions(+), 30 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 2699a3576fb..8e36bfa89fe 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -167,6 +167,7 @@ extension SearchTask { tasksRemaining += 1 let searchContext = contextProvider.newBackgroundContext() + let viewContext = contextProvider.viewContext searchContext.performGroupedBlock { [self] in let selfUser = ZMUser.selfUser(in: searchContext) @@ -175,20 +176,25 @@ extension SearchTask { options.updateForSelfUserTeamRole(selfUser: selfUser) /// search for the local user with matching user ID and active - let activeMembers = teamMembers(matchingQuery: "", team: selfUser.team, searchOptions: options) + let activeMembers = teamMembers( + matchingQuery: "", + team: selfUser.team, + searchOptions: options, + in: searchContext + ) let teamMembers = activeMembers.filter { $0.remoteIdentifier == qualifiedID.uuid } - let connectedUsers = connectedUsers(matchingQuery: "", hostedOnDomain: nil) + let connectedUsers = connectedUsers(matchingQuery: "", hostedOnDomain: nil, in: searchContext) .filter { $0.remoteIdentifier == qualifiedID.uuid } - contextProvider.viewContext.performGroupedBlock { [self] in + viewContext.performGroupedBlock { [self] in let copiedTeamMembers = teamMembers.compactMap(\.user) - .compactMap { contextProvider.viewContext.object(with: $0.objectID) as? Member } + .compactMap { viewContext.object(with: $0.objectID) as? Member } let copiedConnectedUsers = connectedUsers - .compactMap { contextProvider.viewContext.object(with: $0.objectID) as? ZMUser } + .compactMap { viewContext.object(with: $0.objectID) as? ZMUser } let partialResult = SearchResult( - context: contextProvider.viewContext, + context: viewContext, contacts: copiedConnectedUsers.map { ZMSearchUser( contextProvider: contextProvider, @@ -210,7 +216,7 @@ extension SearchTask { ) aggregatedResult = aggregatedResult - .union(withLocalResult: partialResult.copy(on: contextProvider.viewContext)) + .union(withLocalResult: partialResult.copy(on: viewContext)) tasksRemaining -= 1 } @@ -223,6 +229,7 @@ extension SearchTask { tasksRemaining += 1 let searchContext = contextProvider.newBackgroundContext() + let viewContext = contextProvider.viewContext searchContext.performGroupedBlock { [self] in var team: Team? @@ -234,23 +241,26 @@ extension SearchTask { let connectedUsers = request.searchOptions .contains(.contacts) ? connectedUsers( matchingQuery: request.normalizedQuery, - hostedOnDomain: request.searchDomain + hostedOnDomain: request.searchDomain, + in: searchContext ) : [] let teamMembers = request.searchOptions.contains(.teamMembers) ? teamMembers( matchingQuery: request.normalizedQuery, team: team, - searchOptions: request.searchOptions + searchOptions: request.searchOptions, + in: searchContext ) : [] let conversations = request.searchOptions.contains(.conversations) ? conversations( matchingQuery: request.query, - selfUser: selfUser + selfUser: selfUser, + in: searchContext ) : [] - contextProvider.viewContext.performGroupedBlock { [self] in + viewContext.performGroupedBlock { [self] in let copiedConnectedUsers = connectedUsers - .compactMap { contextProvider.viewContext.object(with: $0.objectID) as? ZMUser } + .compactMap { viewContext.object(with: $0.objectID) as? ZMUser } let searchConnectedUsers = copiedConnectedUsers .map { ZMSearchUser( @@ -262,7 +272,7 @@ extension SearchTask { .filter { !$0.hasEmptyName } let copiedteamMembers = teamMembers.compactMap { - contextProvider.viewContext.object(with: $0.objectID) as? Member + viewContext.object(with: $0.objectID) as? Member } let searchTeamMembers = copiedteamMembers .compactMap(\.user) @@ -275,7 +285,7 @@ extension SearchTask { } let partialResult = SearchResult( - context: contextProvider.viewContext, + context: viewContext, contacts: searchConnectedUsers, teamMembers: searchTeamMembers, directory: [], @@ -285,18 +295,20 @@ extension SearchTask { ) aggregatedResult = aggregatedResult - .union(withLocalResult: partialResult.copy(on: contextProvider.viewContext)) + .union(withLocalResult: partialResult.copy(on: viewContext)) tasksRemaining -= 1 } } } - private func filterNonActiveTeamMembers(members: [Member]) -> [Member] { - let searchContext = contextProvider.newBackgroundContext() - let activeConversations = ZMUser.selfUser(in: searchContext).activeConversations + private func filterNonActiveTeamMembers( + members: [Member], + in context: NSManagedObjectContext + ) -> [Member] { + let activeConversations = ZMUser.selfUser(in: context).activeConversations let activeContacts = Set(activeConversations.flatMap(\.localParticipants)) - let selfUser = ZMUser.selfUser(in: searchContext) + let selfUser = ZMUser.selfUser(in: context) return members.filter { guard let user = $0.user else { return false } @@ -304,18 +316,25 @@ extension SearchTask { } } - func teamMembers(matchingQuery query: String, team: Team?, searchOptions: SearchOptions) -> [Member] { + func teamMembers( + matchingQuery query: String, + team: Team?, + searchOptions: SearchOptions, + in context: NSManagedObjectContext + ) -> [Member] { var partialResult = team?.members(matchingQuery: query) ?? [] if searchOptions.contains(.excludeNonActiveTeamMembers) { - partialResult = filterNonActiveTeamMembers(members: partialResult) + partialResult = filterNonActiveTeamMembers( + members: partialResult, + in: context + ) } if searchOptions.contains(.excludeNonActivePartners) { let query = query.strippingLeadingAtSign() - let searchContext = contextProvider.newBackgroundContext() - let selfUser = ZMUser.selfUser(in: searchContext) - let activeConversations = ZMUser.selfUser(in: searchContext).activeConversations + let selfUser = ZMUser.selfUser(in: context) + let activeConversations = ZMUser.selfUser(in: context).activeConversations let activeContacts = Set(activeConversations.flatMap(\.localParticipants)) partialResult = partialResult.filter { membership in @@ -331,7 +350,11 @@ extension SearchTask { return partialResult } - func connectedUsers(matchingQuery query: String, hostedOnDomain: String?) -> [ZMUser] { + func connectedUsers( + matchingQuery query: String, + hostedOnDomain: String?, + in context: NSManagedObjectContext + ) -> [ZMUser] { let fetchRequest: NSFetchRequest = if let hostedOnDomain { ZMUser.sortedFetchRequest(with: ZMUser.predicateForConnectedUsers( withSearch: query, @@ -341,11 +364,14 @@ extension SearchTask { ZMUser.sortedFetchRequest(with: ZMUser.predicateForConnectedUsers(withSearch: query)) } - let searchContext = contextProvider.newBackgroundContext() - return searchContext.fetchOrAssert(request: fetchRequest) as? [ZMUser] ?? [] + return context.fetchOrAssert(request: fetchRequest) as? [ZMUser] ?? [] } - func conversations(matchingQuery query: SearchRequest.Query, selfUser: ZMUser) -> [ZMConversation] { + func conversations( + matchingQuery query: SearchRequest.Query, + selfUser: ZMUser, + in context: NSManagedObjectContext + ) -> [ZMConversation] { // swiftlint:disable:next todo_requires_jira_link // TODO: use the interface with team param? let fetchRequest = ZMConversation.sortedFetchRequest(with: ZMConversation.predicate( @@ -354,8 +380,7 @@ extension SearchTask { )) fetchRequest.sortDescriptors = [NSSortDescriptor(key: ZMNormalizedUserDefinedNameKey, ascending: true)] - let searchContext = contextProvider.newBackgroundContext() - var conversations = searchContext.fetchOrAssert(request: fetchRequest) as? [ZMConversation] ?? [] + var conversations = context.fetchOrAssert(request: fetchRequest) as? [ZMConversation] ?? [] if query.isHandleQuery { // if we are searching for a username only include conversations with matching displayName From bfbe31fb3410a90c2e864e34970c02f7a4d3fc40 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Tue, 3 Feb 2026 19:05:52 +0100 Subject: [PATCH 50/71] minor fixes --- .../Source/UserSession/Search/SearchTask.swift | 12 ++++++------ .../DeepLinkURLActionProcessor.swift | 1 - .../Tests/Source/UserSession/SearchTaskTests.swift | 2 +- wire-ios/Wire-iOS/Sources/AuthenticatedRouter.swift | 4 ++-- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 8e36bfa89fe..d1a2b355e12 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -168,7 +168,7 @@ extension SearchTask { let searchContext = contextProvider.newBackgroundContext() let viewContext = contextProvider.viewContext - searchContext.performGroupedBlock { [self] in + searchContext.perform { [self] in let selfUser = ZMUser.selfUser(in: searchContext) var options = SearchOptions() @@ -230,7 +230,7 @@ extension SearchTask { let searchContext = contextProvider.newBackgroundContext() let viewContext = contextProvider.viewContext - searchContext.performGroupedBlock { [self] in + searchContext.perform { [self] in var team: Team? if let teamObjectID = request.team?.objectID { @@ -416,7 +416,7 @@ extension SearchTask { tasksRemaining += 1 let searchContext = contextProvider.newBackgroundContext() - searchContext.performGroupedBlock { [self] in + searchContext.perform { [self] in let request = type(of: self).searchRequestForUser(qualifiedID: qualifiedID, apiVersion: apiVersion) request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in defer { @@ -476,7 +476,7 @@ extension SearchTask { tasksRemaining += 1 let searchContext = contextProvider.newBackgroundContext() - searchContext.performGroupedBlock { [self] in + searchContext.perform { [self] in let request = Self.searchRequestInDirectory(withRequest: searchRequest, apiVersion: apiVersion) request.add(ZMCompletionHandler(on: contextProvider.viewContext) { [weak self] response in @@ -629,7 +629,7 @@ extension SearchTask { tasksRemaining += 1 let searchContext = contextProvider.newBackgroundContext() - searchContext.performGroupedBlock { [self] in + searchContext.perform { [self] in let request = type(of: self).searchRequestInDirectory( withHandle: searchRequest.query.string, apiVersion: apiVersion @@ -726,7 +726,7 @@ extension SearchTask { tasksRemaining += 1 - searchContext.performGroupedBlock { [self] in + searchContext.perform { [self] in let request = type(of: self).servicesSearchRequest( teamIdentifier: teamIdentifier, diff --git a/wire-ios-sync-engine/Source/UserSession/URLActionProcessors/DeepLinkURLActionProcessor.swift b/wire-ios-sync-engine/Source/UserSession/URLActionProcessors/DeepLinkURLActionProcessor.swift index fc230b3cc11..daed7dcbc71 100644 --- a/wire-ios-sync-engine/Source/UserSession/URLActionProcessors/DeepLinkURLActionProcessor.swift +++ b/wire-ios-sync-engine/Source/UserSession/URLActionProcessors/DeepLinkURLActionProcessor.swift @@ -209,7 +209,6 @@ class DeepLinkURLActionProcessor: URLActionProcessor { private func handleOpenUserProfile(id: UUID, domain: String?, delegate: PresentationDelegate?) { let viewContext = contextProvider.viewContext - if let user = ZMUser.fetch(with: id, domain: domain, in: viewContext) { delegate?.showUserProfile(user: user) } else { diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift index e18330ccb84..015914b2b5b 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift @@ -17,9 +17,9 @@ // import XCTest - import WireMockTransport import WireTransport + @testable import WireSyncEngine final class SearchTaskTests: DatabaseTest { diff --git a/wire-ios/Wire-iOS/Sources/AuthenticatedRouter.swift b/wire-ios/Wire-iOS/Sources/AuthenticatedRouter.swift index 5e9c4316e17..26f9975ba37 100644 --- a/wire-ios/Wire-iOS/Sources/AuthenticatedRouter.swift +++ b/wire-ios/Wire-iOS/Sources/AuthenticatedRouter.swift @@ -176,8 +176,8 @@ extension AuthenticatedRouter: AuthenticatedRouterProtocol { func navigate(to destination: NavigationDestination) { switch destination { - case let .conversation(converation, message): - _zClientViewController?.showConversation(converation, at: message) + case let .conversation(conversation, message): + _zClientViewController?.showConversation(conversation, at: message) case let .connectionRequest(qualifiedID): _zClientViewController?.showConnectionRequest(qualifiedID: qualifiedID) case .conversationList: From d03e77adff1bf197fd56a3a9e91d364922c74654 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Tue, 3 Feb 2026 19:05:58 +0100 Subject: [PATCH 51/71] add debugging output --- .../UserSession/Search/SearchTask.swift | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index d1a2b355e12..066ee6c959c 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -193,6 +193,13 @@ extension SearchTask { let copiedConnectedUsers = connectedUsers .compactMap { viewContext.object(with: $0.objectID) as? ZMUser } + if !copiedTeamMembers.isEmpty { + print("#### \(#function):\(#line) copiedTeamMembers: \(copiedTeamMembers)") + } + if !copiedConnectedUsers.isEmpty { + print("#### \(#function):\(#line) copiedConnectedUsers: \(copiedConnectedUsers)") + } + let partialResult = SearchResult( context: viewContext, contacts: copiedConnectedUsers.map { @@ -284,6 +291,19 @@ extension SearchTask { ) } + if !copiedConnectedUsers.isEmpty { + print("### \(#function):\(#line) copiedConnectedUsers: \(copiedConnectedUsers)") + } + if !searchConnectedUsers.isEmpty { + print("### \(#function):\(#line) searchConnectedUsers: \(searchConnectedUsers)") + } + if !copiedteamMembers.isEmpty { + print("### \(#function):\(#line) copiedteamMembers: \(copiedteamMembers)") + } + if !searchTeamMembers.isEmpty { + print("### \(#function):\(#line) searchTeamMembers: \(searchTeamMembers)") + } + let partialResult = SearchResult( context: viewContext, contacts: searchConnectedUsers, @@ -323,12 +343,18 @@ extension SearchTask { in context: NSManagedObjectContext ) -> [Member] { var partialResult = team?.members(matchingQuery: query) ?? [] + if !partialResult.isEmpty { + print("### \(#function):\(#line) partialResult: \(partialResult)") + } if searchOptions.contains(.excludeNonActiveTeamMembers) { partialResult = filterNonActiveTeamMembers( members: partialResult, in: context ) + if !partialResult.isEmpty { + print("### \(#function):\(#line) partialResult: \(partialResult)") + } } if searchOptions.contains(.excludeNonActivePartners) { @@ -345,6 +371,9 @@ extension SearchTask { false } } + if !partialResult.isEmpty { + print("### \(#function):\(#line) partialResult: \(partialResult)") + } } return partialResult @@ -381,11 +410,17 @@ extension SearchTask { fetchRequest.sortDescriptors = [NSSortDescriptor(key: ZMNormalizedUserDefinedNameKey, ascending: true)] var conversations = context.fetchOrAssert(request: fetchRequest) as? [ZMConversation] ?? [] + if !conversations.isEmpty { + print("### \(#function):\(#line) conversations: \(conversations)") + } if query.isHandleQuery { // if we are searching for a username only include conversations with matching displayName conversations = conversations.filter { ($0.displayName ?? "").contains(query.string) } } + if !conversations.isEmpty { + print("### \(#function):\(#line) conversations: \(conversations)") + } let matchingPredicate = ZMConversation.userDefinedNamePredicate(forSearch: query.string) var matching: [ZMConversation] = [] @@ -399,6 +434,12 @@ extension SearchTask { nonMatching.append(conversation) } } + if !matching.isEmpty { + print("### \(#function):\(#line) matching: \(matching)") + } + if !nonMatching.isEmpty { + print("### \(#function):\(#line) nonMatching: \(nonMatching)") + } return matching + nonMatching } @@ -433,6 +474,8 @@ extension SearchTask { ) else { return } + print("### \(#function):\(#line) partialResult: \(partialResult)") + let updatedResult = aggregatedResult.union(withDirectoryResult: partialResult) aggregatedResult = updatedResult }) @@ -496,6 +539,8 @@ extension SearchTask { return } + print("### \(#function):\(#line) partialResult: \(partialResult)") + if searchRequest.searchOptions.contains(.teamMembers) { performTeamMembershipLookup(on: partialResult, searchRequest: searchRequest) } else { @@ -541,12 +586,15 @@ extension SearchTask { } var updatedResult = searchResult + print("### \(#function):\(#line) updatedResult: \(updatedResult)") updatedResult.extendWithMembershipPayload(payload: payload) + print("### \(#function):\(#line) updatedResult: \(updatedResult)") updatedResult.filterBy( searchOptions: searchRequest.searchOptions, query: searchRequest.query.string, contextProvider: contextProvider ) + print("### \(#function):\(#line) updatedResult: \(updatedResult)") completeRemoteSearch(searchResult: updatedResult) @@ -669,6 +717,8 @@ extension SearchTask { return } + print("### \(#function):\(#line) partialResult: \(partialResult)") + if let user = partialResult.directory.first, !user.isSelfUser { let prevResult = aggregatedResult // prepend result to prevResult only if it doesn't contain it @@ -682,6 +732,7 @@ extension SearchTask { services: prevResult.services, searchUsersCache: searchUsersCache ) + print("### \(#function):\(#line) aggregatedResult: \(aggregatedResult)") } } }) @@ -754,6 +805,7 @@ extension SearchTask { } let updatedResult = aggregatedResult.union(withServiceResult: partialResult) + print("### \(#function):\(#line) updatedResult: \(updatedResult)") aggregatedResult = updatedResult }) From cc6b2553a93f99a0f285e37d48b3dd10b88be060 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Tue, 3 Feb 2026 19:17:01 +0100 Subject: [PATCH 52/71] typo --- .../ZClientViewController+ShowContentDelegate.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController+ShowContentDelegate.swift b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController+ShowContentDelegate.swift index 68b5462503c..17461625f1d 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController+ShowContentDelegate.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController+ShowContentDelegate.swift @@ -29,8 +29,9 @@ extension ZClientViewController { await mainCoordinator.presentViewController(navigationController) } + @MainActor func showConnectionRequest(qualifiedID: QualifiedID) { - let searchUserViewConroller = SearchUserViewController( + let searchUserViewController = SearchUserViewController( qualifiedID: qualifiedID, profileViewControllerDelegate: self, userSession: userSession, @@ -40,7 +41,7 @@ extension ZClientViewController { ) Task { - await wrapInNavigationControllerAndPresent(viewController: searchUserViewConroller) + await wrapInNavigationControllerAndPresent(viewController: searchUserViewController) } } From ffe95b7deeb26f03b5a022e47c91edbec1c40e88 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Tue, 3 Feb 2026 19:18:00 +0100 Subject: [PATCH 53/71] add note --- wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 066ee6c959c..4cd4a1a59b7 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -194,7 +194,7 @@ extension SearchTask { .compactMap { viewContext.object(with: $0.objectID) as? ZMUser } if !copiedTeamMembers.isEmpty { - print("#### \(#function):\(#line) copiedTeamMembers: \(copiedTeamMembers)") + print("#### \(#function):\(#line) copiedTeamMembers: \(copiedTeamMembers)") // TODO: revert d03e77adff1bf197fd56a3a9e91d364922c74654 } if !copiedConnectedUsers.isEmpty { print("#### \(#function):\(#line) copiedConnectedUsers: \(copiedConnectedUsers)") From bc95f3613a1ede9effdb0dcc187e6be78ee51864 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Tue, 3 Feb 2026 19:21:14 +0100 Subject: [PATCH 54/71] fix build error --- .../ZClientViewController+ShowContentDelegate.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController+ShowContentDelegate.swift b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController+ShowContentDelegate.swift index 17461625f1d..ef398b79b96 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController+ShowContentDelegate.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController+ShowContentDelegate.swift @@ -29,7 +29,6 @@ extension ZClientViewController { await mainCoordinator.presentViewController(navigationController) } - @MainActor func showConnectionRequest(qualifiedID: QualifiedID) { let searchUserViewController = SearchUserViewController( qualifiedID: qualifiedID, @@ -40,7 +39,7 @@ extension ZClientViewController { conversationCreationRepository: conversationCreationRepository ) - Task { + Task { @MainActor in await wrapInNavigationControllerAndPresent(viewController: searchUserViewController) } } From cae02ce5e39a00d15adebdf27f9e9949ad6cd0a7 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Tue, 3 Feb 2026 19:42:49 +0100 Subject: [PATCH 55/71] Revert "add debugging output" This reverts commit d03e77adff1bf197fd56a3a9e91d364922c74654. # Conflicts: # wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift --- .../UserSession/Search/SearchTask.swift | 52 ------------------- 1 file changed, 52 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 4cd4a1a59b7..d1a2b355e12 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -193,13 +193,6 @@ extension SearchTask { let copiedConnectedUsers = connectedUsers .compactMap { viewContext.object(with: $0.objectID) as? ZMUser } - if !copiedTeamMembers.isEmpty { - print("#### \(#function):\(#line) copiedTeamMembers: \(copiedTeamMembers)") // TODO: revert d03e77adff1bf197fd56a3a9e91d364922c74654 - } - if !copiedConnectedUsers.isEmpty { - print("#### \(#function):\(#line) copiedConnectedUsers: \(copiedConnectedUsers)") - } - let partialResult = SearchResult( context: viewContext, contacts: copiedConnectedUsers.map { @@ -291,19 +284,6 @@ extension SearchTask { ) } - if !copiedConnectedUsers.isEmpty { - print("### \(#function):\(#line) copiedConnectedUsers: \(copiedConnectedUsers)") - } - if !searchConnectedUsers.isEmpty { - print("### \(#function):\(#line) searchConnectedUsers: \(searchConnectedUsers)") - } - if !copiedteamMembers.isEmpty { - print("### \(#function):\(#line) copiedteamMembers: \(copiedteamMembers)") - } - if !searchTeamMembers.isEmpty { - print("### \(#function):\(#line) searchTeamMembers: \(searchTeamMembers)") - } - let partialResult = SearchResult( context: viewContext, contacts: searchConnectedUsers, @@ -343,18 +323,12 @@ extension SearchTask { in context: NSManagedObjectContext ) -> [Member] { var partialResult = team?.members(matchingQuery: query) ?? [] - if !partialResult.isEmpty { - print("### \(#function):\(#line) partialResult: \(partialResult)") - } if searchOptions.contains(.excludeNonActiveTeamMembers) { partialResult = filterNonActiveTeamMembers( members: partialResult, in: context ) - if !partialResult.isEmpty { - print("### \(#function):\(#line) partialResult: \(partialResult)") - } } if searchOptions.contains(.excludeNonActivePartners) { @@ -371,9 +345,6 @@ extension SearchTask { false } } - if !partialResult.isEmpty { - print("### \(#function):\(#line) partialResult: \(partialResult)") - } } return partialResult @@ -410,17 +381,11 @@ extension SearchTask { fetchRequest.sortDescriptors = [NSSortDescriptor(key: ZMNormalizedUserDefinedNameKey, ascending: true)] var conversations = context.fetchOrAssert(request: fetchRequest) as? [ZMConversation] ?? [] - if !conversations.isEmpty { - print("### \(#function):\(#line) conversations: \(conversations)") - } if query.isHandleQuery { // if we are searching for a username only include conversations with matching displayName conversations = conversations.filter { ($0.displayName ?? "").contains(query.string) } } - if !conversations.isEmpty { - print("### \(#function):\(#line) conversations: \(conversations)") - } let matchingPredicate = ZMConversation.userDefinedNamePredicate(forSearch: query.string) var matching: [ZMConversation] = [] @@ -434,12 +399,6 @@ extension SearchTask { nonMatching.append(conversation) } } - if !matching.isEmpty { - print("### \(#function):\(#line) matching: \(matching)") - } - if !nonMatching.isEmpty { - print("### \(#function):\(#line) nonMatching: \(nonMatching)") - } return matching + nonMatching } @@ -474,8 +433,6 @@ extension SearchTask { ) else { return } - print("### \(#function):\(#line) partialResult: \(partialResult)") - let updatedResult = aggregatedResult.union(withDirectoryResult: partialResult) aggregatedResult = updatedResult }) @@ -539,8 +496,6 @@ extension SearchTask { return } - print("### \(#function):\(#line) partialResult: \(partialResult)") - if searchRequest.searchOptions.contains(.teamMembers) { performTeamMembershipLookup(on: partialResult, searchRequest: searchRequest) } else { @@ -586,15 +541,12 @@ extension SearchTask { } var updatedResult = searchResult - print("### \(#function):\(#line) updatedResult: \(updatedResult)") updatedResult.extendWithMembershipPayload(payload: payload) - print("### \(#function):\(#line) updatedResult: \(updatedResult)") updatedResult.filterBy( searchOptions: searchRequest.searchOptions, query: searchRequest.query.string, contextProvider: contextProvider ) - print("### \(#function):\(#line) updatedResult: \(updatedResult)") completeRemoteSearch(searchResult: updatedResult) @@ -717,8 +669,6 @@ extension SearchTask { return } - print("### \(#function):\(#line) partialResult: \(partialResult)") - if let user = partialResult.directory.first, !user.isSelfUser { let prevResult = aggregatedResult // prepend result to prevResult only if it doesn't contain it @@ -732,7 +682,6 @@ extension SearchTask { services: prevResult.services, searchUsersCache: searchUsersCache ) - print("### \(#function):\(#line) aggregatedResult: \(aggregatedResult)") } } }) @@ -805,7 +754,6 @@ extension SearchTask { } let updatedResult = aggregatedResult.union(withServiceResult: partialResult) - print("### \(#function):\(#line) updatedResult: \(updatedResult)") aggregatedResult = updatedResult }) From 3bc6a9d9c3a9f4702400df1668c4f9965c44e42d Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Tue, 3 Feb 2026 21:08:54 +0100 Subject: [PATCH 56/71] remove unused code --- .../Model/User/UserType+Federation.swift | 2 - .../Source/Model/User/ZMSearchUser.swift | 36 +++-- .../Source/Model/User/ZMUser.m | 12 -- wire-ios-data-model/Source/Public/ZMUser.h | 10 -- .../SearchUserObserverCenterTests.swift | 2 +- .../Observer/SearchUserObserverTests.swift | 2 +- .../Observer/SearchUserSnapshotTests.swift | 2 +- .../User/ZMSearchUserTests+Connections.swift | 2 +- .../ZMSearchUserTests+ProfileImages.swift | 2 +- .../User/ZMSearchUserTests+TeamUser.swift | 2 +- .../Source/Model/User/ZMSearchUserTests.m | 126 +++++++++--------- .../Model/UserTypeTests+Materialize.swift | 2 +- .../Source/UserSession/SearchTaskTests.swift | 2 +- 13 files changed, 87 insertions(+), 115 deletions(-) diff --git a/wire-ios-data-model/Source/Model/User/UserType+Federation.swift b/wire-ios-data-model/Source/Model/User/UserType+Federation.swift index 16069797c7a..917ba9ccf68 100644 --- a/wire-ios-data-model/Source/Model/User/UserType+Federation.swift +++ b/wire-ios-data-model/Source/Model/User/UserType+Federation.swift @@ -16,8 +16,6 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // -import Foundation - extension UserType { func isFederating(with otherUser: UserType) -> Bool { diff --git a/wire-ios-data-model/Source/Model/User/ZMSearchUser.swift b/wire-ios-data-model/Source/Model/User/ZMSearchUser.swift index e31260fe65a..a9e764289b8 100644 --- a/wire-ios-data-model/Source/Model/User/ZMSearchUser.swift +++ b/wire-ios-data-model/Source/Model/User/ZMSearchUser.swift @@ -105,7 +105,7 @@ public class ZMSearchUser: NSObject, UserType { user?.objectId ?? remoteIdentifier! } - fileprivate weak var contextProvider: ContextProvider? // TODO: remove, we seem to only access the viewContext + fileprivate weak var viewContext: NSManagedObjectContext? private let searchUsersCache: SearchUsersCache? fileprivate var internalDomain: String? @@ -162,16 +162,12 @@ public class ZMSearchUser: NSObject, UserType { } public var isFederated: Bool { - guard let contextProvider else { - return false - } - - return ZMUser.selfUser(inUserSession: contextProvider).isFederating(with: self) + guard let viewContext else { return false } + return ZMUser.selfUser(in: viewContext).isFederating(with: self) } public var isSelfUser: Bool { guard let user else { return false } - return user.isSelfUser } @@ -265,8 +261,8 @@ public class ZMSearchUser: NSObject, UserType { } public var oneToOneConversation: ZMConversation? { - if isTeamMember, let uiContext = contextProvider?.viewContext { - materialize(in: uiContext)?.oneToOneConversation + if isTeamMember, let viewContext { + materialize(in: viewContext)?.oneToOneConversation } else { user?.oneToOneConversation } @@ -482,7 +478,7 @@ public class ZMSearchUser: NSObject, UserType { @objc public required init( - contextProvider: ContextProvider, + viewContext: NSManagedObjectContext, name: String, handle: String?, accentColor: ZMAccentColor?, @@ -503,10 +499,10 @@ public class ZMSearchUser: NSObject, UserType { self.internalDomain = domain self.remoteIdentifier = existingUser?.remoteIdentifier ?? remoteIdentifier self.teamIdentifier = existingUser?.teamIdentifier ?? teamIdentifier - self.contextProvider = contextProvider + self.viewContext = viewContext self.searchUsersCache = searchUsersCache - let selfUser = ZMUser.selfUser(inUserSession: contextProvider) + let selfUser = ZMUser.selfUser(in: viewContext) self.internalIsTeamMember = teamIdentifier != nil && selfUser.teamIdentifier == teamIdentifier self.internalIsConnected = internalIsTeamMember @@ -524,7 +520,7 @@ public class ZMSearchUser: NSObject, UserType { searchUsersCache: SearchUsersCache? ) { self.init( - contextProvider: contextProvider, + viewContext: contextProvider.viewContext, name: user.name ?? "", handle: user.handle, accentColor: user.zmAccentColor, @@ -556,7 +552,7 @@ public class ZMSearchUser: NSObject, UserType { let accentColorRawValue = (payload["accent_id"] as? NSNumber)?.int16Value ?? 0 self.init( - contextProvider: contextProvider, + viewContext: contextProvider.viewContext, name: name, handle: handle, accentColor: .from(rawValue: accentColorRawValue) ?? .default, @@ -611,7 +607,7 @@ public class ZMSearchUser: NSObject, UserType { } public func connect(completion: @escaping (Error?) -> Void) { - let selfUser = ZMUser.selfUser(inUserSession: contextProvider!) + let selfUser = ZMUser.selfUser(in: viewContext!) selfUser.sendConnectionRequest(to: self) { [weak self] result in switch result { case .success: @@ -628,7 +624,7 @@ public class ZMSearchUser: NSObject, UserType { private func updateLocalUser() { guard let userID = remoteIdentifier, - let viewContext = contextProvider?.viewContext + let viewContext else { return } @@ -637,7 +633,7 @@ public class ZMSearchUser: NSObject, UserType { } private func notifySearchUserChanged() { - contextProvider?.viewContext.searchUserObserverCenter.notifyUpdatedSearchUser(self) + viewContext?.searchUserObserverCenter.notifyUpdatedSearchUser(self) } public func accept(completion: @escaping (Error?) -> Void) { @@ -671,7 +667,7 @@ public class ZMSearchUser: NSObject, UserType { if let user { user.requestPreviewProfileImage() - } else if let notificationContext = contextProvider?.viewContext.notificationContext { + } else if let notificationContext = viewContext?.notificationContext { NotificationInContext( name: .searchUserDidRequestPreviewAsset, context: notificationContext, @@ -686,7 +682,7 @@ public class ZMSearchUser: NSObject, UserType { if let user { user.requestCompleteProfileImage() - } else if let notificationContext = contextProvider?.viewContext.notificationContext { + } else if let notificationContext = viewContext?.notificationContext { NotificationInContext( name: .searchUserDidRequestCompleteAsset, context: notificationContext, @@ -731,7 +727,7 @@ public class ZMSearchUser: NSObject, UserType { internalCompleteImageData = imageData } - contextProvider?.viewContext.searchUserObserverCenter.notifyUpdatedSearchUser(self) + viewContext?.searchUserObserverCenter.notifyUpdatedSearchUser(self) } public func update(from payload: [String: Any]) { diff --git a/wire-ios-data-model/Source/Model/User/ZMUser.m b/wire-ios-data-model/Source/Model/User/ZMUser.m index 548b12fe0e1..a3d22619e1b 100644 --- a/wire-ios-data-model/Source/Model/User/ZMUser.m +++ b/wire-ios-data-model/Source/Model/User/ZMUser.m @@ -717,18 +717,6 @@ + (instancetype)selfUserInContext:(NSManagedObjectContext *)moc; @end -@implementation ZMUser (Utilities) - -// TODO: consider deleting -+ (ZMUser *)selfUserInUserSession:(id)session -{ - VerifyReturnNil(session != nil); - return [self selfUserInContext:session.viewContext]; -} - -@end - - @implementation ZMUser (Editable) diff --git a/wire-ios-data-model/Source/Public/ZMUser.h b/wire-ios-data-model/Source/Public/ZMUser.h index f9d51022ea0..e360f881463 100644 --- a/wire-ios-data-model/Source/Public/ZMUser.h +++ b/wire-ios-data-model/Source/Public/ZMUser.h @@ -83,16 +83,6 @@ typedef NS_ENUM(int16_t, ZMBlockState) { @end -@protocol ZMEditableUserType; - -@interface ZMUser (Utilities) - -// TODO: consider deleting -+ (ZMUser *_Nonnull)selfUserInUserSession:(id _Nonnull)session; - -@end - - @interface ZMUser (Connections) diff --git a/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserObserverCenterTests.swift b/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserObserverCenterTests.swift index 4baaf034d6c..bf9a9fa503e 100644 --- a/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserObserverCenterTests.swift +++ b/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserObserverCenterTests.swift @@ -170,7 +170,7 @@ final class SearchUserObserverCenterTests: ModelObjectsTests { user: ZMUser? = nil ) -> ZMSearchUser { ZMSearchUser( - contextProvider: coreDataStack, + viewContext: coreDataStack.viewContext, name: name, handle: handle, accentColor: accentColor, diff --git a/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserObserverTests.swift b/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserObserverTests.swift index d3b72eb16e9..8c235f782e0 100644 --- a/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserObserverTests.swift +++ b/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserObserverTests.swift @@ -137,7 +137,7 @@ final class SearchUserObserverTests: NotificationDispatcherTestBase { user: ZMUser? = nil ) -> ZMSearchUser { ZMSearchUser( - contextProvider: coreDataStack, + viewContext: coreDataStack.viewContext, name: name, handle: name.lowercased(), accentColor: .amber, diff --git a/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserSnapshotTests.swift b/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserSnapshotTests.swift index 6cae178855c..449150565af 100644 --- a/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserSnapshotTests.swift +++ b/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserSnapshotTests.swift @@ -225,7 +225,7 @@ final class SearchUserSnapshotTests: ZMBaseManagedObjectTest { user: ZMUser? = nil ) -> ZMSearchUser { ZMSearchUser( - contextProvider: coreDataStack, + viewContext: coreDataStack.viewContext, name: name, handle: handle, accentColor: accentColor, diff --git a/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+Connections.swift b/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+Connections.swift index 97fd22103a3..2ba5a5e54a9 100644 --- a/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+Connections.swift +++ b/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+Connections.swift @@ -24,7 +24,7 @@ final class ZMSearchUserTests_Connections: ModelObjectsTests { func testThatConnectSendsAConnectToUserNotification() { // given let searchUser = ZMSearchUser( - contextProvider: coreDataStack, + viewContext: coreDataStack.viewContext, name: "John Doe", handle: "johndoe", accentColor: .turquoise, diff --git a/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+ProfileImages.swift b/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+ProfileImages.swift index 944b9585a06..b568e530f50 100644 --- a/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+ProfileImages.swift +++ b/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+ProfileImages.swift @@ -177,7 +177,7 @@ final class ZMSearchUserTests_ProfileImages: ZMBaseManagedObjectTest { user: ZMUser? = nil ) -> ZMSearchUser { ZMSearchUser( - contextProvider: coreDataStack, + viewContext: coreDataStack.viewContext, name: name, handle: name.lowercased(), accentColor: .amber, diff --git a/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+TeamUser.swift b/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+TeamUser.swift index 8c0f9184415..d0c457927b9 100644 --- a/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+TeamUser.swift +++ b/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+TeamUser.swift @@ -73,7 +73,7 @@ final class ZMSearchUserTests_TeamUser: ModelObjectsTests { private func makeSearchUser(teamIdentifier: UUID?) -> ZMSearchUser { ZMSearchUser( - contextProvider: coreDataStack, + viewContext: coreDataStack.viewContext, name: "Foo", handle: "foo", accentColor: .amber, diff --git a/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests.m b/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests.m index 7756f71c07a..7978fd77f9c 100644 --- a/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests.m +++ b/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests.m @@ -49,40 +49,40 @@ - (void)testThatItComparesEqualBasedOnRemoteID; NSUUID *remoteIDA = [NSUUID createUUID]; NSUUID *remoteIDB = [NSUUID createUUID]; - ZMSearchUser *user1 = [[ZMSearchUser alloc] initWithContextProvider:self.coreDataStack - name:@"A" - handle:@"a" - accentColor:ZMAccentColor.green - remoteIdentifier:remoteIDA - domain:nil - teamIdentifier:nil - user:nil - searchUsersCache:nil]; + ZMSearchUser *user1 = [[ZMSearchUser alloc] initWithViewContext:self.coreDataStack.viewContext + name:@"A" + handle:@"a" + accentColor:ZMAccentColor.green + remoteIdentifier:remoteIDA + domain:nil + teamIdentifier:nil + user:nil + searchUsersCache:nil]; // (1) - ZMSearchUser *user2 = [[ZMSearchUser alloc] initWithContextProvider:self.coreDataStack - name:@"B" - handle:@"b" - accentColor:ZMAccentColor.purple - remoteIdentifier:remoteIDA - domain:nil - teamIdentifier:nil - user:nil - searchUsersCache:nil]; + ZMSearchUser *user2 = [[ZMSearchUser alloc] initWithViewContext:self.coreDataStack.viewContext + name:@"B" + handle:@"b" + accentColor:ZMAccentColor.purple + remoteIdentifier:remoteIDA + domain:nil + teamIdentifier:nil + user:nil + searchUsersCache:nil]; XCTAssertEqualObjects(user1, user2); XCTAssertEqual(user1.hash, user2.hash); // (2) - ZMSearchUser *user3 = [[ZMSearchUser alloc] initWithContextProvider:self.coreDataStack - name:@"A" - handle:@"b" - accentColor:ZMAccentColor.green - remoteIdentifier:remoteIDB - domain:nil - teamIdentifier:nil - user:nil - searchUsersCache:nil]; + ZMSearchUser *user3 = [[ZMSearchUser alloc] initWithViewContext:self.coreDataStack.viewContext + name:@"A" + handle:@"b" + accentColor:ZMAccentColor.green + remoteIdentifier:remoteIDB + domain:nil + teamIdentifier:nil + user:nil + searchUsersCache:nil]; XCTAssertNotEqualObjects(user1, user3); } @@ -95,15 +95,15 @@ - (void)testThatItHasAllDataItWasInitializedWith NSUUID *remoteID = [NSUUID createUUID]; // when - ZMSearchUser *searchUser = [[ZMSearchUser alloc] initWithContextProvider:self.coreDataStack - name:name - handle:handle - accentColor:ZMAccentColor.green - remoteIdentifier:remoteID - domain:nil - teamIdentifier:nil - user:nil - searchUsersCache:nil]; + ZMSearchUser *searchUser = [[ZMSearchUser alloc] initWithViewContext:self.coreDataStack.viewContext + name:name + handle:handle + accentColor:ZMAccentColor.green + remoteIdentifier:remoteID + domain:nil + teamIdentifier:nil + user:nil + searchUsersCache:nil]; // then @@ -134,15 +134,15 @@ - (void)testThatItUsesDataFromAUserIfItHasOne // when - ZMSearchUser *searchUser = [[ZMSearchUser alloc] initWithContextProvider:self.coreDataStack - name:@"Wrong name" - handle:@"not_my_handle" - accentColor:ZMAccentColor.green - remoteIdentifier:[NSUUID createUUID] - domain:nil - teamIdentifier:nil - user:user - searchUsersCache:nil]; + ZMSearchUser *searchUser = [[ZMSearchUser alloc] initWithViewContext:self.coreDataStack.viewContext + name:@"Wrong name" + handle:@"not_my_handle" + accentColor:ZMAccentColor.green + remoteIdentifier:[NSUUID createUUID] + domain:nil + teamIdentifier:nil + user:user + searchUsersCache:nil]; // then XCTAssertEqualObjects(searchUser.name, user.name); @@ -162,15 +162,15 @@ - (void)testThatItUsesDataFromAUserIfItHasOne - (void)testThatItCanBeConnectedIfItIsNotAlreadyConnected { // given - ZMSearchUser *searchUser = [[ZMSearchUser alloc] initWithContextProvider:self.coreDataStack - name:@"Hans" - handle:@"hans" - accentColor:ZMAccentColor.green - remoteIdentifier:NSUUID.createUUID - domain:nil - teamIdentifier:nil - user:nil - searchUsersCache:nil]; + ZMSearchUser *searchUser = [[ZMSearchUser alloc] initWithViewContext:self.coreDataStack.viewContext + name:@"Hans" + handle:@"hans" + accentColor:ZMAccentColor.green + remoteIdentifier:NSUUID.createUUID + domain:nil + teamIdentifier:nil + user:nil + searchUsersCache:nil]; // then @@ -181,15 +181,15 @@ - (void)testThatItCanBeConnectedIfItIsNotAlreadyConnected - (void)testThatItCanNotBeConnectedIfItHasNoRemoteIdentifier { // given - ZMSearchUser *searchUser = [[ZMSearchUser alloc] initWithContextProvider:self.coreDataStack - name:@"Hans" - handle:@"hans" - accentColor:ZMAccentColor.green - remoteIdentifier:nil - domain:nil - teamIdentifier:nil - user:nil - searchUsersCache:nil]; + ZMSearchUser *searchUser = [[ZMSearchUser alloc] initWithViewContext:self.coreDataStack.viewContext + name:@"Hans" + handle:@"hans" + accentColor:ZMAccentColor.green + remoteIdentifier:nil + domain:nil + teamIdentifier:nil + user:nil + searchUsersCache:nil]; // then XCTAssertFalse(searchUser.canBeConnected); diff --git a/wire-ios-data-model/Tests/Source/Model/UserTypeTests+Materialize.swift b/wire-ios-data-model/Tests/Source/Model/UserTypeTests+Materialize.swift index 3f06a9e2266..e4d9a96df95 100644 --- a/wire-ios-data-model/Tests/Source/Model/UserTypeTests+Materialize.swift +++ b/wire-ios-data-model/Tests/Source/Model/UserTypeTests+Materialize.swift @@ -97,7 +97,7 @@ final class UserTypeTests_Materialize: ModelObjectsTests { teamIdentifier: UUID? ) -> ZMSearchUser { ZMSearchUser( - contextProvider: coreDataStack, + viewContext: coreDataStack.viewContext, name: name.capitalized, handle: name.lowercased(), accentColor: .amber, diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift index 015914b2b5b..7b753d9c401 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift @@ -16,9 +16,9 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // -import XCTest import WireMockTransport import WireTransport +import XCTest @testable import WireSyncEngine From b0ca485e32b8a404bd4a59823c92b835dd3e55fe Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Tue, 3 Feb 2026 21:16:54 +0100 Subject: [PATCH 57/71] fix build errors in sync engine --- .../Source/SessionManager/SessionManager+Backup.swift | 6 +++--- .../Source/SessionManager/SessionManager.swift | 2 +- .../ConnectToBotURLActionProcessor.swift | 2 +- .../ZMUserSession/ZMUserSession+Authentication.swift | 4 ++-- .../ZMUserSession/ZMUserSession+UserSession.swift | 6 +++--- .../Strategies/SearchUserImageStrategyTests.swift | 2 +- .../Tests/Source/UserSession/SearchDirectoryTests.swift | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/wire-ios-sync-engine/Source/SessionManager/SessionManager+Backup.swift b/wire-ios-sync-engine/Source/SessionManager/SessionManager+Backup.swift index e5d84e32e8d..f04b202c020 100644 --- a/wire-ios-sync-engine/Source/SessionManager/SessionManager+Backup.swift +++ b/wire-ios-sync-engine/Source/SessionManager/SessionManager+Backup.swift @@ -31,9 +31,9 @@ extension SessionManager { public func backupActiveAccount(password: String) async throws -> URL { guard let userId = accountManager.selectedAccount?.userIdentifier, - let clientId = activeUserSession?.selfUserClient?.remoteIdentifier, - let handle = activeUserSession.flatMap(ZMUser.selfUser)?.handle, - let activeUserSession + let activeUserSession, + let clientId = activeUserSession.selfUserClient?.remoteIdentifier, + let handle = ZMUser.selfUser(in: activeUserSession.viewContext).handle else { throw CreateLegacyBackupError.noActiveAccountForExport } diff --git a/wire-ios-sync-engine/Source/SessionManager/SessionManager.swift b/wire-ios-sync-engine/Source/SessionManager/SessionManager.swift index 6880ba02af7..02e76f99210 100644 --- a/wire-ios-sync-engine/Source/SessionManager/SessionManager.swift +++ b/wire-ios-sync-engine/Source/SessionManager/SessionManager.swift @@ -1173,7 +1173,7 @@ public final class SessionManager: NSObject, SessionManagerType { fileprivate func registerObservers(account: Account, session: ZMUserSession) { - let selfUser = ZMUser.selfUser(inUserSession: session) + let selfUser = ZMUser.selfUser(in: session.viewContext) let teamObserver = TeamChangeInfo.add( observer: self, for: nil, diff --git a/wire-ios-sync-engine/Source/UserSession/URLActionProcessors/ConnectToBotURLActionProcessor.swift b/wire-ios-sync-engine/Source/UserSession/URLActionProcessors/ConnectToBotURLActionProcessor.swift index 867e0865c6d..8f2420f4e5b 100644 --- a/wire-ios-sync-engine/Source/UserSession/URLActionProcessors/ConnectToBotURLActionProcessor.swift +++ b/wire-ios-sync-engine/Source/UserSession/URLActionProcessors/ConnectToBotURLActionProcessor.swift @@ -44,7 +44,7 @@ final class ConnectToBotURLActionProcessor: NSObject, URLActionProcessor { let providerIdentifier = serviceUserData.provider.transportString() let serviceUser = ZMSearchUser( - contextProvider: contextProvider, + viewContext: contextProvider.viewContext, name: "", handle: nil, accentColor: .blue, diff --git a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+Authentication.swift b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+Authentication.swift index ffd7b53bd08..4fdd9ed13c1 100644 --- a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+Authentication.swift +++ b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+Authentication.swift @@ -98,8 +98,8 @@ extension ZMUserSession { public func logout(credentials: UserEmailCredentials, _ completion: @escaping (Result) -> Void) { guard - let accountID = ZMUser.selfUser(inUserSession: self).remoteIdentifier, - let selfClientIdentifier = ZMUser.selfUser(inUserSession: self).selfClient()?.remoteIdentifier, + let accountID = ZMUser.selfUser(in: viewContext).remoteIdentifier, + let selfClientIdentifier = ZMUser.selfUser(in: viewContext).selfClient()?.remoteIdentifier, let apiVersion = resolvedBackendMetadata.apiVersion else { return diff --git a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+UserSession.swift b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+UserSession.swift index 6d1d49d8796..ad65c4727c2 100644 --- a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+UserSession.swift +++ b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+UserSession.swift @@ -123,15 +123,15 @@ extension ZMUserSession: UserSession { } public var selfUser: any UserType { - ZMUser.selfUser(inUserSession: self) + ZMUser.selfUser(in: viewContext) } public var selfUserLegalHoldSubject: any SelfUserLegalHoldable { - ZMUser.selfUser(inUserSession: self) + ZMUser.selfUser(in: viewContext) } public var editableSelfUser: any EditableUserType & UserType { - ZMUser.selfUser(inUserSession: self) + ZMUser.selfUser(in: viewContext) } public func addUserObserver( diff --git a/wire-ios-sync-engine/Tests/Source/Synchronization/Strategies/SearchUserImageStrategyTests.swift b/wire-ios-sync-engine/Tests/Source/Synchronization/Strategies/SearchUserImageStrategyTests.swift index aea314d11c5..429e5b4085d 100644 --- a/wire-ios-sync-engine/Tests/Source/Synchronization/Strategies/SearchUserImageStrategyTests.swift +++ b/wire-ios-sync-engine/Tests/Source/Synchronization/Strategies/SearchUserImageStrategyTests.swift @@ -55,7 +55,7 @@ final class SearchUserImageStrategyTests: MessagingTest { func createSearchUser() -> ZMSearchUser { ZMSearchUser( - contextProvider: coreDataStack, + viewContext: coreDataStack.viewContext, name: "Foo", handle: "foo", accentColor: .amber, diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift index 764c210fff1..eb410209ff3 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift @@ -72,7 +72,7 @@ final class SearchDirectoryTests: DatabaseTest { private func insertSearchUser(remoteIdentifier: UUID) { _ = ZMSearchUser( - contextProvider: coreDataStack!, + viewContext: coreDataStack!.viewContext, name: "John Doe", handle: "john", accentColor: .amber, From 1f2192e1c92ca806d909826fab356fd017bea3bf Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Tue, 3 Feb 2026 21:21:00 +0100 Subject: [PATCH 58/71] fix build errors in wire-ios --- .../Developer/DeveloperTools/DeveloperToolsViewModel.swift | 2 +- .../Wire-iOS/Sources/Helpers/syncengine/ZMUser+Self.swift | 4 ++-- .../Conversation+OptionsConfiguration.swift | 2 +- .../UserInterface/SelfProfile/SelfProfileViewController.swift | 3 +-- .../Sources/UserInterface/Settings/DebugActions.swift | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/DeveloperToolsViewModel.swift b/wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/DeveloperToolsViewModel.swift index 52f0ea145aa..09722282e74 100644 --- a/wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/DeveloperToolsViewModel.swift +++ b/wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/DeveloperToolsViewModel.swift @@ -456,7 +456,7 @@ final class DeveloperToolsViewModel: ObservableObject { private var selfUser: ZMUser? { guard let userSession else { return nil } - return ZMUser.selfUser(inUserSession: userSession) + return ZMUser.selfUser(in: userSession.viewContext) } private var selfClient: UserClient? { diff --git a/wire-ios/Wire-iOS/Sources/Helpers/syncengine/ZMUser+Self.swift b/wire-ios/Wire-iOS/Sources/Helpers/syncengine/ZMUser+Self.swift index 4cf895e1dae..92b672713fa 100644 --- a/wire-ios/Wire-iOS/Sources/Helpers/syncengine/ZMUser+Self.swift +++ b/wire-ios/Wire-iOS/Sources/Helpers/syncengine/ZMUser+Self.swift @@ -39,7 +39,7 @@ import WireSyncEngine } else { guard let session = ZMUserSession.shared() else { return nil } - return ZMUser.selfUser(inUserSession: session) + return ZMUser.selfUser(in: session.viewContext) } } } @@ -52,7 +52,7 @@ import WireSyncEngine static func selfUser() -> ZMUser? { guard let session = ZMUserSession.shared() else { return nil } - return ZMUser.selfUser(inUserSession: session) + return ZMUser.selfUser(in: session.viewContext) } } #endif diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/Conversation+OptionsConfiguration.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/Conversation+OptionsConfiguration.swift index 26e5d2980f7..c3818654a61 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/Conversation+OptionsConfiguration.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/Conversation+OptionsConfiguration.swift @@ -65,7 +65,7 @@ extension ZMConversation { } var isConversationFromSelfTeam: Bool { - let selfUser = ZMUser.selfUser(inUserSession: userSession) + let selfUser = ZMUser.selfUser(in: userSession.viewContext) return conversation.teamRemoteIdentifier == selfUser.teamIdentifier } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/SelfProfile/SelfProfileViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/SelfProfile/SelfProfileViewController.swift index 6f146e1eae8..e045dcaf4c2 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/SelfProfile/SelfProfileViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/SelfProfile/SelfProfileViewController.swift @@ -288,8 +288,7 @@ final class SelfProfileViewController: UIViewController { private func onTeamCreationBannerInteraction( apiVersion: WireNetwork.APIVersion ) { - let sessionContextProvider = userSession.contextProvider - let user = ZMUser.selfUser(inUserSession: sessionContextProvider) + let user = ZMUser.selfUser(in: userSession.contextProvider.viewContext) guard let userName = user.normalizedName, let useCase = SessionManager.shared?.activeUserSession? .createIndividualToTeamMigrationUseCase() else { diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/DebugActions.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/DebugActions.swift index bd403d0a9f0..49d599fab1c 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/DebugActions.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/DebugActions.swift @@ -166,7 +166,7 @@ enum DebugActions { return } - let selfUser = ZMUser.selfUser(inUserSession: userSession) + let selfUser = ZMUser.selfUser(in: userSession.viewContext) let alert = UIAlertController( title: "Analytics identifier", From 7295e04d734543c42f66cde08de441a9e58fe5e5 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Tue, 3 Feb 2026 21:24:21 +0100 Subject: [PATCH 59/71] remove ZMUser.selfUser(inUserSession:) --- .../Model/User/UserType+Federation.swift | 2 - .../Source/Model/User/ZMSearchUser.swift | 36 +++-- .../Source/Model/User/ZMUser.m | 11 -- wire-ios-data-model/Source/Public/ZMUser.h | 9 -- .../SearchUserObserverCenterTests.swift | 2 +- .../Observer/SearchUserObserverTests.swift | 2 +- .../Observer/SearchUserSnapshotTests.swift | 2 +- .../User/ZMSearchUserTests+Connections.swift | 2 +- .../ZMSearchUserTests+ProfileImages.swift | 2 +- .../User/ZMSearchUserTests+TeamUser.swift | 2 +- .../Source/Model/User/ZMSearchUserTests.m | 126 +++++++++--------- .../Model/UserTypeTests+Materialize.swift | 2 +- .../SessionManager+Backup.swift | 6 +- .../SessionManager/SessionManager.swift | 2 +- .../SessionManager/UserSessionLoader.swift | 3 +- .../ConnectToBotURLActionProcessor.swift | 2 +- .../DeepLinkURLActionProcessor.swift | 1 - .../ZMUserSession+Authentication.swift | 4 +- .../ZMUserSession+UserSession.swift | 6 +- .../SearchUserImageStrategyTests.swift | 2 +- .../UserSession/SearchDirectoryTests.swift | 2 +- .../Source/UserSession/SearchTaskTests.swift | 4 +- .../Sources/AuthenticatedRouter.swift | 4 +- .../DeveloperToolsViewModel.swift | 2 +- .../Helpers/syncengine/ZMUser+Self.swift | 4 +- .../Conversation+OptionsConfiguration.swift | 2 +- ...ntViewController+ShowContentDelegate.swift | 6 +- .../SelfProfileViewController.swift | 3 +- .../UserInterface/Settings/DebugActions.swift | 2 +- 29 files changed, 112 insertions(+), 141 deletions(-) diff --git a/wire-ios-data-model/Source/Model/User/UserType+Federation.swift b/wire-ios-data-model/Source/Model/User/UserType+Federation.swift index 16069797c7a..917ba9ccf68 100644 --- a/wire-ios-data-model/Source/Model/User/UserType+Federation.swift +++ b/wire-ios-data-model/Source/Model/User/UserType+Federation.swift @@ -16,8 +16,6 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // -import Foundation - extension UserType { func isFederating(with otherUser: UserType) -> Bool { diff --git a/wire-ios-data-model/Source/Model/User/ZMSearchUser.swift b/wire-ios-data-model/Source/Model/User/ZMSearchUser.swift index 93762445803..a9e764289b8 100644 --- a/wire-ios-data-model/Source/Model/User/ZMSearchUser.swift +++ b/wire-ios-data-model/Source/Model/User/ZMSearchUser.swift @@ -105,7 +105,7 @@ public class ZMSearchUser: NSObject, UserType { user?.objectId ?? remoteIdentifier! } - fileprivate weak var contextProvider: ContextProvider? + fileprivate weak var viewContext: NSManagedObjectContext? private let searchUsersCache: SearchUsersCache? fileprivate var internalDomain: String? @@ -162,16 +162,12 @@ public class ZMSearchUser: NSObject, UserType { } public var isFederated: Bool { - guard let contextProvider else { - return false - } - - return ZMUser.selfUser(inUserSession: contextProvider).isFederating(with: self) + guard let viewContext else { return false } + return ZMUser.selfUser(in: viewContext).isFederating(with: self) } public var isSelfUser: Bool { guard let user else { return false } - return user.isSelfUser } @@ -265,8 +261,8 @@ public class ZMSearchUser: NSObject, UserType { } public var oneToOneConversation: ZMConversation? { - if isTeamMember, let uiContext = contextProvider?.viewContext { - materialize(in: uiContext)?.oneToOneConversation + if isTeamMember, let viewContext { + materialize(in: viewContext)?.oneToOneConversation } else { user?.oneToOneConversation } @@ -482,7 +478,7 @@ public class ZMSearchUser: NSObject, UserType { @objc public required init( - contextProvider: ContextProvider, + viewContext: NSManagedObjectContext, name: String, handle: String?, accentColor: ZMAccentColor?, @@ -503,10 +499,10 @@ public class ZMSearchUser: NSObject, UserType { self.internalDomain = domain self.remoteIdentifier = existingUser?.remoteIdentifier ?? remoteIdentifier self.teamIdentifier = existingUser?.teamIdentifier ?? teamIdentifier - self.contextProvider = contextProvider + self.viewContext = viewContext self.searchUsersCache = searchUsersCache - let selfUser = ZMUser.selfUser(inUserSession: contextProvider) + let selfUser = ZMUser.selfUser(in: viewContext) self.internalIsTeamMember = teamIdentifier != nil && selfUser.teamIdentifier == teamIdentifier self.internalIsConnected = internalIsTeamMember @@ -524,7 +520,7 @@ public class ZMSearchUser: NSObject, UserType { searchUsersCache: SearchUsersCache? ) { self.init( - contextProvider: contextProvider, + viewContext: contextProvider.viewContext, name: user.name ?? "", handle: user.handle, accentColor: user.zmAccentColor, @@ -556,7 +552,7 @@ public class ZMSearchUser: NSObject, UserType { let accentColorRawValue = (payload["accent_id"] as? NSNumber)?.int16Value ?? 0 self.init( - contextProvider: contextProvider, + viewContext: contextProvider.viewContext, name: name, handle: handle, accentColor: .from(rawValue: accentColorRawValue) ?? .default, @@ -611,7 +607,7 @@ public class ZMSearchUser: NSObject, UserType { } public func connect(completion: @escaping (Error?) -> Void) { - let selfUser = ZMUser.selfUser(inUserSession: contextProvider!) + let selfUser = ZMUser.selfUser(in: viewContext!) selfUser.sendConnectionRequest(to: self) { [weak self] result in switch result { case .success: @@ -628,7 +624,7 @@ public class ZMSearchUser: NSObject, UserType { private func updateLocalUser() { guard let userID = remoteIdentifier, - let viewContext = contextProvider?.viewContext + let viewContext else { return } @@ -637,7 +633,7 @@ public class ZMSearchUser: NSObject, UserType { } private func notifySearchUserChanged() { - contextProvider?.viewContext.searchUserObserverCenter.notifyUpdatedSearchUser(self) + viewContext?.searchUserObserverCenter.notifyUpdatedSearchUser(self) } public func accept(completion: @escaping (Error?) -> Void) { @@ -671,7 +667,7 @@ public class ZMSearchUser: NSObject, UserType { if let user { user.requestPreviewProfileImage() - } else if let notificationContext = contextProvider?.viewContext.notificationContext { + } else if let notificationContext = viewContext?.notificationContext { NotificationInContext( name: .searchUserDidRequestPreviewAsset, context: notificationContext, @@ -686,7 +682,7 @@ public class ZMSearchUser: NSObject, UserType { if let user { user.requestCompleteProfileImage() - } else if let notificationContext = contextProvider?.viewContext.notificationContext { + } else if let notificationContext = viewContext?.notificationContext { NotificationInContext( name: .searchUserDidRequestCompleteAsset, context: notificationContext, @@ -731,7 +727,7 @@ public class ZMSearchUser: NSObject, UserType { internalCompleteImageData = imageData } - contextProvider?.viewContext.searchUserObserverCenter.notifyUpdatedSearchUser(self) + viewContext?.searchUserObserverCenter.notifyUpdatedSearchUser(self) } public func update(from payload: [String: Any]) { diff --git a/wire-ios-data-model/Source/Model/User/ZMUser.m b/wire-ios-data-model/Source/Model/User/ZMUser.m index 52e3ac4f8a6..a3d22619e1b 100644 --- a/wire-ios-data-model/Source/Model/User/ZMUser.m +++ b/wire-ios-data-model/Source/Model/User/ZMUser.m @@ -717,17 +717,6 @@ + (instancetype)selfUserInContext:(NSManagedObjectContext *)moc; @end -@implementation ZMUser (Utilities) - -+ (ZMUser *)selfUserInUserSession:(id)session -{ - VerifyReturnNil(session != nil); - return [self selfUserInContext:session.viewContext]; -} - -@end - - @implementation ZMUser (Editable) diff --git a/wire-ios-data-model/Source/Public/ZMUser.h b/wire-ios-data-model/Source/Public/ZMUser.h index 9698d32a57a..e360f881463 100644 --- a/wire-ios-data-model/Source/Public/ZMUser.h +++ b/wire-ios-data-model/Source/Public/ZMUser.h @@ -83,15 +83,6 @@ typedef NS_ENUM(int16_t, ZMBlockState) { @end -@protocol ZMEditableUserType; - -@interface ZMUser (Utilities) - -+ (ZMUser *_Nonnull)selfUserInUserSession:(id _Nonnull)session; - -@end - - @interface ZMUser (Connections) diff --git a/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserObserverCenterTests.swift b/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserObserverCenterTests.swift index 4baaf034d6c..bf9a9fa503e 100644 --- a/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserObserverCenterTests.swift +++ b/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserObserverCenterTests.swift @@ -170,7 +170,7 @@ final class SearchUserObserverCenterTests: ModelObjectsTests { user: ZMUser? = nil ) -> ZMSearchUser { ZMSearchUser( - contextProvider: coreDataStack, + viewContext: coreDataStack.viewContext, name: name, handle: handle, accentColor: accentColor, diff --git a/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserObserverTests.swift b/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserObserverTests.swift index d3b72eb16e9..8c235f782e0 100644 --- a/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserObserverTests.swift +++ b/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserObserverTests.swift @@ -137,7 +137,7 @@ final class SearchUserObserverTests: NotificationDispatcherTestBase { user: ZMUser? = nil ) -> ZMSearchUser { ZMSearchUser( - contextProvider: coreDataStack, + viewContext: coreDataStack.viewContext, name: name, handle: name.lowercased(), accentColor: .amber, diff --git a/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserSnapshotTests.swift b/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserSnapshotTests.swift index 6cae178855c..449150565af 100644 --- a/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserSnapshotTests.swift +++ b/wire-ios-data-model/Tests/Source/Model/Observer/SearchUserSnapshotTests.swift @@ -225,7 +225,7 @@ final class SearchUserSnapshotTests: ZMBaseManagedObjectTest { user: ZMUser? = nil ) -> ZMSearchUser { ZMSearchUser( - contextProvider: coreDataStack, + viewContext: coreDataStack.viewContext, name: name, handle: handle, accentColor: accentColor, diff --git a/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+Connections.swift b/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+Connections.swift index 97fd22103a3..2ba5a5e54a9 100644 --- a/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+Connections.swift +++ b/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+Connections.swift @@ -24,7 +24,7 @@ final class ZMSearchUserTests_Connections: ModelObjectsTests { func testThatConnectSendsAConnectToUserNotification() { // given let searchUser = ZMSearchUser( - contextProvider: coreDataStack, + viewContext: coreDataStack.viewContext, name: "John Doe", handle: "johndoe", accentColor: .turquoise, diff --git a/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+ProfileImages.swift b/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+ProfileImages.swift index 944b9585a06..b568e530f50 100644 --- a/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+ProfileImages.swift +++ b/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+ProfileImages.swift @@ -177,7 +177,7 @@ final class ZMSearchUserTests_ProfileImages: ZMBaseManagedObjectTest { user: ZMUser? = nil ) -> ZMSearchUser { ZMSearchUser( - contextProvider: coreDataStack, + viewContext: coreDataStack.viewContext, name: name, handle: name.lowercased(), accentColor: .amber, diff --git a/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+TeamUser.swift b/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+TeamUser.swift index 8c0f9184415..d0c457927b9 100644 --- a/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+TeamUser.swift +++ b/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests+TeamUser.swift @@ -73,7 +73,7 @@ final class ZMSearchUserTests_TeamUser: ModelObjectsTests { private func makeSearchUser(teamIdentifier: UUID?) -> ZMSearchUser { ZMSearchUser( - contextProvider: coreDataStack, + viewContext: coreDataStack.viewContext, name: "Foo", handle: "foo", accentColor: .amber, diff --git a/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests.m b/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests.m index 7756f71c07a..7978fd77f9c 100644 --- a/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests.m +++ b/wire-ios-data-model/Tests/Source/Model/User/ZMSearchUserTests.m @@ -49,40 +49,40 @@ - (void)testThatItComparesEqualBasedOnRemoteID; NSUUID *remoteIDA = [NSUUID createUUID]; NSUUID *remoteIDB = [NSUUID createUUID]; - ZMSearchUser *user1 = [[ZMSearchUser alloc] initWithContextProvider:self.coreDataStack - name:@"A" - handle:@"a" - accentColor:ZMAccentColor.green - remoteIdentifier:remoteIDA - domain:nil - teamIdentifier:nil - user:nil - searchUsersCache:nil]; + ZMSearchUser *user1 = [[ZMSearchUser alloc] initWithViewContext:self.coreDataStack.viewContext + name:@"A" + handle:@"a" + accentColor:ZMAccentColor.green + remoteIdentifier:remoteIDA + domain:nil + teamIdentifier:nil + user:nil + searchUsersCache:nil]; // (1) - ZMSearchUser *user2 = [[ZMSearchUser alloc] initWithContextProvider:self.coreDataStack - name:@"B" - handle:@"b" - accentColor:ZMAccentColor.purple - remoteIdentifier:remoteIDA - domain:nil - teamIdentifier:nil - user:nil - searchUsersCache:nil]; + ZMSearchUser *user2 = [[ZMSearchUser alloc] initWithViewContext:self.coreDataStack.viewContext + name:@"B" + handle:@"b" + accentColor:ZMAccentColor.purple + remoteIdentifier:remoteIDA + domain:nil + teamIdentifier:nil + user:nil + searchUsersCache:nil]; XCTAssertEqualObjects(user1, user2); XCTAssertEqual(user1.hash, user2.hash); // (2) - ZMSearchUser *user3 = [[ZMSearchUser alloc] initWithContextProvider:self.coreDataStack - name:@"A" - handle:@"b" - accentColor:ZMAccentColor.green - remoteIdentifier:remoteIDB - domain:nil - teamIdentifier:nil - user:nil - searchUsersCache:nil]; + ZMSearchUser *user3 = [[ZMSearchUser alloc] initWithViewContext:self.coreDataStack.viewContext + name:@"A" + handle:@"b" + accentColor:ZMAccentColor.green + remoteIdentifier:remoteIDB + domain:nil + teamIdentifier:nil + user:nil + searchUsersCache:nil]; XCTAssertNotEqualObjects(user1, user3); } @@ -95,15 +95,15 @@ - (void)testThatItHasAllDataItWasInitializedWith NSUUID *remoteID = [NSUUID createUUID]; // when - ZMSearchUser *searchUser = [[ZMSearchUser alloc] initWithContextProvider:self.coreDataStack - name:name - handle:handle - accentColor:ZMAccentColor.green - remoteIdentifier:remoteID - domain:nil - teamIdentifier:nil - user:nil - searchUsersCache:nil]; + ZMSearchUser *searchUser = [[ZMSearchUser alloc] initWithViewContext:self.coreDataStack.viewContext + name:name + handle:handle + accentColor:ZMAccentColor.green + remoteIdentifier:remoteID + domain:nil + teamIdentifier:nil + user:nil + searchUsersCache:nil]; // then @@ -134,15 +134,15 @@ - (void)testThatItUsesDataFromAUserIfItHasOne // when - ZMSearchUser *searchUser = [[ZMSearchUser alloc] initWithContextProvider:self.coreDataStack - name:@"Wrong name" - handle:@"not_my_handle" - accentColor:ZMAccentColor.green - remoteIdentifier:[NSUUID createUUID] - domain:nil - teamIdentifier:nil - user:user - searchUsersCache:nil]; + ZMSearchUser *searchUser = [[ZMSearchUser alloc] initWithViewContext:self.coreDataStack.viewContext + name:@"Wrong name" + handle:@"not_my_handle" + accentColor:ZMAccentColor.green + remoteIdentifier:[NSUUID createUUID] + domain:nil + teamIdentifier:nil + user:user + searchUsersCache:nil]; // then XCTAssertEqualObjects(searchUser.name, user.name); @@ -162,15 +162,15 @@ - (void)testThatItUsesDataFromAUserIfItHasOne - (void)testThatItCanBeConnectedIfItIsNotAlreadyConnected { // given - ZMSearchUser *searchUser = [[ZMSearchUser alloc] initWithContextProvider:self.coreDataStack - name:@"Hans" - handle:@"hans" - accentColor:ZMAccentColor.green - remoteIdentifier:NSUUID.createUUID - domain:nil - teamIdentifier:nil - user:nil - searchUsersCache:nil]; + ZMSearchUser *searchUser = [[ZMSearchUser alloc] initWithViewContext:self.coreDataStack.viewContext + name:@"Hans" + handle:@"hans" + accentColor:ZMAccentColor.green + remoteIdentifier:NSUUID.createUUID + domain:nil + teamIdentifier:nil + user:nil + searchUsersCache:nil]; // then @@ -181,15 +181,15 @@ - (void)testThatItCanBeConnectedIfItIsNotAlreadyConnected - (void)testThatItCanNotBeConnectedIfItHasNoRemoteIdentifier { // given - ZMSearchUser *searchUser = [[ZMSearchUser alloc] initWithContextProvider:self.coreDataStack - name:@"Hans" - handle:@"hans" - accentColor:ZMAccentColor.green - remoteIdentifier:nil - domain:nil - teamIdentifier:nil - user:nil - searchUsersCache:nil]; + ZMSearchUser *searchUser = [[ZMSearchUser alloc] initWithViewContext:self.coreDataStack.viewContext + name:@"Hans" + handle:@"hans" + accentColor:ZMAccentColor.green + remoteIdentifier:nil + domain:nil + teamIdentifier:nil + user:nil + searchUsersCache:nil]; // then XCTAssertFalse(searchUser.canBeConnected); diff --git a/wire-ios-data-model/Tests/Source/Model/UserTypeTests+Materialize.swift b/wire-ios-data-model/Tests/Source/Model/UserTypeTests+Materialize.swift index 3f06a9e2266..e4d9a96df95 100644 --- a/wire-ios-data-model/Tests/Source/Model/UserTypeTests+Materialize.swift +++ b/wire-ios-data-model/Tests/Source/Model/UserTypeTests+Materialize.swift @@ -97,7 +97,7 @@ final class UserTypeTests_Materialize: ModelObjectsTests { teamIdentifier: UUID? ) -> ZMSearchUser { ZMSearchUser( - contextProvider: coreDataStack, + viewContext: coreDataStack.viewContext, name: name.capitalized, handle: name.lowercased(), accentColor: .amber, diff --git a/wire-ios-sync-engine/Source/SessionManager/SessionManager+Backup.swift b/wire-ios-sync-engine/Source/SessionManager/SessionManager+Backup.swift index e5d84e32e8d..f04b202c020 100644 --- a/wire-ios-sync-engine/Source/SessionManager/SessionManager+Backup.swift +++ b/wire-ios-sync-engine/Source/SessionManager/SessionManager+Backup.swift @@ -31,9 +31,9 @@ extension SessionManager { public func backupActiveAccount(password: String) async throws -> URL { guard let userId = accountManager.selectedAccount?.userIdentifier, - let clientId = activeUserSession?.selfUserClient?.remoteIdentifier, - let handle = activeUserSession.flatMap(ZMUser.selfUser)?.handle, - let activeUserSession + let activeUserSession, + let clientId = activeUserSession.selfUserClient?.remoteIdentifier, + let handle = ZMUser.selfUser(in: activeUserSession.viewContext).handle else { throw CreateLegacyBackupError.noActiveAccountForExport } diff --git a/wire-ios-sync-engine/Source/SessionManager/SessionManager.swift b/wire-ios-sync-engine/Source/SessionManager/SessionManager.swift index 6880ba02af7..02e76f99210 100644 --- a/wire-ios-sync-engine/Source/SessionManager/SessionManager.swift +++ b/wire-ios-sync-engine/Source/SessionManager/SessionManager.swift @@ -1173,7 +1173,7 @@ public final class SessionManager: NSObject, SessionManagerType { fileprivate func registerObservers(account: Account, session: ZMUserSession) { - let selfUser = ZMUser.selfUser(inUserSession: session) + let selfUser = ZMUser.selfUser(in: session.viewContext) let teamObserver = TeamChangeInfo.add( observer: self, for: nil, diff --git a/wire-ios-sync-engine/Source/SessionManager/UserSessionLoader.swift b/wire-ios-sync-engine/Source/SessionManager/UserSessionLoader.swift index 2ac09b05294..d542c0ef8e7 100644 --- a/wire-ios-sync-engine/Source/SessionManager/UserSessionLoader.swift +++ b/wire-ios-sync-engine/Source/SessionManager/UserSessionLoader.swift @@ -469,8 +469,7 @@ final class UserSessionLoader { accountID: accountID, databaseContexts: [ coreDataStack.viewContext, - coreDataStack.syncContext, - coreDataStack.searchContext + coreDataStack.syncContext ], canPerformKeyMigration: true, sharedUserDefaults: sharedUserDefaults, diff --git a/wire-ios-sync-engine/Source/UserSession/URLActionProcessors/ConnectToBotURLActionProcessor.swift b/wire-ios-sync-engine/Source/UserSession/URLActionProcessors/ConnectToBotURLActionProcessor.swift index 867e0865c6d..8f2420f4e5b 100644 --- a/wire-ios-sync-engine/Source/UserSession/URLActionProcessors/ConnectToBotURLActionProcessor.swift +++ b/wire-ios-sync-engine/Source/UserSession/URLActionProcessors/ConnectToBotURLActionProcessor.swift @@ -44,7 +44,7 @@ final class ConnectToBotURLActionProcessor: NSObject, URLActionProcessor { let providerIdentifier = serviceUserData.provider.transportString() let serviceUser = ZMSearchUser( - contextProvider: contextProvider, + viewContext: contextProvider.viewContext, name: "", handle: nil, accentColor: .blue, diff --git a/wire-ios-sync-engine/Source/UserSession/URLActionProcessors/DeepLinkURLActionProcessor.swift b/wire-ios-sync-engine/Source/UserSession/URLActionProcessors/DeepLinkURLActionProcessor.swift index fc230b3cc11..daed7dcbc71 100644 --- a/wire-ios-sync-engine/Source/UserSession/URLActionProcessors/DeepLinkURLActionProcessor.swift +++ b/wire-ios-sync-engine/Source/UserSession/URLActionProcessors/DeepLinkURLActionProcessor.swift @@ -209,7 +209,6 @@ class DeepLinkURLActionProcessor: URLActionProcessor { private func handleOpenUserProfile(id: UUID, domain: String?, delegate: PresentationDelegate?) { let viewContext = contextProvider.viewContext - if let user = ZMUser.fetch(with: id, domain: domain, in: viewContext) { delegate?.showUserProfile(user: user) } else { diff --git a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+Authentication.swift b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+Authentication.swift index ffd7b53bd08..4fdd9ed13c1 100644 --- a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+Authentication.swift +++ b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+Authentication.swift @@ -98,8 +98,8 @@ extension ZMUserSession { public func logout(credentials: UserEmailCredentials, _ completion: @escaping (Result) -> Void) { guard - let accountID = ZMUser.selfUser(inUserSession: self).remoteIdentifier, - let selfClientIdentifier = ZMUser.selfUser(inUserSession: self).selfClient()?.remoteIdentifier, + let accountID = ZMUser.selfUser(in: viewContext).remoteIdentifier, + let selfClientIdentifier = ZMUser.selfUser(in: viewContext).selfClient()?.remoteIdentifier, let apiVersion = resolvedBackendMetadata.apiVersion else { return diff --git a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+UserSession.swift b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+UserSession.swift index 6d1d49d8796..ad65c4727c2 100644 --- a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+UserSession.swift +++ b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+UserSession.swift @@ -123,15 +123,15 @@ extension ZMUserSession: UserSession { } public var selfUser: any UserType { - ZMUser.selfUser(inUserSession: self) + ZMUser.selfUser(in: viewContext) } public var selfUserLegalHoldSubject: any SelfUserLegalHoldable { - ZMUser.selfUser(inUserSession: self) + ZMUser.selfUser(in: viewContext) } public var editableSelfUser: any EditableUserType & UserType { - ZMUser.selfUser(inUserSession: self) + ZMUser.selfUser(in: viewContext) } public func addUserObserver( diff --git a/wire-ios-sync-engine/Tests/Source/Synchronization/Strategies/SearchUserImageStrategyTests.swift b/wire-ios-sync-engine/Tests/Source/Synchronization/Strategies/SearchUserImageStrategyTests.swift index aea314d11c5..429e5b4085d 100644 --- a/wire-ios-sync-engine/Tests/Source/Synchronization/Strategies/SearchUserImageStrategyTests.swift +++ b/wire-ios-sync-engine/Tests/Source/Synchronization/Strategies/SearchUserImageStrategyTests.swift @@ -55,7 +55,7 @@ final class SearchUserImageStrategyTests: MessagingTest { func createSearchUser() -> ZMSearchUser { ZMSearchUser( - contextProvider: coreDataStack, + viewContext: coreDataStack.viewContext, name: "Foo", handle: "foo", accentColor: .amber, diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift index 9e23de73b85..d17c42be4a0 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/SearchDirectoryTests.swift @@ -73,7 +73,7 @@ final class SearchDirectoryTests: DatabaseTest { private func insertSearchUser(remoteIdentifier: UUID) { _ = ZMSearchUser( - contextProvider: coreDataStack!, + viewContext: coreDataStack!.viewContext, name: "John Doe", handle: "john", accentColor: .amber, diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift index bea95e9daac..051d27d41e4 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/SearchTaskTests.swift @@ -16,10 +16,10 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // -import XCTest - import WireMockTransport import WireTransport +import XCTest + @testable import WireSyncEngine final class SearchTaskTests: DatabaseTest { diff --git a/wire-ios/Wire-iOS/Sources/AuthenticatedRouter.swift b/wire-ios/Wire-iOS/Sources/AuthenticatedRouter.swift index 5e9c4316e17..26f9975ba37 100644 --- a/wire-ios/Wire-iOS/Sources/AuthenticatedRouter.swift +++ b/wire-ios/Wire-iOS/Sources/AuthenticatedRouter.swift @@ -176,8 +176,8 @@ extension AuthenticatedRouter: AuthenticatedRouterProtocol { func navigate(to destination: NavigationDestination) { switch destination { - case let .conversation(converation, message): - _zClientViewController?.showConversation(converation, at: message) + case let .conversation(conversation, message): + _zClientViewController?.showConversation(conversation, at: message) case let .connectionRequest(qualifiedID): _zClientViewController?.showConnectionRequest(qualifiedID: qualifiedID) case .conversationList: diff --git a/wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/DeveloperToolsViewModel.swift b/wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/DeveloperToolsViewModel.swift index 52f0ea145aa..09722282e74 100644 --- a/wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/DeveloperToolsViewModel.swift +++ b/wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/DeveloperToolsViewModel.swift @@ -456,7 +456,7 @@ final class DeveloperToolsViewModel: ObservableObject { private var selfUser: ZMUser? { guard let userSession else { return nil } - return ZMUser.selfUser(inUserSession: userSession) + return ZMUser.selfUser(in: userSession.viewContext) } private var selfClient: UserClient? { diff --git a/wire-ios/Wire-iOS/Sources/Helpers/syncengine/ZMUser+Self.swift b/wire-ios/Wire-iOS/Sources/Helpers/syncengine/ZMUser+Self.swift index 4cf895e1dae..92b672713fa 100644 --- a/wire-ios/Wire-iOS/Sources/Helpers/syncengine/ZMUser+Self.swift +++ b/wire-ios/Wire-iOS/Sources/Helpers/syncengine/ZMUser+Self.swift @@ -39,7 +39,7 @@ import WireSyncEngine } else { guard let session = ZMUserSession.shared() else { return nil } - return ZMUser.selfUser(inUserSession: session) + return ZMUser.selfUser(in: session.viewContext) } } } @@ -52,7 +52,7 @@ import WireSyncEngine static func selfUser() -> ZMUser? { guard let session = ZMUserSession.shared() else { return nil } - return ZMUser.selfUser(inUserSession: session) + return ZMUser.selfUser(in: session.viewContext) } } #endif diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/Conversation+OptionsConfiguration.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/Conversation+OptionsConfiguration.swift index 26e5d2980f7..c3818654a61 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/Conversation+OptionsConfiguration.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/Conversation+OptionsConfiguration.swift @@ -65,7 +65,7 @@ extension ZMConversation { } var isConversationFromSelfTeam: Bool { - let selfUser = ZMUser.selfUser(inUserSession: userSession) + let selfUser = ZMUser.selfUser(in: userSession.viewContext) return conversation.teamRemoteIdentifier == selfUser.teamIdentifier } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController+ShowContentDelegate.swift b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController+ShowContentDelegate.swift index 68b5462503c..ef398b79b96 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController+ShowContentDelegate.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController+ShowContentDelegate.swift @@ -30,7 +30,7 @@ extension ZClientViewController { } func showConnectionRequest(qualifiedID: QualifiedID) { - let searchUserViewConroller = SearchUserViewController( + let searchUserViewController = SearchUserViewController( qualifiedID: qualifiedID, profileViewControllerDelegate: self, userSession: userSession, @@ -39,8 +39,8 @@ extension ZClientViewController { conversationCreationRepository: conversationCreationRepository ) - Task { - await wrapInNavigationControllerAndPresent(viewController: searchUserViewConroller) + Task { @MainActor in + await wrapInNavigationControllerAndPresent(viewController: searchUserViewController) } } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/SelfProfile/SelfProfileViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/SelfProfile/SelfProfileViewController.swift index 6f146e1eae8..e045dcaf4c2 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/SelfProfile/SelfProfileViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/SelfProfile/SelfProfileViewController.swift @@ -288,8 +288,7 @@ final class SelfProfileViewController: UIViewController { private func onTeamCreationBannerInteraction( apiVersion: WireNetwork.APIVersion ) { - let sessionContextProvider = userSession.contextProvider - let user = ZMUser.selfUser(inUserSession: sessionContextProvider) + let user = ZMUser.selfUser(in: userSession.contextProvider.viewContext) guard let userName = user.normalizedName, let useCase = SessionManager.shared?.activeUserSession? .createIndividualToTeamMigrationUseCase() else { diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/DebugActions.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/DebugActions.swift index bd403d0a9f0..49d599fab1c 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Settings/DebugActions.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Settings/DebugActions.swift @@ -166,7 +166,7 @@ enum DebugActions { return } - let selfUser = ZMUser.selfUser(inUserSession: userSession) + let selfUser = ZMUser.selfUser(in: userSession.viewContext) let alert = UIAlertController( title: "Analytics identifier", From 18b47dbe457ffe67f07654f770d0bb48b961bc77 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Tue, 3 Feb 2026 23:08:06 +0100 Subject: [PATCH 60/71] Trigger CI From 75f527f62f35c95b76d57a523ef69b9380885321 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Wed, 4 Feb 2026 07:26:50 +0100 Subject: [PATCH 61/71] revert change --- .../Source/SessionManager/UserSessionLoader.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wire-ios-sync-engine/Source/SessionManager/UserSessionLoader.swift b/wire-ios-sync-engine/Source/SessionManager/UserSessionLoader.swift index d542c0ef8e7..2ac09b05294 100644 --- a/wire-ios-sync-engine/Source/SessionManager/UserSessionLoader.swift +++ b/wire-ios-sync-engine/Source/SessionManager/UserSessionLoader.swift @@ -469,7 +469,8 @@ final class UserSessionLoader { accountID: accountID, databaseContexts: [ coreDataStack.viewContext, - coreDataStack.syncContext + coreDataStack.syncContext, + coreDataStack.searchContext ], canPerformKeyMigration: true, sharedUserDefaults: sharedUserDefaults, From c2b8c2aa347dda24fd60c2c4a2246aad1b743317 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Wed, 4 Feb 2026 07:34:07 +0100 Subject: [PATCH 62/71] clean up searchMOC properties --- .../Tests/Source/Model/ZMBaseManagedObjectTest.h | 1 - wire-ios-sync-engine/Tests/Source/MessagingTest.h | 1 - wire-ios-sync-engine/Tests/Source/MessagingTest.m | 3 --- 3 files changed, 5 deletions(-) diff --git a/wire-ios-data-model/Tests/Source/Model/ZMBaseManagedObjectTest.h b/wire-ios-data-model/Tests/Source/Model/ZMBaseManagedObjectTest.h index 02fdc886094..efff622d4d7 100644 --- a/wire-ios-data-model/Tests/Source/Model/ZMBaseManagedObjectTest.h +++ b/wire-ios-data-model/Tests/Source/Model/ZMBaseManagedObjectTest.h @@ -44,7 +44,6 @@ @property (nonatomic, readonly, nonnull) CoreDataStack *coreDataStack; @property (nonatomic, readonly, nonnull) NSManagedObjectContext *uiMOC; @property (nonatomic, readonly, nonnull) NSManagedObjectContext *syncMOC; -@property (nonatomic, readonly, nonnull) NSManagedObjectContext *searchMOC; @property (nonatomic, readonly) BOOL shouldUseRealKeychain; @property (nonatomic, readonly) BOOL shouldUseInMemoryStore; diff --git a/wire-ios-sync-engine/Tests/Source/MessagingTest.h b/wire-ios-sync-engine/Tests/Source/MessagingTest.h index ab89dfedb34..36f0897fa01 100644 --- a/wire-ios-sync-engine/Tests/Source/MessagingTest.h +++ b/wire-ios-sync-engine/Tests/Source/MessagingTest.h @@ -46,7 +46,6 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) NSManagedObjectContext *uiMOC; @property (nonatomic, readonly) NSManagedObjectContext *syncMOC; -@property (nonatomic, readonly) NSManagedObjectContext *searchMOC; @property (nonatomic, readonly) NSManagedObjectContext *eventMOC; @property (nonatomic, readonly) CoreDataStack *coreDataStack; @property (nonatomic, readonly) ApplicationMock *application; diff --git a/wire-ios-sync-engine/Tests/Source/MessagingTest.m b/wire-ios-sync-engine/Tests/Source/MessagingTest.m index a2ad59ccee3..5c7de75226a 100644 --- a/wire-ios-sync-engine/Tests/Source/MessagingTest.m +++ b/wire-ios-sync-engine/Tests/Source/MessagingTest.m @@ -270,9 +270,6 @@ - (NSArray *)allManagedObjectContexts; if (self.syncMOC != nil) { [result addObject:self.syncMOC]; } - if (self.searchMOC != nil) { - [result addObject:self.searchMOC]; - } if (self.mockTransportSession.managedObjectContext != nil) { [result addObject:self.mockTransportSession.managedObjectContext]; } From 471d2c16f505592b2ce1b56ed69237a6460726d5 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Wed, 4 Feb 2026 07:47:26 +0100 Subject: [PATCH 63/71] fix merge conflicts --- .../UserSession/Search/SearchTask.swift | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index fd96a9a34d3..0a86d7d8920 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -145,7 +145,12 @@ extension SearchTask { options.updateForSelfUserTeamRole(selfUser: selfUser) /// search for the local user with matching user ID and active - let activeMembers = self.teamMembers(matchingQuery: "", team: selfUser.team, searchOptions: options) + let activeMembers = self.teamMembers( + matchingQuery: "", + team: selfUser.team, + searchOptions: options, + in: searchContext + ) let teamMembers = activeMembers .filter { $0.remoteIdentifier == qualifiedID.uuid } .compactMap(\.user) @@ -275,9 +280,11 @@ extension SearchTask { } } - private func filterNonActiveTeamMembers(members: [Member]) -> [Member] { - let searchContext = contextProvider.newBackgroundContext() - let activeConversations = ZMUser.selfUser(in: searchContext).activeConversations + private func filterNonActiveTeamMembers( + members: [Member], + in context: NSManagedObjectContext + ) -> [Member] { + let activeConversations = ZMUser.selfUser(in: context).activeConversations let activeContacts = Set(activeConversations.flatMap(\.localParticipants)) let selfUser = ZMUser.selfUser(in: context) @@ -287,7 +294,12 @@ extension SearchTask { } } - private func teamMembers(matchingQuery query: String, team: Team?, searchOptions: SearchOptions) -> [Member] { + private func teamMembers( + matchingQuery query: String, + team: Team?, + searchOptions: SearchOptions, + in context: NSManagedObjectContext + ) -> [Member] { var partialResult = team?.members(matchingQuery: query) ?? [] if searchOptions.contains(.excludeNonActiveTeamMembers) { @@ -299,9 +311,8 @@ extension SearchTask { if searchOptions.contains(.excludeNonActivePartners) { let query = query.strippingLeadingAtSign() - let searchContext = contextProvider.newBackgroundContext() // TODO: which context to work on actually? test also manually - let selfUser = ZMUser.selfUser(in: searchContext) - let activeConversations = ZMUser.selfUser(in: searchContext).activeConversations + let selfUser = ZMUser.selfUser(in: context) + let activeConversations = ZMUser.selfUser(in: context).activeConversations let activeContacts = Set(activeConversations.flatMap(\.localParticipants)) partialResult = partialResult.filter { membership in @@ -334,7 +345,11 @@ extension SearchTask { return context.fetchOrAssert(request: fetchRequest) as? [ZMUser] ?? [] } - private func conversations(matchingQuery query: SearchRequest.Query, selfUser: ZMUser) -> [ZMConversation] { + private func conversations( + matchingQuery query: SearchRequest.Query, + selfUser: ZMUser, + in context: NSManagedObjectContext + ) -> [ZMConversation] { // swiftlint:disable:next todo_requires_jira_link // TODO: use the interface with team param? let fetchRequest = ZMConversation.sortedFetchRequest(with: ZMConversation.predicate( From 41387804ff28862067e3188dca2ae3fb0479010f Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Wed, 4 Feb 2026 09:49:45 +0100 Subject: [PATCH 64/71] fix API violation --- .../Source/UserSession/Search/SearchTask.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 0a86d7d8920..7f297981605 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -358,9 +358,7 @@ extension SearchTask { )) fetchRequest.sortDescriptors = [NSSortDescriptor(key: ZMNormalizedUserDefinedNameKey, ascending: true)] - let searchContext = contextProvider.newBackgroundContext() - var conversations = searchContext - .fetchOrAssert(request: fetchRequest) as? [ZMConversation] ?? [] + var conversations = context.fetchOrAssert(request: fetchRequest) as? [ZMConversation] ?? [] if query.isHandleQuery { // if we are searching for a username only include conversations with matching displayName From 9078d19395f9d14e7a65a3781caa64c729819be0 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Wed, 4 Feb 2026 09:55:27 +0100 Subject: [PATCH 65/71] fix comment --- .../Source/UserSession/Search/SearchTask.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 7f297981605..c189eee3c43 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -38,10 +38,11 @@ public final class SearchTask { /// 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 three different ways of aggregating search 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? From f5b16d18fb06985434c881df993438b18adee0bb Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Wed, 4 Feb 2026 09:56:22 +0100 Subject: [PATCH 66/71] remove comments --- .../Source/UserSession/Search/SearchTask.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index c189eee3c43..84b38f44fcb 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -131,7 +131,7 @@ public final class SearchTask { extension SearchTask { /// Look up a user ID from contacts and teamMembers locally. - private func performLocalLookup() async -> SearchResultAggregator { // TODO: create subtask struct + private func performLocalLookup() async -> SearchResultAggregator { guard case let .lookup(qualifiedID) = type else { return { _ in } @@ -197,7 +197,7 @@ extension SearchTask { } - /* private */ func performLocalSearch() async -> SearchResultAggregator { // TODO: make private + func performLocalSearch() async -> SearchResultAggregator { guard case let .search(request) = type else { return { _ in } } @@ -386,7 +386,7 @@ extension SearchTask { extension SearchTask { - /* private */ func performUserLookup() async -> SearchResultAggregator { // TODO: make private + func performUserLookup() async -> SearchResultAggregator { guard case let .lookup(qualifiedID) = type, let apiVersion @@ -431,7 +431,7 @@ extension SearchTask { extension SearchTask { - /* private */ func performRemoteSearch() async -> SearchResultAggregator { // TODO: make private + func performRemoteSearch() async -> SearchResultAggregator { guard let apiVersion, apiVersion >= .v1, @@ -581,7 +581,7 @@ extension SearchTask { extension SearchTask { - /* private */ func performRemoteSearchForTeamUser() async -> SearchResultAggregator { // TODO: make private + func performRemoteSearchForTeamUser() async -> SearchResultAggregator { guard let apiVersion, apiVersion <= .v1, @@ -672,7 +672,7 @@ extension SearchTask { extension SearchTask { - /* private */ func performRemoteSearchForServices() async -> SearchResultAggregator { // TODO: create subtask struct + func performRemoteSearchForServices() async -> SearchResultAggregator { let searchContext = contextProvider.newBackgroundContext() let teamIdentifier = await searchContext.perform { @@ -717,7 +717,7 @@ extension SearchTask { } } - /* private */ static func servicesSearchRequest( // TODO: make private + static func servicesSearchRequest( teamIdentifier: UUID, query: String, apiVersion: APIVersion From 99c93121e199d3e2294be7ad8fea4016861b887b Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Wed, 4 Feb 2026 10:19:58 +0100 Subject: [PATCH 67/71] resolve TODOs --- .../Source/UserSession/Search/SearchTask.swift | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 84b38f44fcb..17e234bdcab 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -73,7 +73,7 @@ public final class SearchTask { } /// Start the search task. Errors will not be thrown. - public func start() async -> SearchResult { // TODO: test manually with two clients, develop and this code + public func start() async -> SearchResult { guard status == .pending else { assertionFailure() return SearchResult() @@ -89,28 +89,28 @@ public final class SearchTask { // search services taskGroup.addTask { - await self.performRemoteSearchForServices() // TODO: manually test each call + await self.performRemoteSearchForServices() } // search People or groups taskGroup.addTask { - await self.performLocalLookup() // TODO: manually test each call + await self.performLocalLookup() } taskGroup.addTask { - await self.performLocalSearch() // TODO: manually test each call + await self.performLocalSearch() } // v1 taskGroup.addTask { - await self.performRemoteSearchForTeamUser() // TODO: manually test each call + await self.performRemoteSearchForTeamUser() } // v2+ taskGroup.addTask { - await self.performRemoteSearch() // TODO: manually test each call + await self.performRemoteSearch() } taskGroup.addTask { - await self.performUserLookup() // TODO: manually test each call + await self.performUserLookup() } var result = SearchResult() From 33d0b0697d5eb3d31bead05eb72d51ebfc36dcdc Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Fri, 6 Feb 2026 10:00:11 +0100 Subject: [PATCH 68/71] add cancellation --- .../APIs/Rest/Search/SearchAPIV15.swift | 1 + .../RemoveParticipantActionHandlerTests.swift | 1 + .../UserSession/Search/SearchTask.swift | 23 +++++++++++-------- 3 files changed, 16 insertions(+), 9 deletions(-) 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-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/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index 17e234bdcab..f0dcd702036 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -29,9 +29,7 @@ public final class SearchTask { private enum Status { case pending - case running - case cancelled - case completed + case started(taskGroup: TaskGroup) } private var status = Status.pending @@ -68,25 +66,28 @@ public final class SearchTask { /// Cancel a previously started task public func cancel() { - guard status == .running else { return assertionFailure() } - status = .cancelled + guard case let .started(taskGroup) = status else { + assertionFailure() + return + } + + taskGroup.cancelAll() } /// Start the search task. Errors will not be thrown. public func start() async -> SearchResult { - guard status == .pending else { + guard case .pending = status else { assertionFailure() return SearchResult() } - status = .running - defer { status = .completed } - return await withTaskGroup( of: SearchResultAggregator.self, returning: SearchResult.self ) { @MainActor taskGroup in + status = .started(taskGroup: taskGroup) + // search services taskGroup.addTask { await self.performRemoteSearchForServices() @@ -118,6 +119,10 @@ public final class SearchTask { 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) From 6a885cbf75888d158897d88add391d5993608bdb Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Fri, 6 Feb 2026 10:04:19 +0100 Subject: [PATCH 69/71] revert changes --- .../Source/UserSession/Search/SearchDirectory.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift index c167a1a29d9..a2a24db1f41 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift @@ -92,13 +92,13 @@ public final class SearchDirectory { } } -public extension SearchDirectory { +extension SearchDirectory: TearDownCapable { /// Tear down the SearchDirectory. /// /// NOTE: this must be called before releasing the instance - func tearDown() { + public func tearDown() { let tearDown = { [self] in // Evict all cached search users searchUsersCache?.removeAllObjects() From c1afe5c00e492a53c96328b12a48e24ba751e076 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Fri, 6 Feb 2026 10:07:27 +0100 Subject: [PATCH 70/71] fix issue --- wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift index f0dcd702036..e3ed64e94d5 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchTask.swift @@ -529,7 +529,7 @@ extension SearchTask { contextProvider: contextProvider ) - continuation.resume(returning: { $0 = $0.union(withDirectoryResult: searchResult) }) + continuation.resume(returning: { $0 = $0.union(withDirectoryResult: updatedResult) }) }) From b419e1670566678585a9050fa28f7badef472db1 Mon Sep 17 00:00:00 2001 From: Christoph Aldrian Date: Fri, 6 Feb 2026 10:15:19 +0100 Subject: [PATCH 71/71] fix build error --- .../Source/UserSession/Search/SearchDirectory.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift index a2a24db1f41..c676840b0f4 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchDirectory.swift @@ -18,7 +18,7 @@ import Foundation -public final class SearchDirectory { +public final class SearchDirectory: NSObject { private let contextProvider: ContextProvider private let transportSession: TransportSessionType