diff --git a/WireDomain/Sources/WireDomain/Synchronization/OneOnOneResolver.swift b/WireDomain/Sources/WireDomain/Synchronization/OneOnOneResolver.swift index b475516dc3c..169e9eeb79c 100644 --- a/WireDomain/Sources/WireDomain/Synchronization/OneOnOneResolver.swift +++ b/WireDomain/Sources/WireDomain/Synchronization/OneOnOneResolver.swift @@ -169,7 +169,8 @@ struct OneOnOneResolver: OneOnOneResolverProtocol { await switchLocalConversationToMLS( mlsConversation: mlsConversation, - for: user + for: user, + userID: userID ) } @@ -219,15 +220,38 @@ struct OneOnOneResolver: OneOnOneResolverProtocol { private func switchLocalConversationToMLS( mlsConversation: ZMConversation, - for user: ZMUser + for user: ZMUser, + userID: WireDataModel.QualifiedID ) async { await context.perform { - /// Move local messages from proteus conversation if it exists - if let proteusConversation = user.oneOnOneConversation { - /// Since ZMMessages only have a single conversation connected, - /// forming this union also removes the relationship to the proteus conversation. + + // Note on proteus, it's possible to have 2 duplicate 1-1 conversations, so we need to fetch both + // conversations here. + let proteusConversations: [ZMConversation] = fetchAllTeamOneOnOneProteusConversations( + otherUserID: userID, + in: context + ) + + var allProteusConversations = Set(proteusConversations) + if let existingConversation = user.oneOnOneConversation, + existingConversation.messageProtocol == .proteus { + allProteusConversations.insert(existingConversation) + } + + // move local messages from proteus conversations if they exist + for proteusConversation in allProteusConversations { + // Since ZMMessages only have a single conversation connected, + // forming this union also removes the relationship to the proteus conversation. mlsConversation.mutableMessages.union(proteusConversation.allMessages) + } + + // insert system message that we moved from proteus to MLS + let sender = ZMUser.selfUser(in: context) + mlsConversation.appendMLSMigrationFinalizedSystemMessage(sender: sender, at: .now) + + if !allProteusConversations.isEmpty { mlsConversation.isForcedReadOnly = false + // update just to be sure mlsConversation.needsToBeUpdatedFromBackend = true } @@ -236,6 +260,37 @@ struct OneOnOneResolver: OneOnOneResolverProtocol { } } + private func fetchAllTeamOneOnOneProteusConversations( + otherUserID: WireDataModel.QualifiedID, + in context: NSManagedObjectContext + ) -> [ZMConversation] { + guard let otherUser = ZMUser.fetch(with: otherUserID, in: context) else { + return [] + } + let selfUser = ZMUser.selfUser(in: context) + guard selfUser.team != nil else { + return [] + } + + let request = NSFetchRequest(entityName: ZMConversation.entityName()) + let teamOneOnOnePredicate = ZMConversation.predicateForTeamOneToOneConversation() + + let sameParticipant = NSPredicate( + format: "ANY %K.user == %@ AND ANY %K.user == %@", + ZMConversationParticipantRolesKey, + otherUser, + ZMConversationParticipantRolesKey, + selfUser + ) + + request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ + teamOneOnOnePredicate, + sameParticipant + ]) + + return context.fetchOrAssert(request: request) + } + /// Resolves a Proteus 1:1 conversation. /// - Parameter user: The user to resolve the conversation for. diff --git a/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift b/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift index ad66078dbf4..3d736afa1af 100644 --- a/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift +++ b/WireDomain/Tests/WireDomainTests/Synchronization/OneOnOneResolverTests.swift @@ -178,8 +178,19 @@ final class OneOnOneResolverTests: XCTestCase { // Then - await context.perform { - let migratedMessagesTexts = mlsOneOnOneConversation.allMessages + try await context.perform { [self] in + let allMessages = mlsOneOnOneConversation.allMessages + + try XCTAssertCount(allMessages, count: 3) + let mlsSystemMessage = try XCTUnwrap(mlsOneOnOneConversation.lastMessage as? ZMSystemMessage) + XCTAssertEqual( + mlsSystemMessage.systemMessageType.rawValue, + ZMSystemMessageType.mlsMigrationFinalized.rawValue + ) + + XCTAssertEqual(mlsOneOnOneConversation.needsToBeUpdatedFromBackend, true) + + let migratedMessagesTexts = allMessages .compactMap(\.textMessageData) .compactMap(\.messageText) .sorted() diff --git a/wire-ios-data-model/Source/MLS/OneOnOne/OneOnOneMigrator.swift b/wire-ios-data-model/Source/MLS/OneOnOne/OneOnOneMigrator.swift index 47e14964c12..a08e57e0db5 100644 --- a/wire-ios-data-model/Source/MLS/OneOnOne/OneOnOneMigrator.swift +++ b/wire-ios-data-model/Source/MLS/OneOnOne/OneOnOneMigrator.swift @@ -76,6 +76,7 @@ public struct OneOnOneMigrator: OneOnOneMigratorInterface { ) await context.perform { + _ = context.saveOrRollback() } @@ -166,6 +167,10 @@ public struct OneOnOneMigrator: OneOnOneMigratorInterface { mlsConversation.mutableMessages.union(proteusConversation.allMessages) } + // insert system message that we moved from proteus to MLS + let sender = ZMUser.selfUser(in: context) + mlsConversation.appendMLSMigrationFinalizedSystemMessage(sender: sender, at: .now) + if !proteusConversations.isEmpty { // update just to be sure mlsConversation.needsToBeUpdatedFromBackend = true diff --git a/wire-ios-data-model/Tests/OneOnOne/OneOnOneMigratorTests.swift b/wire-ios-data-model/Tests/OneOnOne/OneOnOneMigratorTests.swift index 63bbf5fe653..fb7e65159d4 100644 --- a/wire-ios-data-model/Tests/OneOnOne/OneOnOneMigratorTests.swift +++ b/wire-ios-data-model/Tests/OneOnOne/OneOnOneMigratorTests.swift @@ -244,13 +244,16 @@ final class OneOnOneMigratorTests: XCTestCase { ) // Then - await syncContext.perform { + try await syncContext.perform { let mlsMessages = mlsConversation.allMessages.sortedAscendingPrependingNil(by: \.serverTimestamp) - XCTAssertEqual(mlsMessages.count, 3) + XCTAssertEqual(mlsMessages.count, 4) XCTAssertEqual(mlsMessages[0].textMessageData?.messageText, "Hello World!") XCTAssertTrue(mlsMessages[1].isKnock) XCTAssertTrue(mlsMessages[2].isImage) + let systemMessage = try XCTUnwrap(mlsMessages[3] as? ZMSystemMessage) + XCTAssertEqual(systemMessage.systemMessageType, .mlsMigrationFinalized) + XCTAssertNil(proteusConversation.lastMessage) } withExtendedLifetime(handler) {} @@ -351,7 +354,7 @@ final class OneOnOneMigratorTests: XCTestCase { // Then await syncContext.perform { let mlsMessages = mlsConversation.allMessages.sortedAscendingPrependingNil(by: \.serverTimestamp) - let expectedMessagesCount = 6 + let expectedMessagesCount = 7 if mlsMessages.count == expectedMessagesCount { XCTAssertEqual(mlsMessages[0].textMessageData?.messageText, "Hello World!") XCTAssertTrue(mlsMessages[1].isKnock) @@ -359,6 +362,7 @@ final class OneOnOneMigratorTests: XCTestCase { XCTAssertEqual(mlsMessages[3].textMessageData?.messageText, "Hello World Dup!") XCTAssertTrue(mlsMessages[4].isKnock) XCTAssertTrue(mlsMessages[5].isImage) + XCTAssertTrue(mlsMessages[6].isSystem) } else { XCTFail("messages count is \(mlsMessages.count) instead of \(expectedMessagesCount)") } @@ -497,7 +501,7 @@ final class OneOnOneMigratorTests: XCTestCase { // Then await syncContext.perform { let mlsMessages = mlsConversation.allMessages.sortedAscendingPrependingNil(by: \.serverTimestamp) - let expectedMessagesCount = 9 + let expectedMessagesCount = 10 if mlsMessages.count == expectedMessagesCount { XCTAssertEqual(mlsMessages[0].textMessageData?.messageText, "Hello World!") XCTAssertTrue(mlsMessages[1].isKnock) @@ -508,6 +512,7 @@ final class OneOnOneMigratorTests: XCTestCase { XCTAssertEqual(mlsMessages[6].textMessageData?.messageText, "Hello World 1!") XCTAssertEqual(mlsMessages[7].textMessageData?.messageText, "Hello World 2!") XCTAssertEqual(mlsMessages[8].textMessageData?.messageText, "Hello World 3!") + XCTAssertTrue(mlsMessages[9].isSystem) } else { XCTFail("messages count is \(mlsMessages.count) instead of \(expectedMessagesCount)") } diff --git a/wire-ios-request-strategy/Sources/Notifications/Push Notifications/Notification Types/Content/ZMLocalNotification+Events.swift b/wire-ios-request-strategy/Sources/Notifications/Push Notifications/Notification Types/Content/ZMLocalNotification+Events.swift index f28fa94e844..fcfb497e9f3 100644 --- a/wire-ios-request-strategy/Sources/Notifications/Push Notifications/Notification Types/Content/ZMLocalNotification+Events.swift +++ b/wire-ios-request-strategy/Sources/Notifications/Push Notifications/Notification Types/Content/ZMLocalNotification+Events.swift @@ -16,6 +16,7 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // +import Foundation import WireDataModel public extension ZMLocalNotification { @@ -191,6 +192,17 @@ private class ConversationCreateEventNotificationBuilder: EventNotificationBuild LocalNotificationType.event(.conversationCreated) } + override func shouldCreateNotification() -> Bool { + if conversation == nil { + // WPB-8946: fixes bug: notifications shown even though availability is busy or away + let availability = moc.performAndWait { ZMUser.selfUser(in: moc).availability } + return [.none, .available].contains(availability) + } + + // default behavior + return super.shouldCreateNotification() + } + } // MARK: - Conversation Delete Event @@ -322,7 +334,12 @@ private class NewMessageNotificationBuilder: EventNotificationBuilder { "Not creating local notification for message with nonce = \(event.messageNonce) because conversation is silenced" ) return false + } else if conversation == nil { + // WPB-8946: fixes bug: notifications shown even though availability is busy or away + let availability = moc.performAndWait { ZMUser.selfUser(in: moc).availability } + return [.none, .available].contains(availability) } + if ZMUser.selfUser(in: moc).remoteIdentifier == event.senderUUID { return false } diff --git a/wire-ios/Wire-iOS/Sources/AppDelegate.swift b/wire-ios/Wire-iOS/Sources/AppDelegate.swift index 3f3a06a3391..060999f9a35 100644 --- a/wire-ios/Wire-iOS/Sources/AppDelegate.swift +++ b/wire-ios/Wire-iOS/Sources/AppDelegate.swift @@ -320,7 +320,11 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { } func applicationProtectedDataDidBecomeAvailable(_ application: UIApplication) { - guard appRootRouter == nil else { return } + WireLogger.appDelegate.info("applicationProtectedDataDidBecomeAvailable", attributes: .safePublic) + guard appRootRouter == nil else { + WireLogger.appDelegate.debug("applicationProtectedDataDidBecomeAvailable: appRootRouter nil") + return + } createAppRootRouterAndInitialiazeOperations(launchOptions) } } @@ -347,6 +351,12 @@ private extension AppDelegate { fatalError("sessionManager is not created") } + guard mainWindow != nil else { + WireLogger.appDelegate.critical("no mainWindow this should not be possible at this point") + assertionFailure("no mainWindow this should not be possible at this point") + return + } + appRootRouter = AppRootRouter( mainWindow: mainWindow, sessionManager: sessionManager,