diff --git a/CHANGELOG.en.md b/CHANGELOG.en.md index 93317acf..4c083e85 100644 --- a/CHANGELOG.en.md +++ b/CHANGELOG.en.md @@ -1,8 +1,16 @@ # Changelog -## [0.10.0 (495)] - 2022-03-24 +## [0.10.1 (501)] - 2022-03-24 -- New behavior of your address book! Now, an Olvid user becomes a contact only if you explicitly agree. You are now in full control of your address book! +- It is now possible to reply to a message or to mark it as read right from the notification! +- If you like a particular reaction made by another user, you can now easily add it to the list of your preferred reactions. +- Fixes a bug preventing a received message from being edited by the sender. +- Fixes an issue preventing the minimum supported and recommended Olvid version would not be properly updated. +- Background tasks are more reliable. + +## [0.10.0 (495)] - 2022-03-21 + +- New behavior of your address book! Now, an Olvid user becomes a contact *only* if you explicitly agree. You are now in full control of your address book! - A new list of "other" Olvid users is now accessible from the "Contacts" tab. Typically, these users are part of the same discussion groups as you. Inviting these users to be a contact of yours can be done in one tap! - A group invite from a contact is now automatically accepted. - You still need to explicitly accept group invites from Olvid users who are not part of your contacts. diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index 2fae66c1..e827db52 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -1,8 +1,16 @@ # Changelog -## [0.10.0 (495)] - 2022-03-24 +## [0.10.1 (501)] - 2022-03-24 -- Nouveau comportement de votre carnet d'adresse Olvid ! Maintenant, un autre utilisateur d'Olvid devient un contact uniquement si vous l'acceptez explicitement. Vous avez enfin un contrôle total sur votre carnet d'adresse ;-) +- Il est maintenant possible de répondre à un message, ou de le marquer comme lu, directement depuis la notification ! +- Si vous appréciez une réaction faite par un autre utilisateur, vous pouvez facilement l'ajouter à la liste de vos réactions préférées. +- Corrige un problème empêchant un message reçu d'être édité par son envoyeur. +- Corrige un problème empêchant la bonne mise à jour des versions supportées et recommandées d'Olvid. +- Les tâches de fonds sont plus robustes. + +## [0.10.0 (495)] - 2022-03-21 + +- Nouveau comportement de votre carnet d'adresse Olvid ! Maintenant, un autre utilisateur d'Olvid devient un contact *uniquement* si vous l'acceptez explicitement. Vous avez enfin un contrôle total sur votre carnet d'adresse ;-) - Une nouvelle liste « d'autres » utilisateurs d'Olvid est maintenant accessible depuis l'écran de Contacts. Ces utilisateurs sont typiquement ceux qui font partie des mêmes groupes que vous mais qui ne sont néanmoins pas des contacts. Pour vous les inviter en une touche ! - Maintenant, une invitation à un groupe provenant d'un contact est automatiquement acceptée. - Vous devez toujours accepter explicitement une invitation à un groupe si elle provient d'un utilisateur qui ne fait partie de vos contacts. diff --git a/Engine/ObvBackupManager/ObvBackupManager/CoreData/Backup.swift b/Engine/ObvBackupManager/ObvBackupManager/CoreData/Backup.swift index 55f0795e..34fd995e 100644 --- a/Engine/ObvBackupManager/ObvBackupManager/CoreData/Backup.swift +++ b/Engine/ObvBackupManager/ObvBackupManager/CoreData/Backup.swift @@ -128,7 +128,7 @@ final class Backup: NSManagedObject, ObvManagedObject { private convenience init(forExport: Bool, status: Status, backupKey: BackupKey, delegateManager: ObvBackupDelegateManager) throws { - guard let obvContext = backupKey.obvContext else { throw NSError() } + guard let obvContext = backupKey.obvContext else { throw Self.makeError(message: "The context of the backupKey is nil") } let entityDescription = NSEntityDescription.entity(forEntityName: Self.entityName, in: obvContext)! self.init(entity: entityDescription, insertInto: obvContext) diff --git a/Engine/ObvBackupManager/ObvBackupManager/ObvBackupManagerImplementation.swift b/Engine/ObvBackupManager/ObvBackupManager/ObvBackupManagerImplementation.swift index 908ff5d3..c167fdbe 100644 --- a/Engine/ObvBackupManager/ObvBackupManager/ObvBackupManagerImplementation.swift +++ b/Engine/ObvBackupManager/ObvBackupManager/ObvBackupManagerImplementation.swift @@ -703,13 +703,13 @@ extension ObvBackupManagerImplementation { public func fulfill(requiredDelegate delegate: AnyObject, forDelegateType delegateType: ObvEngineDelegateType) throws { switch delegateType { case .ObvCreateContextDelegate: - guard let delegate = delegate as? ObvCreateContextDelegate else { throw NSError() } + guard let delegate = delegate as? ObvCreateContextDelegate else { throw Self.makeError(message: "The ObvCreateContextDelegate is nil") } delegateManager.contextCreator = delegate case .ObvNotificationDelegate: - guard let delegate = delegate as? ObvNotificationDelegate else { throw NSError() } + guard let delegate = delegate as? ObvNotificationDelegate else { throw Self.makeError(message: "The ObvNotificationDelegate is nil") } delegateManager.notificationDelegate = delegate default: - throw NSError() + throw Self.makeError(message: "Unexpected delegate type") } } diff --git a/Engine/ObvChannelManager/ObvChannelManager/Coordinators/NetworkReceivedMessageDecryptor.swift b/Engine/ObvChannelManager/ObvChannelManager/Coordinators/NetworkReceivedMessageDecryptor.swift index eb72e518..d660b47a 100644 --- a/Engine/ObvChannelManager/ObvChannelManager/Coordinators/NetworkReceivedMessageDecryptor.swift +++ b/Engine/ObvChannelManager/ObvChannelManager/Coordinators/NetworkReceivedMessageDecryptor.swift @@ -65,6 +65,8 @@ extension NetworkReceivedMessageDecryptor { } + + /// This method is called on each new received message. func decryptAndProcess(_ receivedMessage: ObvNetworkReceivedMessageEncrypted, within obvContext: ObvContext) throws { guard let delegateManager = delegateManager else { diff --git a/Engine/ObvDatabaseManager/ObvDatabaseManager/Migration/v24_to_v25/InboxAttachmentToInboxAttachmentMigrationPolicyV24ToV25.swift b/Engine/ObvDatabaseManager/ObvDatabaseManager/Migration/v24_to_v25/InboxAttachmentToInboxAttachmentMigrationPolicyV24ToV25.swift index 1dae534a..08a442d3 100644 --- a/Engine/ObvDatabaseManager/ObvDatabaseManager/Migration/v24_to_v25/InboxAttachmentToInboxAttachmentMigrationPolicyV24ToV25.swift +++ b/Engine/ObvDatabaseManager/ObvDatabaseManager/Migration/v24_to_v25/InboxAttachmentToInboxAttachmentMigrationPolicyV24ToV25.swift @@ -209,7 +209,7 @@ extension InboxAttachmentToInboxAttachmentMigrationPolicyV24ToV25 { throw makeError(message: "Could not create file for writting chunks") } - guard let fh = FileHandle(forWritingAtPath: url.path) else { throw NSError() } + guard let fh = FileHandle(forWritingAtPath: url.path) else { throw makeError(message: "Could get FileHandle") } fh.seek(toFileOffset: UInt64(cleartextLength)) fh.closeFile() diff --git a/Engine/ObvEngine/ObvEngine/CoreData/PersistedEngineDialog.swift b/Engine/ObvEngine/ObvEngine/CoreData/PersistedEngineDialog.swift index 720bfb79..54b5c9dd 100644 --- a/Engine/ObvEngine/ObvEngine/CoreData/PersistedEngineDialog.swift +++ b/Engine/ObvEngine/ObvEngine/CoreData/PersistedEngineDialog.swift @@ -86,7 +86,9 @@ final class PersistedEngineDialog: NSManagedObject, ObvManagedObject { extension PersistedEngineDialog { func update(with obvDialog: ObvDialog) throws { - guard self.uuid == obvDialog.uuid else { throw NSError() } + guard self.uuid == obvDialog.uuid else { + throw Self.makeError(message: "Could not get obvDialog's uuid") + } self.obvDialog = obvDialog notificationRelatedChanges.insert(.obvDialog) } diff --git a/Engine/ObvEngine/ObvEngine/NotificationSender.swift b/Engine/ObvEngine/ObvEngine/NotificationSender.swift index 0c52bb60..eb075457 100644 --- a/Engine/ObvEngine/ObvEngine/NotificationSender.swift +++ b/Engine/ObvEngine/ObvEngine/NotificationSender.swift @@ -32,7 +32,7 @@ extension ObvEngine { func registerToInternalNotifications() throws { - guard let notificationDelegate = notificationDelegate else { throw NSError() } + guard let notificationDelegate = notificationDelegate else { throw Self.makeError(message: "The notification delegate is not set") } notificationCenterTokens.append(ObvNetworkFetchNotificationNew.observeAppStoreReceiptVerificationSucceededButSubscriptionIsExpired(within: notificationDelegate) { [weak self] (ownedIdentity, transactionIdentifier, flowId) in guard let _self = self else { return } @@ -642,21 +642,15 @@ extension ObvEngine { guard let createContextDelegate = _self.createContextDelegate else { return } guard let identityDelegate = _self.identityDelegate else { return } - var obvContactIdentity: ObvContactIdentity! - var error: Error? + var obvContactIdentity: ObvContactIdentity? let randomFlowId = FlowIdentifier() createContextDelegate.performBackgroundTaskAndWait(flowId: randomFlowId) { (obvContext) in - let _obvContactIdentity = ObvContactIdentity(contactCryptoIdentity: contactCryptoIdentity, - ownedCryptoIdentity: ownedCryptoIdentity, - identityDelegate: identityDelegate, - within: obvContext) - guard _obvContactIdentity != nil else { - error = NSError() - return - } - obvContactIdentity = _obvContactIdentity + obvContactIdentity = ObvContactIdentity(contactCryptoIdentity: contactCryptoIdentity, + ownedCryptoIdentity: ownedCryptoIdentity, + identityDelegate: identityDelegate, + within: obvContext) } - guard error == nil else { + guard let obvContactIdentity = obvContactIdentity else { os_log("Could not get contact identity", log: _self.log, type: .fault) return } @@ -678,25 +672,18 @@ extension ObvEngine { guard let createContextDelegate = _self.createContextDelegate else { return } guard let identityDelegate = _self.identityDelegate else { return } - var obvContactIdentity: ObvContactIdentity! - var error: Error? + var obvContactIdentity: ObvContactIdentity? let randomFlowId = FlowIdentifier() createContextDelegate.performBackgroundTaskAndWait(flowId: randomFlowId) { (obvContext) in - let _obvContactIdentity = ObvContactIdentity(contactCryptoIdentity: contactCryptoIdentity, + obvContactIdentity = ObvContactIdentity(contactCryptoIdentity: contactCryptoIdentity, ownedCryptoIdentity: ownedCryptoIdentity, identityDelegate: identityDelegate, within: obvContext) - guard _obvContactIdentity != nil else { - error = NSError() - return - } - obvContactIdentity = _obvContactIdentity } - guard error == nil else { + guard let obvContactIdentity = obvContactIdentity else { os_log("Could not get contact identity", log: _self.log, type: .fault) return } - ObvEngineNotificationNew.updatedContactIdentity(obvContactIdentity: obvContactIdentity, trustedIdentityDetailsWereUpdated: false, publishedIdentityDetailsWereUpdated: true) .postOnBackgroundQueue(within: _self.appNotificationCenter) } @@ -852,19 +839,13 @@ extension ObvEngine { guard let createContextDelegate = _self.createContextDelegate else { return } guard let identityDelegate = _self.identityDelegate else { return } - var obvOwnedIdentity: ObvOwnedIdentity! - var error: Error? + var obvOwnedIdentity: ObvOwnedIdentity? let randomFlowId = FlowIdentifier() createContextDelegate.performBackgroundTaskAndWait(flowId: randomFlowId) { (obvContext) in - let _obvOwnedIdentity = ObvOwnedIdentity(ownedCryptoIdentity: ownedCryptoIdentity, - identityDelegate: identityDelegate, within: obvContext) - guard _obvOwnedIdentity != nil else { - error = NSError() - return - } - obvOwnedIdentity = _obvOwnedIdentity + obvOwnedIdentity = ObvOwnedIdentity(ownedCryptoIdentity: ownedCryptoIdentity, + identityDelegate: identityDelegate, within: obvContext) } - guard error == nil else { + guard let obvOwnedIdentity = obvOwnedIdentity else { os_log("Could not get owned identity", log: _self.log, type: .fault) return } diff --git a/Engine/ObvEngine/ObvEngine/ObvEngine.swift b/Engine/ObvEngine/ObvEngine/ObvEngine.swift index 1a12c04e..b3be5cbb 100644 --- a/Engine/ObvEngine/ObvEngine/ObvEngine.swift +++ b/Engine/ObvEngine/ObvEngine/ObvEngine.swift @@ -419,11 +419,10 @@ extension ObvEngine { // MARK: - Public API for managing the database -extension ObvEngine { +extension ObvEngine: ObvErrorMaker { - private static let errorDomain = "ObvEngine" + public static let errorDomain = "ObvEngine" - private static func makeError(message: String) -> Error { NSError(domain: errorDomain, code: 0, userInfo: [NSLocalizedFailureReasonErrorKey: message]) } private func makeError(message: String) -> Error { Self.makeError(message: message) } public func replayTransactionsHistory() { diff --git a/Engine/ObvEngine/ObvEngine/Types/ObvAttachment.swift b/Engine/ObvEngine/ObvEngine/Types/ObvAttachment.swift index 560faeba..c04e1829 100644 --- a/Engine/ObvEngine/ObvEngine/Types/ObvAttachment.swift +++ b/Engine/ObvEngine/ObvEngine/Types/ObvAttachment.swift @@ -71,15 +71,23 @@ public struct ObvAttachment: Hashable { return self.status == .paused } - private static let errorDomain = String(describing: ObvAttachment.self) + + private static func makeError(message: String, code: Int = 0) -> Error { + NSError(domain: "ObvAttachment", code: code, userInfo: [NSLocalizedFailureReasonErrorKey: message]) + } + init(attachmentId: AttachmentIdentifier, networkFetchDelegate: ObvNetworkFetchDelegate, identityDelegate: ObvIdentityDelegate, within obvContext: ObvContext) throws { - guard let networkReceivedAttachment = networkFetchDelegate.getAttachment(withId: attachmentId, flowId: obvContext.flowId) else { throw NSError() } + guard let networkReceivedAttachment = networkFetchDelegate.getAttachment(withId: attachmentId, flowId: obvContext.flowId) else { + throw Self.makeError(message: "Coult not get attachment") + } try self.init(networkReceivedAttachment: networkReceivedAttachment, identityDelegate: identityDelegate, within: obvContext) } init(attachmentId: AttachmentIdentifier, fromContactIdentity: ObvContactIdentity, networkFetchDelegate: ObvNetworkFetchDelegate, flowId: FlowIdentifier) throws { - guard let networkReceivedAttachment = networkFetchDelegate.getAttachment(withId: attachmentId, flowId: flowId) else { throw NSError() } + guard let networkReceivedAttachment = networkFetchDelegate.getAttachment(withId: attachmentId, flowId: flowId) else { + throw Self.makeError(message: "Coult not get attachment") + } self.fromContactIdentity = fromContactIdentity self.attachmentId = networkReceivedAttachment.attachmentId metadata = networkReceivedAttachment.metadata @@ -94,7 +102,9 @@ public struct ObvAttachment: Hashable { guard let obvContact = ObvContactIdentity(contactCryptoIdentity: networkReceivedAttachment.fromCryptoIdentity, ownedCryptoIdentity: networkReceivedAttachment.attachmentId.messageId.ownedCryptoIdentity, identityDelegate: identityDelegate, - within: obvContext) else { throw NSError() } + within: obvContext) else { + throw Self.makeError(message: "Could not get ObvContactIdentity") + } self.fromContactIdentity = obvContact self.attachmentId = networkReceivedAttachment.attachmentId metadata = networkReceivedAttachment.metadata @@ -162,9 +172,7 @@ extension ObvAttachment: Codable { self.url = try values.decode(URL.self, forKey: .url) let rawStatus = try values.decode(Int.self, forKey: .status) guard let status = Status(rawValue: rawStatus) else { - let message = "Could not decode status" - let userInfo = [NSLocalizedFailureReasonErrorKey: message] - throw NSError(domain: ObvAttachment.errorDomain, code: 0, userInfo: userInfo) + throw Self.makeError(message: "Could not decode status") } self.status = status self.attachmentId = try values.decode(AttachmentIdentifier.self, forKey: .attachmentId) diff --git a/Engine/ObvEngine/ObvEngine/Types/ObvCryptoId.swift b/Engine/ObvEngine/ObvEngine/Types/ObvCryptoId.swift index fe64aa5a..25260192 100644 --- a/Engine/ObvEngine/ObvEngine/Types/ObvCryptoId.swift +++ b/Engine/ObvEngine/ObvEngine/Types/ObvCryptoId.swift @@ -34,6 +34,11 @@ public struct ObvCryptoId { public func belongsTo(serverURL: URL) -> Bool { return cryptoIdentity.serverURL == serverURL } + + private static func makeError(message: String, code: Int = 0) -> Error { + NSError(domain: "ObvCryptoId", code: code, userInfo: [NSLocalizedFailureReasonErrorKey: message]) + } + } @@ -88,7 +93,7 @@ extension ObvCryptoId { } public init(identity: Data) throws { - guard let cryptoIdentity = ObvCryptoIdentity(from: identity) else { throw NSError() } + guard let cryptoIdentity = ObvCryptoIdentity(from: identity) else { throw Self.makeError(message: "Could not get ObvCryptoIdentity") } self.cryptoIdentity = cryptoIdentity } diff --git a/Engine/ObvEngine/ObvEngine/Types/ObvMessage.swift b/Engine/ObvEngine/ObvEngine/Types/ObvMessage.swift index a4690461..d3b052da 100644 --- a/Engine/ObvEngine/ObvEngine/Types/ObvMessage.swift +++ b/Engine/ObvEngine/ObvEngine/Types/ObvMessage.swift @@ -45,21 +45,30 @@ public struct ObvMessage { return fromContactIdentity.ownedIdentity.cryptoId } + + private static func makeError(message: String, code: Int = 0) -> Error { + NSError(domain: "ObvMessage", code: code, userInfo: [NSLocalizedFailureReasonErrorKey: message]) + } + + init(messageId: MessageIdentifier, networkFetchDelegate: ObvNetworkFetchDelegate, identityDelegate: ObvIdentityDelegate, within obvContext: ObvContext) throws { - guard let networkReceivedMessage = networkFetchDelegate.getDecryptedMessage(messageId: messageId, flowId: obvContext.flowId) else { throw NSError() } + guard let networkReceivedMessage = networkFetchDelegate.getDecryptedMessage(messageId: messageId, flowId: obvContext.flowId) else { + throw Self.makeError(message: "The call to getDecryptedMessage did fail") + } try self.init(networkReceivedMessage: networkReceivedMessage, networkFetchDelegate: networkFetchDelegate, identityDelegate: identityDelegate, within: obvContext) } + init(networkReceivedMessage: ObvNetworkReceivedMessageDecrypted, networkFetchDelegate: ObvNetworkFetchDelegate, identityDelegate: ObvIdentityDelegate, within obvContext: ObvContext) throws { guard let obvContact = ObvContactIdentity(contactCryptoIdentity: networkReceivedMessage.fromIdentity, ownedCryptoIdentity: networkReceivedMessage.messageId.ownedCryptoIdentity, identityDelegate: identityDelegate, - within: obvContext) else { throw NSError() } - - + within: obvContext) else { + throw Self.makeError(message: "Could not get ObvContactIdentity") + } self.fromContactIdentity = obvContact self.messageId = networkReceivedMessage.messageId diff --git a/Engine/ObvFlowManager/ObvFlowManager/ObvFlowManager.swift b/Engine/ObvFlowManager/ObvFlowManager/ObvFlowManager.swift index 35f4da61..0b9e3e88 100644 --- a/Engine/ObvFlowManager/ObvFlowManager/ObvFlowManager.swift +++ b/Engine/ObvFlowManager/ObvFlowManager/ObvFlowManager.swift @@ -158,10 +158,10 @@ extension ObvFlowManager { public func fulfill(requiredDelegate delegate: AnyObject, forDelegateType delegateType: ObvEngineDelegateType) throws { switch delegateType { case .ObvNotificationDelegate: - guard let delegate = delegate as? ObvNotificationDelegate else { throw NSError() } + guard let delegate = delegate as? ObvNotificationDelegate else { throw Self.makeError(message: "The ObvNotificationDelegate is not set") } delegateManager.notificationDelegate = delegate default: - throw NSError() + throw Self.makeError(message: "Unexpected delegate type") } } diff --git a/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/BootstrapWorker.swift b/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/BootstrapWorker.swift index 0a14bb3a..8e5ed01c 100644 --- a/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/BootstrapWorker.swift +++ b/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/BootstrapWorker.swift @@ -75,7 +75,6 @@ final class BootstrapWorker { self?.deleteOrphanedDatabaseObjects(flowId: flowId, log: log, contextCreator: contextCreator) self?.reschedulePendingDeleteFromServers(flowId: flowId, log: log, delegateManager: delegateManager, contextCreator: contextCreator) delegateManager.downloadAttachmentChunksDelegate.cleanExistingOutboxAttachmentSessions(flowId: flowId) - delegateManager.wellKnownCacheDelegate.initializateCache(flowId: flowId) } } @@ -106,6 +105,7 @@ final class BootstrapWorker { internalQueue.addOperation { [weak self] in // We cannot call this method in the finalizeInitialization method because the generated notifications would not be received by the app self?.rescheduleAllInboxMessagesAndAttachments(flowId: flowId, log: log, contextCreator: contextCreator, delegateManager: delegateManager) + delegateManager.wellKnownCacheDelegate.initializateCache(flowId: flowId) } } diff --git a/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/Coordinators/MessagesCoordinator.swift b/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/Coordinators/MessagesCoordinator.swift index 05096f1a..cff9ce9c 100644 --- a/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/Coordinators/MessagesCoordinator.swift +++ b/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/Coordinators/MessagesCoordinator.swift @@ -340,21 +340,18 @@ extension MessagesCoordinator: MessagesDelegate { let listOfMessageAndAttachmentsOnServer = [message] let localDownloadTimestamp = Date() - var idOfNewMessage: MessageIdentifier! try localQueue.sync { - let idsOfNewMessages = try saveMessagesAndAttachmentsFromServer(listOfMessageAndAttachmentsOnServer, downloadTimestampFromServer: downloadTimestampFromServer, localDownloadTimestamp: localDownloadTimestamp, ownedIdentity: ownedIdentity, flowId: flowId) guard idsOfNewMessages.count == 1 else { throw makeError(message: "Could not save message") } - idOfNewMessage = idsOfNewMessages.first! } - delegateManager?.networkFetchFlowDelegate.aMessageReceivedThroughTheWebsocketWasSavedByTheMessageDelegate(for: ownedIdentity, - idOfNewMessage: idOfNewMessage, - flowId: flowId) + queueForCallingDelegate.async { [weak self] in + self?.delegateManager?.networkFetchFlowDelegate.aMessageReceivedThroughTheWebsocketWasSavedByTheMessageDelegate(flowId: flowId) + } } } @@ -585,7 +582,7 @@ extension MessagesCoordinator: URLSessionDataDelegate { os_log("🌊 We successfully downloaded %d messages (%d are new) for identity %@ within flow %{public}@", log: log, type: .debug, listOfMessageAndAttachmentsOnServer.count, idsOfNewMessages.count, ownedIdentity.debugDescription, flowId.debugDescription) _ = removeInfoFor(task) queueForCallingDelegate.async { - delegateManager.networkFetchFlowDelegate.downloadingMessagesAndListingAttachmentWasPerformed(for: ownedIdentity, andDeviceUid: deviceUid, idsOfNewMessages: idsOfNewMessages, flowId: flowId) + delegateManager.networkFetchFlowDelegate.downloadingMessagesAndListingAttachmentWasPerformed(for: ownedIdentity, andDeviceUid: deviceUid, flowId: flowId) } } @@ -913,7 +910,6 @@ extension MessagesCoordinator: URLSessionDataDelegate { do { message = try InboxMessage( messageId: messageId, - toIdentity: ownedIdentity, encryptedContent: messageAndAttachmentsOnServer.encryptedContent, hasEncryptedExtendedMessagePayload: messageAndAttachmentsOnServer.hasEncryptedExtendedMessagePayload, wrappedKey: messageAndAttachmentsOnServer.wrappedKey, diff --git a/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/Coordinators/NetworkFetchFlowCoordinator.swift b/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/Coordinators/NetworkFetchFlowCoordinator.swift index c59f6966..a1956093 100644 --- a/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/Coordinators/NetworkFetchFlowCoordinator.swift +++ b/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/Coordinators/NetworkFetchFlowCoordinator.swift @@ -397,14 +397,14 @@ extension NetworkFetchFlowCoordinator { } - func downloadingMessagesAndListingAttachmentWasPerformed(for identity: ObvCryptoIdentity, andDeviceUid uid: UID, idsOfNewMessages: [MessageIdentifier], flowId: FlowIdentifier) { + func downloadingMessagesAndListingAttachmentWasPerformed(for identity: ObvCryptoIdentity, andDeviceUid uid: UID, flowId: FlowIdentifier) { failedAttemptsCounterManager.reset(counter: .downloadMessagesAndListAttachments(ownedIdentity: identity)) processUnprocessedMessages(flowId: flowId) pollingWorker.pollingIfRequired(for: identity, withDeviceUid: uid, flowId: flowId) } - func aMessageReceivedThroughTheWebsocketWasSavedByTheMessageDelegate(for identity: ObvCryptoIdentity, idOfNewMessage: MessageIdentifier, flowId: FlowIdentifier) { + func aMessageReceivedThroughTheWebsocketWasSavedByTheMessageDelegate(flowId: FlowIdentifier) { processUnprocessedMessages(flowId: flowId) } @@ -1065,6 +1065,8 @@ extension NetworkFetchFlowCoordinator { let log = OSLog(subsystem: delegateManager.logSubsystem, category: logCategory) + os_log("New well known was cached", log: log, type: .info) + guard let notificationDelegate = delegateManager.notificationDelegate else { os_log("The notification delegate is not set", log: log, type: .fault) assertionFailure() diff --git a/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/Coordinators/WellKnownCoordinator/WellKnownCoordinator.swift b/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/Coordinators/WellKnownCoordinator/WellKnownCoordinator.swift index a47a1f5a..0410c96b 100644 --- a/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/Coordinators/WellKnownCoordinator/WellKnownCoordinator.swift +++ b/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/Coordinators/WellKnownCoordinator/WellKnownCoordinator.swift @@ -219,6 +219,8 @@ extension WellKnownCoordinator: WellKnownDownloadOperationDelegate { let log = OSLog(subsystem: delegateManager.logSubsystem, category: logCategory) + os_log("Well known was downloaded", log: log, type: .info) + guard let contextCreator = delegateManager.contextCreator else { os_log("The context creator manager is not set", log: log, type: .fault) assertionFailure() diff --git a/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/CoreData/InboxAttachment.swift b/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/CoreData/InboxAttachment.swift index 00ecd7f8..5a85b435 100644 --- a/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/CoreData/InboxAttachment.swift +++ b/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/CoreData/InboxAttachment.swift @@ -420,7 +420,9 @@ extension InboxAttachment { throw InternalError.couldNotCreateAttachmentFile(error: nil) } - guard let fh = FileHandle(forWritingAtPath: url.path) else { throw NSError() } + guard let fh = FileHandle(forWritingAtPath: url.path) else { + throw Self.makeError(message: "Could not get FileHandle") + } fh.seek(toFileOffset: UInt64(cleartextLength)) fh.closeFile() diff --git a/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/CoreData/InboxMessage.swift b/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/CoreData/InboxMessage.swift index 73a00249..35194ac1 100644 --- a/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/CoreData/InboxMessage.swift +++ b/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/CoreData/InboxMessage.swift @@ -126,7 +126,12 @@ final class InboxMessage: NSManagedObject, ObvManagedObject, ObvErrorMaker { // MARK: - Initializer - convenience init(messageId: MessageIdentifier, toIdentity: ObvCryptoIdentity, encryptedContent: EncryptedData, hasEncryptedExtendedMessagePayload: Bool, wrappedKey: EncryptedData, messageUploadTimestampFromServer: Date, downloadTimestampFromServer: Date, localDownloadTimestamp: Date, within obvContext: ObvContext) throws { + convenience init(messageId: MessageIdentifier, encryptedContent: EncryptedData, hasEncryptedExtendedMessagePayload: Bool, wrappedKey: EncryptedData, messageUploadTimestampFromServer: Date, downloadTimestampFromServer: Date, localDownloadTimestamp: Date, within obvContext: ObvContext) throws { + + guard !Self.thisMessageWasRecentlyDeleted(messageId: messageId) else { + assertionFailure("This assert can be removed if necessary") + throw InternalError.tryingToInsertAMessageThatWasAlreadyDeleted + } os_log("🔑 Creating InboxMessage with id %{public}@", log: Self.log, type: .info, messageId.debugDescription) @@ -148,7 +153,39 @@ final class InboxMessage: NSManagedObject, ObvManagedObject, ObvErrorMaker { self.wrappedKey = wrappedKey } + + + /// We keep in memory a list of all messages that were "recently" deleted. This prevents the re-creation of a message that we would list from the server and delete at the same time. + /// Every 10 minutes or so, we remove old entries. + private static var messagesRecentlyDeleted = [MessageIdentifier: Date]() + + + /// Allows to keep track of the date when we last removed old entries from `messagesRecentlyDeleted` + private static var lastRemovalOfOldEntriesInMessagesRecentlyDeleted = Date.distantPast + + + /// Removes old entries from `messagesRecentlyDeleted` but only if we did not do this recently. + private static func removeOldEntriesFromMessagesRecentlyDeletedIfAppropriate() { + // We do not remove old entries from `messagesRecentlyDeleted` if we did this already less than 10 minutes ago + guard Date().timeIntervalSince(lastRemovalOfOldEntriesInMessagesRecentlyDeleted) > TimeInterval(minutes: 10) else { return } + lastRemovalOfOldEntriesInMessagesRecentlyDeleted = Date() + let threshold = Date(timeInterval: -TimeInterval(minutes: 10), since: lastRemovalOfOldEntriesInMessagesRecentlyDeleted) + // Keep the most recent values in messagesRecentlyDeleted + messagesRecentlyDeleted = messagesRecentlyDeleted.filter({ $0.value > threshold }) + } + + + /// Returns `true` iff we recently deleted a message with the given message identifier. + private static func thisMessageWasRecentlyDeleted(messageId: MessageIdentifier) -> Bool { + removeOldEntriesFromMessagesRecentlyDeletedIfAppropriate() + return messagesRecentlyDeleted.keys.contains(messageId) + } + + private static func trackRecentlyDeletedMessage(messageId: MessageIdentifier) { + messagesRecentlyDeleted[messageId] = Date() + } + } @@ -285,3 +322,23 @@ extension InboxMessage { } } + + +// MARK: - Other callbacks + +extension InboxMessage { + + override func prepareForDeletion() { + super.prepareForDeletion() + + // We do not wait until the context is saved for inserting the current message in the list of recently deleted messages. + // The reason is the following : + // - Either the save fails: in that case, the message stays in the database and we won't be able to create a new one with the same Id anyway. + // This message will eventually be deleted and the list of recently deleted messages will be updated with a new, more recent, timestamp. + // - Either the save succeeds: in that case, we make sure that there won't be a time interval during which the message does not exists in DB without being stored in the list of recently deleted messages. + + Self.trackRecentlyDeletedMessage(messageId: self.messageId) + + } + +} diff --git a/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/InternalDelegates/NetworkFetchFlowDelegate.swift b/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/InternalDelegates/NetworkFetchFlowDelegate.swift index 34d23f95..e3011fcf 100644 --- a/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/InternalDelegates/NetworkFetchFlowDelegate.swift +++ b/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/InternalDelegates/NetworkFetchFlowDelegate.swift @@ -53,8 +53,8 @@ protocol NetworkFetchFlowDelegate { func downloadingMessagesAndListingAttachmentFailed(for: ObvCryptoIdentity, andDeviceUid: UID, flowId: FlowIdentifier) func downloadingMessagesAndListingAttachmentWasNotNeeded(for: ObvCryptoIdentity, andDeviceUid: UID, flowId: FlowIdentifier) - func downloadingMessagesAndListingAttachmentWasPerformed(for: ObvCryptoIdentity, andDeviceUid: UID, idsOfNewMessages: [MessageIdentifier], flowId: FlowIdentifier) - func aMessageReceivedThroughTheWebsocketWasSavedByTheMessageDelegate(for identity: ObvCryptoIdentity, idOfNewMessage: MessageIdentifier, flowId: FlowIdentifier) + func downloadingMessagesAndListingAttachmentWasPerformed(for: ObvCryptoIdentity, andDeviceUid: UID, flowId: FlowIdentifier) + func aMessageReceivedThroughTheWebsocketWasSavedByTheMessageDelegate(flowId: FlowIdentifier) func processUnprocessedMessages(flowId: FlowIdentifier) func messagePayloadAndFromIdentityWereSet(messageId: MessageIdentifier, attachmentIds: [AttachmentIdentifier], hasEncryptedExtendedMessagePayload: Bool, flowId: FlowIdentifier) diff --git a/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/ObvNetworkFetchManagerImplementationDummy.swift b/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/ObvNetworkFetchManagerImplementationDummy.swift index a5547191..bec784a4 100644 --- a/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/ObvNetworkFetchManagerImplementationDummy.swift +++ b/Engine/ObvNetworkFetchManager/ObvNetworkFetchManager/ObvNetworkFetchManagerImplementationDummy.swift @@ -26,8 +26,9 @@ import ObvTypes import OlvidUtils -public final class ObvNetworkFetchManagerImplementationDummy: ObvNetworkFetchDelegate { - +public final class ObvNetworkFetchManagerImplementationDummy: ObvNetworkFetchDelegate, ObvErrorMaker { + + public static var errorDomain = "ObvNetworkFetchManagerImplementationDummy" static let defaultLogSubsystem = "io.olvid.network.fetch.dummy" lazy public var logSubsystem: String = { return ObvNetworkFetchManagerImplementationDummy.defaultLogSubsystem @@ -110,27 +111,27 @@ public final class ObvNetworkFetchManagerImplementationDummy: ObvNetworkFetchDel public func allAttachmentsCanBeDownloadedForMessage(withId: MessageIdentifier, within: ObvContext) throws -> Bool { os_log("allAttachmentsCanBeDownloadedForMessage(withId: MessageIdentifier, within: ObvContext) does nothing in this dummy implementation", log: log, type: .error) - throw NSError() + throw Self.makeError(message: "allAttachmentsCanBeDownloadedForMessage(withId: MessageIdentifier, within: ObvContext) does nothing in this dummy implementation") } public func allAttachmentsHaveBeenDownloadedForMessage(withId: MessageIdentifier, within: ObvContext) throws -> Bool { os_log("allAttachmentsHaveBeenDownloadedForMessage(withId: MessageIdentifier, within: ObvContext) does nothing in this dummy implementation", log: log, type: .error) - throw NSError() + throw Self.makeError(message: "allAttachmentsHaveBeenDownloadedForMessage(withId: MessageIdentifier, within: ObvContext) does nothing in this dummy implementation") } public func attachment(withId: AttachmentIdentifier, canBeDownloadedwithin: ObvContext) throws -> Bool { os_log("attachment(withId: AttachmentIdentifier, canBeDownloadedwithin: ObvContext) does nothing in this dummy implementation", log: log, type: .error) - throw NSError() + throw Self.makeError(message: "attachment(withId: AttachmentIdentifier, canBeDownloadedwithin: ObvContext) does nothing in this dummy implementation") } public func set(remoteCryptoIdentity: ObvCryptoIdentity, messagePayload: Data, extendedMessagePayloadKey: AuthenticatedEncryptionKey?, andAttachmentsInfos: [ObvNetworkFetchAttachmentInfos], forApplicationMessageWithmessageId: MessageIdentifier, within obvContext: ObvContext) throws { os_log("set(remoteCryptoIdentity: ObvCryptoIdentity, messagePayload: Data, andAttachmentsInfos: [ObvNetworkFetchAttachmentInfos], forApplicationMessageWithMessageId: MessageIdentifier, within obvContext: ObvContext) does nothing in this dummy implementation", log: log, type: .error) - throw NSError() + throw Self.makeError(message: "set(remoteCryptoIdentity: ObvCryptoIdentity, messagePayload: Data, andAttachmentsInfos: [ObvNetworkFetchAttachmentInfos], forApplicationMessageWithMessageId: MessageIdentifier, within obvContext: ObvContext) does nothing in this dummy implementation") } public func pauseDownloadOfAttachment(attachmentId: AttachmentIdentifier, flowId: FlowIdentifier) throws { os_log("pauseDownloadOfAttachment(attachmentId: AttachmentIdentifier, flowId: FlowIdentifier) does nothing in this dummy implementation", log: log, type: .error) - throw NSError() + throw Self.makeError(message: "pauseDownloadOfAttachment(attachmentId: AttachmentIdentifier, flowId: FlowIdentifier) does nothing in this dummy implementation") } public func requestProgressesOfAllInboxAttachmentsOfMessage(withIdentifier messageIdentifier: MessageIdentifier, flowId: FlowIdentifier) { diff --git a/Engine/ObvProtocolManager/ObvProtocolManager/ObvProtocolManager.swift b/Engine/ObvProtocolManager/ObvProtocolManager/ObvProtocolManager.swift index b15d70fa..32eb1279 100644 --- a/Engine/ObvProtocolManager/ObvProtocolManager/ObvProtocolManager.swift +++ b/Engine/ObvProtocolManager/ObvProtocolManager/ObvProtocolManager.swift @@ -246,7 +246,7 @@ extension ObvProtocolManager { guard let genericReceivedMessage = GenericReceivedProtocolMessage(with: obvProtocolReceivedMessage) else { os_log("Could not parse the protocol received message", log: log, type: .error) - throw NSError() + throw Self.makeError(message: "Could not parse the protocol received message") } save(genericReceivedMessage, within: obvContext) @@ -257,8 +257,8 @@ extension ObvProtocolManager { public func process(_ obvProtocolReceivedDialogResponse: ObvProtocolReceivedDialogResponse, within obvContext: ObvContext) throws { guard let genericReceivedMessage = GenericReceivedProtocolMessage(with: obvProtocolReceivedDialogResponse) else { - os_log("Could not parse the protocol received dialog response ", log: log, type: .error) - throw NSError() + os_log("Could not parse the protocol received dialog response", log: log, type: .error) + throw Self.makeError(message: "Could not parse the protocol received dialog response ") } save(genericReceivedMessage, within: obvContext) @@ -269,8 +269,8 @@ extension ObvProtocolManager { public func process(_ obvProtocolReceivedServerResponse: ObvProtocolReceivedServerResponse, within obvContext: ObvContext) throws { guard let genericReceivedMessage = GenericReceivedProtocolMessage(with: obvProtocolReceivedServerResponse) else { - os_log("Could not parse the protocol received dialog response ", log: log, type: .error) - throw NSError() + os_log("Could not parse the protocol received server response", log: log, type: .error) + throw Self.makeError(message: "Could not parse the protocol received server response") } save(genericReceivedMessage, within: obvContext) @@ -281,6 +281,7 @@ extension ObvProtocolManager { guard let notificationDelegate = delegateManager.notificationDelegate else { os_log("The notification delegate is not set", log: log, type: .fault) + assertionFailure() return } diff --git a/iOSClient/ObvMessenger/ObvMessenger.xcodeproj/project.pbxproj b/iOSClient/ObvMessenger/ObvMessenger.xcodeproj/project.pbxproj index f07731bf..3ed8601d 100644 --- a/iOSClient/ObvMessenger/ObvMessenger.xcodeproj/project.pbxproj +++ b/iOSClient/ObvMessenger/ObvMessenger.xcodeproj/project.pbxproj @@ -102,6 +102,8 @@ C0541723248A40F20055B72C /* PersistedMessageExpiration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0541722248A40F20055B72C /* PersistedMessageExpiration.swift */; }; C0541725248A40F20055B72C /* PersistedMessageExpiration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0541722248A40F20055B72C /* PersistedMessageExpiration.swift */; }; C0567FEB277241E700313EFB /* ExternalLibrariesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0567FEA277241E700313EFB /* ExternalLibrariesViewController.swift */; }; + C05C197227D91907007D4032 /* DeleteOldPendingRepliedToOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05C197127D91907007D4032 /* DeleteOldPendingRepliedToOperation.swift */; }; + C05F8FCF27D57A0400B236B1 /* CreateUnprocessedReplyToPersistedMessageSentFromBodyOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C05F8FCE27D57A0400B236B1 /* CreateUnprocessedReplyToPersistedMessageSentFromBodyOperation.swift */; }; C0601BB12782F59900120A27 /* ChangeNewComposeMessageViewActionOrderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0601BB02782F59900120A27 /* ChangeNewComposeMessageViewActionOrderViewController.swift */; }; C064062826A1C70F00B25290 /* ObvAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C04BABA126A1C43B00FBF283 /* ObvAudioPlayer.swift */; }; C06831C62722F60300C2693B /* MissedMessageBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06831C52722F60300C2693B /* MissedMessageBubbleView.swift */; }; @@ -184,6 +186,7 @@ C0B6C2E727A06EE300434D50 /* ObvNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C400059F20CB69A500AC148C /* ObvNavigationController.swift */; }; C0B6C33527A071FB00434D50 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C4CB84D92084B0DA004D0730 /* Assets.xcassets */; }; C0B6D92B27E49009006C8C9B /* SaveContextOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B6D92A27E49009006C8C9B /* SaveContextOperation.swift */; }; + C0B6E0FF27EB6FCC006C8C9B /* MarkAsReadReceivedMessageOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B6E0FE27EB6FCC006C8C9B /* MarkAsReadReceivedMessageOperation.swift */; }; C0BB70C22487928800AFD692 /* NSNumber+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0BB70C12487928800AFD692 /* NSNumber+Utils.swift */; }; C0BB70C42487928900AFD692 /* NSNumber+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0BB70C12487928800AFD692 /* NSNumber+Utils.swift */; }; C0BC223E24ADD10B00227D15 /* CellWithMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0BC223D24ADD10B00227D15 /* CellWithMessage.swift */; }; @@ -204,6 +207,7 @@ C0CC887E269634B6009CAE24 /* RTCSdpType+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0CC887D269634B5009CAE24 /* RTCSdpType+Extension.swift */; }; C0D4BF082625F47E001A561B /* MigrationAppDatabase_v28_to_v29.txt in Resources */ = {isa = PBXBuildFile; fileRef = C0D4BF072625F47E001A561B /* MigrationAppDatabase_v28_to_v29.txt */; }; C0D4BF0C2625F6BD001A561B /* ObvMessengerMappingModel_v28_to_v29.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = C0D4BF0B2625F6BD001A561B /* ObvMessengerMappingModel_v28_to_v29.xcmappingmodel */; }; + C0D7ACBC27DB5ACB009C5338 /* CreateUnprocessedPersistedMessageSentFromBodyOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D7ACBB27DB5ACB009C5338 /* CreateUnprocessedPersistedMessageSentFromBodyOperation.swift */; }; C0D7ACFD27DBB5BE009C5338 /* NSManagedObject+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0D7ACFC27DBB5BE009C5338 /* NSManagedObject+Utils.swift */; }; C0DA1E642652C62F003A7756 /* CleanExpiredMuteNotficationEndDatesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DA1E632652C62F003A7756 /* CleanExpiredMuteNotficationEndDatesOperation.swift */; }; C0DA20152656EC52003A7756 /* WebRTCDataChannelMessageJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = C48177F8251B8F2800D8BEC7 /* WebRTCDataChannelMessageJSON.swift */; }; @@ -481,7 +485,6 @@ C44BDC7C22D63FFD00532073 /* PrivacyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44BDC7B22D63FFD00532073 /* PrivacyViewController.swift */; }; C44CB7CE26AEC12A00BB4389 /* ConfirmAddContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CB7CD26AEC12A00BB4389 /* ConfirmAddContactView.swift */; }; C44CCFED24CBA61B006E0428 /* RTCIceGatheringState+CustomDebugStringConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44CCFEC24CBA61B006E0428 /* RTCIceGatheringState+CustomDebugStringConvertible.swift */; }; - C44D96E3281E976100E00119 /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = C4F6E51B2778E71600DEA75F /* AppAuth */; }; C44DC382251BB30B00CBE322 /* DataChannelWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44DC381251BB30B00CBE322 /* DataChannelWorker.swift */; }; C44FB720237E0812000C09D4 /* PrivacyTableViewController+Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44FB71F237E0812000C09D4 /* PrivacyTableViewController+Strings.swift */; }; C44FDEB122555CFF000BDC76 /* NoChannelCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44FDEB022555CFF000BDC76 /* NoChannelCollectionReusableView.swift */; }; @@ -916,6 +919,7 @@ C4DAAD2A20AEDCD0005E63C0 /* ButtonsCardCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DAAD2820AEDCD0005E63C0 /* ButtonsCardCollectionViewCell.swift */; }; C4DAAD2D20AEDD33005E63C0 /* ButtonsCardCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C4DAAD2C20AEDD33005E63C0 /* ButtonsCardCollectionViewCell.xib */; }; C4DAAD2F20AEE07A005E63C0 /* ObvButtonBorderless.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DAAD2E20AEE07A005E63C0 /* ObvButtonBorderless.swift */; }; + C4DAE30B282518600013853E /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = C4F6E51B2778E71600DEA75F /* AppAuth */; }; C4DB114C2763C77500740136 /* ObvSimpleListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DB114B2763C77500740136 /* ObvSimpleListItemView.swift */; }; C4DB30122556E1D300060706 /* AvailableSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DB30112556E1D300060706 /* AvailableSubscription.swift */; }; C4DB63882409590800C02ADF /* BackupTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4DB63872409590800C02ADF /* BackupTableViewController.swift */; }; @@ -1176,6 +1180,8 @@ C051CD85264C244700165E15 /* Bindings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bindings.swift; sourceTree = ""; }; C0541722248A40F20055B72C /* PersistedMessageExpiration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistedMessageExpiration.swift; sourceTree = ""; }; C0567FEA277241E700313EFB /* ExternalLibrariesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalLibrariesViewController.swift; sourceTree = ""; }; + C05C197127D91907007D4032 /* DeleteOldPendingRepliedToOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteOldPendingRepliedToOperation.swift; sourceTree = ""; }; + C05F8FCE27D57A0400B236B1 /* CreateUnprocessedReplyToPersistedMessageSentFromBodyOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateUnprocessedReplyToPersistedMessageSentFromBodyOperation.swift; sourceTree = ""; }; C0601BB02782F59900120A27 /* ChangeNewComposeMessageViewActionOrderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeNewComposeMessageViewActionOrderViewController.swift; sourceTree = ""; }; C06831C52722F60300C2693B /* MissedMessageBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MissedMessageBubbleView.swift; sourceTree = ""; }; C06902E32677A97D00FD8F92 /* ReportEndCallOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportEndCallOperation.swift; sourceTree = ""; }; @@ -1222,6 +1228,7 @@ C0B6C1A427A042C500434D50 /* ProfilePictureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePictureView.swift; sourceTree = ""; }; C0B6C1A727A0431C00434D50 /* TextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextView.swift; sourceTree = ""; }; C0B6D92A27E49009006C8C9B /* SaveContextOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveContextOperation.swift; sourceTree = ""; }; + C0B6E0FE27EB6FCC006C8C9B /* MarkAsReadReceivedMessageOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkAsReadReceivedMessageOperation.swift; sourceTree = ""; }; C0BB70C12487928800AFD692 /* NSNumber+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSNumber+Utils.swift"; sourceTree = ""; }; C0BC223D24ADD10B00227D15 /* CellWithMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellWithMessage.swift; sourceTree = ""; }; C0BC224124AF279900227D15 /* InfosOfSentMessageTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfosOfSentMessageTableViewController.swift; sourceTree = ""; }; @@ -1238,6 +1245,7 @@ C0CC887D269634B5009CAE24 /* RTCSdpType+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RTCSdpType+Extension.swift"; sourceTree = ""; }; C0D4BF072625F47E001A561B /* MigrationAppDatabase_v28_to_v29.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = MigrationAppDatabase_v28_to_v29.txt; sourceTree = ""; }; C0D4BF0B2625F6BD001A561B /* ObvMessengerMappingModel_v28_to_v29.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = ObvMessengerMappingModel_v28_to_v29.xcmappingmodel; sourceTree = ""; }; + C0D7ACBB27DB5ACB009C5338 /* CreateUnprocessedPersistedMessageSentFromBodyOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateUnprocessedPersistedMessageSentFromBodyOperation.swift; sourceTree = ""; }; C0D7ACFC27DBB5BE009C5338 /* NSManagedObject+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObject+Utils.swift"; sourceTree = ""; }; C0DA1E632652C62F003A7756 /* CleanExpiredMuteNotficationEndDatesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CleanExpiredMuteNotficationEndDatesOperation.swift; sourceTree = ""; }; C0E1D2282718736A0085BAA2 /* ICloudBackupListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ICloudBackupListView.swift; sourceTree = ""; }; @@ -2081,7 +2089,7 @@ C4CB87662084D644004D0730 /* ObvCrypto.framework in Frameworks */, C4B140BF20F41D2A007AB7F5 /* ObvEngine.framework in Frameworks */, C4CB877C2084D720004D0730 /* ObvProtocolManager.framework in Frameworks */, - C44D96E3281E976100E00119 /* AppAuth in Frameworks */, + C4DAE30B282518600013853E /* AppAuth in Frameworks */, C4CB87722084D6F8004D0730 /* ObvMetaManager.framework in Frameworks */, C418756526308E2800761E31 /* OlvidUtils.framework in Frameworks */, ); @@ -2339,6 +2347,8 @@ C4238AB42507B6D30005EFCE /* CreateUnprocessedPersistedMessageSentFromPersistedDraftOperation.swift */, C40F986126BE107B00BC055A /* ComputeExtendedPayloadOperation.swift */, C40EDA482507BD4200872B80 /* SendUnprocessedPersistedMessageSentOperation.swift */, + C05F8FCE27D57A0400B236B1 /* CreateUnprocessedReplyToPersistedMessageSentFromBodyOperation.swift */, + C0D7ACBB27DB5ACB009C5338 /* CreateUnprocessedPersistedMessageSentFromBodyOperation.swift */, C080975D27D2792C003E2C4B /* RefreshUpdatedObjectsModifiedByShareExtensionOperation.swift */, ); path = "Sending messages"; @@ -3311,6 +3321,7 @@ C4A4FB2E27A2E56300C430A3 /* AutoAcceptPendingGroupInvitesIfPossibleOperation.swift */, C433D20227A35B8E0077976E /* DeletePersistedInvitationTheCannotBeParsedAnymoreOperation.swift */, C4815C9727A84CA700512F4B /* SyncPersistedInvitationsWithEngineOperation.swift */, + C05C197127D91907007D4032 /* DeleteOldPendingRepliedToOperation.swift */, ); path = Operations; sourceTree = ""; @@ -3707,6 +3718,7 @@ C48FDE7327030FDF0088F07B /* CleanOrphanedPersistedMessageTimestampedMetadataOperation.swift */, C0C2D651275A5C2C001ECCBF /* DeleteOldOrOrphanedPendingReactionsOperation.swift */, C0C2D655275A6137001ECCBF /* ApplyPendingReactionsOperation.swift */, + C0B6E0FE27EB6FCC006C8C9B /* MarkAsReadReceivedMessageOperation.swift */, ); path = "Receiving messages"; sourceTree = ""; @@ -5569,6 +5581,7 @@ C46F28442587E12C0079BA89 /* UtilsForAppMigrationV24ToV25.swift in Sources */, C4E9E91020EE58B300A2CD4C /* SasAcceptedView.swift in Sources */, C4EC96592591632500422DF1 /* SentMessageInfosHostingViewController.swift in Sources */, + C0D7ACBC27DB5ACB009C5338 /* CreateUnprocessedPersistedMessageSentFromBodyOperation.swift in Sources */, C4E8E295226C770300CF83F7 /* SettingsFlowViewController.swift in Sources */, C427B2F12519506B00D37D86 /* CallView.swift in Sources */, C4FAE4B2250AE2D700A7468C /* ReceivingMessageAndAttachmentsOperations.swift in Sources */, @@ -5653,6 +5666,7 @@ C47E8BFA22A184FF002DB74F /* PendingGroupMembersTableViewController+Strings.swift in Sources */, C403CB9A23B43E650026EF32 /* BlockBarButtonItem.swift in Sources */, C4814BD7218B166700F6743B /* TrustOriginsTableViewController.swift in Sources */, + C0B6E0FF27EB6FCC006C8C9B /* MarkAsReadReceivedMessageOperation.swift in Sources */, C022097727A4555C006E330C /* PersistedDiscussionLocalConfiguration+Backup.swift in Sources */, C4F08CB2226F33D8003719C0 /* UIImage+Insets.swift in Sources */, C42C1E482518FEBC00F77B1A /* RoundedButtonView.swift in Sources */, @@ -5889,6 +5903,7 @@ C49399C0268F609B009DCC82 /* MessageCellConstants.swift in Sources */, C4EA017422010CEF00FAD04A /* PersistedObvContactIdentityToPersistedObvContactIdentityMigrationPolicyV8ToV9.swift in Sources */, C4EA17832086A834004B312B /* DiscussionsFlowViewController.swift in Sources */, + C05F8FCF27D57A0400B236B1 /* CreateUnprocessedReplyToPersistedMessageSentFromBodyOperation.swift in Sources */, C46A987A26910F22003ABC43 /* RemoveReplyToOnDraftOperation.swift in Sources */, C4B067F127679F780002DC39 /* AppBackupItem.swift in Sources */, C4F7305C25A3D4FF003D2363 /* MessageMetadatasSectionView.swift in Sources */, @@ -5909,6 +5924,7 @@ C4FE5E712590BCF400453AA9 /* DeleteMessagesWithExpiredCountBasedRetentionOperation.swift in Sources */, C413126B20B1543600F8FF94 /* InvitationCollectionCell.swift in Sources */, C4AEE86225592B230059FB66 /* BackupKeyPartTextField.swift in Sources */, + C05C197227D91907007D4032 /* DeleteOldPendingRepliedToOperation.swift in Sources */, C04E49A626E22A370042DDB6 /* UpdateDraftBodyOperation.swift in Sources */, C44438312551D0B60073EFB5 /* PeriodUnit+localizedDescription.swift in Sources */, C4CCF7A722671FDB0089B46F /* ObvAutoGrowingTextViewDelegate.swift in Sources */, @@ -6215,7 +6231,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 495; + CURRENT_PROJECT_VERSION = 501; DEVELOPMENT_TEAM = VMDQ4PU27W; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ObvMessengerIntentsExtension/Info.plist; @@ -6227,7 +6243,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 0.10.0; + MARKETING_VERSION = 0.10.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "io.olvid.messenger-debug.ObvMessengerIntentsExtension"; @@ -6246,7 +6262,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 495; + CURRENT_PROJECT_VERSION = 501; DEVELOPMENT_TEAM = VMDQ4PU27W; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = ObvMessengerIntentsExtension/Info.plist; @@ -6258,7 +6274,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 0.10.0; + MARKETING_VERSION = 0.10.1; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = io.olvid.messenger.ObvMessengerIntentsExtension; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6276,7 +6292,7 @@ CODE_SIGN_ENTITLEMENTS = ObvMessengerNotificationServiceExtension/ObvMessengerNotificationServiceExtensionDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 495; + CURRENT_PROJECT_VERSION = 501; DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES; DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; @@ -6287,7 +6303,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 0.10.0; + MARKETING_VERSION = 0.10.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(OBV_PRODUCT_BUNDLE_IDENTIFIER_FOR_NOTIFICATION_SERVICE_EXTENSION)"; @@ -6307,7 +6323,7 @@ CODE_SIGN_ENTITLEMENTS = ObvMessengerNotificationServiceExtension/ObvMessengerNotificationServiceExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 495; + CURRENT_PROJECT_VERSION = 501; DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES; DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; @@ -6318,7 +6334,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 0.10.0; + MARKETING_VERSION = 0.10.1; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(OBV_PRODUCT_BUNDLE_IDENTIFIER_FOR_NOTIFICATION_SERVICE_EXTENSION)"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6470,7 +6486,7 @@ CODE_SIGN_ENTITLEMENTS = ObvMessenger/ObvMessengerDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 495; + CURRENT_PROJECT_VERSION = 501; DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; @@ -6486,7 +6502,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.10.0; + MARKETING_VERSION = 0.10.1; OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-function-bodies=500 -Xfrontend -warn-long-expression-type-checking=1500"; PRODUCT_BUNDLE_IDENTIFIER = "$(OBV_PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -6508,7 +6524,7 @@ CODE_SIGN_ENTITLEMENTS = ObvMessenger/ObvMessenger.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 495; + CURRENT_PROJECT_VERSION = 501; DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; @@ -6525,7 +6541,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.10.0; + MARKETING_VERSION = 0.10.1; PRODUCT_BUNDLE_IDENTIFIER = "$(OBV_PRODUCT_BUNDLE_IDENTIFIER)"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -6542,7 +6558,7 @@ CODE_SIGN_ENTITLEMENTS = ObvMessengerShareExtension/ObvMessengerShareExtensionDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 495; + CURRENT_PROJECT_VERSION = 501; DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES; DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; @@ -6553,7 +6569,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 0.10.0; + MARKETING_VERSION = 0.10.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(OBV_PRODUCT_BUNDLE_IDENTIFIER_FOR_SHARE_EXTENSION)"; @@ -6573,7 +6589,7 @@ CODE_SIGN_ENTITLEMENTS = ObvMessengerShareExtension/ObvMessengerShareExtension.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 495; + CURRENT_PROJECT_VERSION = 501; DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES; DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; @@ -6584,7 +6600,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 0.10.0; + MARKETING_VERSION = 0.10.1; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "$(OBV_PRODUCT_BUNDLE_IDENTIFIER_FOR_SHARE_EXTENSION)"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/iOSClient/ObvMessenger/ObvMessenger/AppDelegate.swift b/iOSClient/ObvMessenger/ObvMessenger/AppDelegate.swift index f2d27a8f..368abd4a 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/AppDelegate.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/AppDelegate.swift @@ -75,10 +75,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } obvEngine?.applicationDidEnterBackground() BackgroundTasksManager.shared.cancelAllPendingBGTask() - scheduleBackgroundTaskForCleaningExpiredMessages() - scheduleBackgroundTaskForApplyingRetentionPolicies() - scheduleBackgroundTaskForUpdatingBadge() - scheduleBackgroundTaskForListingMessagesOnServer() + BackgroundTasksManager.shared.scheduleBackgroundTasks() } diff --git a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/BoostrapCoordinator/BoostrapCoordinator.swift b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/BoostrapCoordinator/BoostrapCoordinator.swift index 0648f14e..04bc5f43 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/BoostrapCoordinator/BoostrapCoordinator.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/BoostrapCoordinator/BoostrapCoordinator.swift @@ -120,19 +120,13 @@ extension BootstrapCoordinator { private func deleteOldPendingRepliedTo() { - let log = self.log - ObvStack.shared.performBackgroundTaskAndWait { context in - do { - try PersistedMessageReceived.batchDeletePendingRepliedToEntriesOlderThan(Date(timeIntervalSinceNow: -TimeInterval(months: 1)), within: context) - try context.save(logOnFailure: log) - } catch { - assertionFailure() - os_log("Failed to delete old PendingRepliedTo entries: %{public}@", log: log, type: .fault, error.localizedDescription) - } - } + assert(!Thread.isMainThread) + let op1 = DeleteOldPendingRepliedToOperation() + let composedOp = CompositionOfOneContextualOperation(op1: op1, contextCreator: ObvStack.shared, log: log, flowId: FlowIdentifier()) + internalQueue.addOperations([composedOp], waitUntilFinished: true) + composedOp.logReasonIfCancelled(log: log) } - - + private func removeOldCachedURLMetadata() { let dateLimit = Date().addingTimeInterval(TimeInterval(integerLiteral: -ObvMessengerConstants.TTL.cachedURLMetadata)) LPMetadataProvider.removeCachedURLMetadata(olderThan: dateLimit) diff --git a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/BoostrapCoordinator/Operations/DeleteOldPendingRepliedToOperation.swift b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/BoostrapCoordinator/Operations/DeleteOldPendingRepliedToOperation.swift new file mode 100644 index 00000000..8757cb59 --- /dev/null +++ b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/BoostrapCoordinator/Operations/DeleteOldPendingRepliedToOperation.swift @@ -0,0 +1,41 @@ +/* + * Olvid for iOS + * Copyright © 2019-2022 Olvid SAS + * + * This file is part of Olvid for iOS. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + + +import Foundation +import OlvidUtils +import os.log + +final class DeleteOldPendingRepliedToOperation: ContextualOperationWithSpecificReasonForCancel { + + override func main() { + + guard let obvContext = self.obvContext else { + return cancel(withReason: .contextIsNil) + } + + obvContext.performAndWait { + do { + try PersistedMessageReceived.batchDeletePendingRepliedToEntriesOlderThan(Date(timeIntervalSinceNow: -TimeInterval(months: 1)), within: obvContext.context) + } catch let error { + return cancel(withReason: .coreDataError(error: error)) + } + } + } +} diff --git a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/ExpirationMessagesCoordinator.swift b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/ExpirationMessagesCoordinator.swift index 1e60089f..223bd50c 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/ExpirationMessagesCoordinator.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/ExpirationMessagesCoordinator.swift @@ -75,13 +75,7 @@ final class ExpirationMessagesCoordinator { private func observeCleanExpiredMessagesBackgroundTaskWasLaunched() { observationTokens.append(ObvMessengerInternalNotification.observeCleanExpiredMessagesBackgroundTaskWasLaunched { (completion) in - let completionHandler: (Bool) -> Void = { (success) in - DispatchQueue.main.async { - (UIApplication.shared.delegate as? AppDelegate)?.scheduleBackgroundTaskForCleaningExpiredMessages() - completion(success) - } - } - ObvMessengerInternalNotification.wipeAllMessagesThatExpiredEarlierThanNow(launchedByBackgroundTask: true, completionHandler: completionHandler) + ObvMessengerInternalNotification.wipeAllMessagesThatExpiredEarlierThanNow(launchedByBackgroundTask: true, completionHandler: completion) .postOnDispatchQueue() }) } @@ -181,43 +175,3 @@ protocol ScheduleNextTimerOperationDelegate: AnyObject { func replaceCurrentTimerWith(newTimer: Timer) func timerFired(timer: Timer) } - - - -// MARK: - Extending AppDelegate for managing the background task allowing to wipe expired messages - -extension AppDelegate { - - /// If there exists at least one message expiration in database, this method schedules a background task allowing to perform a wipe of the associated message in the background. - /// This method is called when the app goes in the background. - func scheduleBackgroundTaskForCleaningExpiredMessages() { - // We make sure the app was initialized. Otherwise, the shared stack is not garanteed to exist. Accessing it would crash the app. - guard AppStateManager.shared.currentState.isInitialized else { return } - ObvStack.shared.performBackgroundTaskAndWait { (context) in - let log = ExpirationMessagesCoordinator.log - let nextExpirationDate: Date - do { - guard let expiration = try PersistedMessageExpiration.getEarliestExpiration(laterThan: Date(), within: context) else { - os_log("🤿 We do not schedule any background task for message expiration since there is no expiration left", log: log, type: .info) - return - } - nextExpirationDate = expiration.expirationDate - } catch { - os_log("🤿 We could not get earliest expiration: %{public}@", log: log, type: .fault, error.localizedDescription) - assertionFailure() - return - } - // If we reach this point, we should schedule a background task for message expiration - do { - try BackgroundTasksManager.shared.submit(task: .cleanExpiredMessages, earliestBeginDate: nextExpirationDate) - } catch { - guard ObvMessengerConstants.isRunningOnRealDevice else { assertionFailure("We should not be scheduling BG tasks on a simulator as they are unsuported"); return } - os_log("🤿 Could not schedule next expiration: %{public}@", log: log, type: .fault, error.localizedDescription) - assertionFailure() - return - } - } - } - - -} diff --git a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/Operations/Receiving messages/MarkAsReadReceivedMessageOperation.swift b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/Operations/Receiving messages/MarkAsReadReceivedMessageOperation.swift new file mode 100644 index 00000000..744436b6 --- /dev/null +++ b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/Operations/Receiving messages/MarkAsReadReceivedMessageOperation.swift @@ -0,0 +1,100 @@ +/* + * Olvid for iOS + * Copyright © 2019-2022 Olvid SAS + * + * This file is part of Olvid for iOS. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + + +import Foundation +import os.log +import CoreData +import OlvidUtils + +final class MarkAsReadReceivedMessageOperation: ContextualOperationWithSpecificReasonForCancel { + + private let log = OSLog(subsystem: ObvMessengerConstants.logSubsystem, category: String(describing: MarkAsReadReceivedMessageOperation.self)) + + let persistedContactObjectID: NSManagedObjectID + let messageIdentifierFromEngine: Data + + private(set) var persistedMessageSentObjectID: TypeSafeManagedObjectID? + + init(persistedContactObjectID: NSManagedObjectID, messageIdentifierFromEngine: Data) { + self.persistedContactObjectID = persistedContactObjectID + self.messageIdentifierFromEngine = messageIdentifierFromEngine + super.init() + } + + override func main() { + + guard let obvContext = self.obvContext else { + return cancel(withReason: .contextIsNil) + } + + obvContext.performAndWait { + do { + guard let contactIdentity = try PersistedObvContactIdentity.get(objectID: persistedContactObjectID, within: obvContext.context) else { + assertionFailure() + return cancel(withReason: .couldNotFindContactIdentityInDatabase) + } + + // Find message to mark as read + guard let message = try PersistedMessageReceived.get(messageIdentifierFromEngine: messageIdentifierFromEngine, from: contactIdentity) else { + assertionFailure() + return cancel(withReason: .couldNotFindReceivedMessageInDatabase) + } + + try message.markAsNotNew(now: Date()) + + } catch(let error) { + assertionFailure() + return cancel(withReason: .coreDataError(error: error)) + } + } + + } +} + +enum MarkAsReadReceivedMessageOperationReasonForCancel: LocalizedErrorWithLogType { + + case contextIsNil + case coreDataError(error: Error) + case couldNotFindContactIdentityInDatabase + case couldNotFindReceivedMessageInDatabase + + var logType: OSLogType { + switch self { + case .contextIsNil: + return .fault + case .coreDataError: + return .fault + case .couldNotFindReceivedMessageInDatabase: + return .error + case .couldNotFindContactIdentityInDatabase: + return .error + } + } + + var errorDescription: String? { + switch self { + case .contextIsNil: return "Context is nil" + case .couldNotFindContactIdentityInDatabase: return "Could not obtain persisted contact identity in database" + case .coreDataError(error: let error): return "Core Data error: \(error.localizedDescription)" + case .couldNotFindReceivedMessageInDatabase: return "Could not find received message in database" + } + } + +} diff --git a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/Operations/Receiving messages/ReceivingMessageAndAttachmentsOperations.swift b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/Operations/Receiving messages/ReceivingMessageAndAttachmentsOperations.swift index 0818735f..5d05bf86 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/Operations/Receiving messages/ReceivingMessageAndAttachmentsOperations.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/Operations/Receiving messages/ReceivingMessageAndAttachmentsOperations.swift @@ -56,25 +56,20 @@ final class CreatePersistedMessageReceivedFromReceivedObvMessageOperation: Conte let currentUserActivityPersistedDiscussionObjectID = ObvUserActivitySingleton.shared.currentPersistedDiscussionObjectID obvContext.performAndWait { - - // Grab the persisted contact and the appropriate discussion - let persistedContactIdentity: PersistedObvContactIdentity do { - guard let _persistedContactIdentity = try PersistedObvContactIdentity.get(persisted: obvMessage.fromContactIdentity, whereOneToOneStatusIs: .any, within: obvContext.context) else { + + // Grab the persisted contact and the appropriate discussion + + guard let persistedContactIdentity = try PersistedObvContactIdentity.get(persisted: obvMessage.fromContactIdentity, whereOneToOneStatusIs: .any, within: obvContext.context) else { return cancel(withReason: .couldNotFindPersistedObvContactIdentityInDatabase) } - persistedContactIdentity = _persistedContactIdentity - } catch { - return cancel(withReason: .coreDataError(error: error)) - } - - guard let ownedIdentity = persistedContactIdentity.ownedIdentity else { - return cancel(withReason: .couldNotDetermineOwnedIdentity) - } - - let discussion: PersistedDiscussion - do { + + guard let ownedIdentity = persistedContactIdentity.ownedIdentity else { + return cancel(withReason: .couldNotDetermineOwnedIdentity) + } + + let discussion: PersistedDiscussion if let groupId = messageJSON.groupId { guard let contactGroup = try PersistedContactGroup.getContactGroup(groupId: groupId, ownedIdentity: ownedIdentity) else { return cancel(withReason: .couldNotFindPersistedContactGroupInDatabase) @@ -88,38 +83,88 @@ final class CreatePersistedMessageReceivedFromReceivedObvMessageOperation: Conte } else { return cancel(withReason: .couldNotFindDiscussion) } - } catch { - return cancel(withReason: .coreDataError(error: error)) - } - - // Try to insert a EndToEndEncryptedSystemMessage if the discussion is empty - - try? PersistedDiscussion.insertSystemMessagesIfDiscussionIsEmpty(discussionObjectID: discussion.objectID, markAsRead: true, within: obvContext.context) - - // If overridePreviousPersistedMessage is true, we update any previously stored message from DB. If no such message exists, we create it. - // If overridePreviousPersistedMessage is false, we make sure that no existing PersistedMessageReceived exists in DB. If this is the case, we create the message. - // Note that processing attachments requires overridePreviousPersistedMessage to be true - - if overridePreviousPersistedMessage { - if let previousMessage = PersistedMessageReceived.get(messageIdentifierFromEngine: obvMessage.messageIdentifierFromEngine, from: persistedContactIdentity) { + // Try to insert a EndToEndEncryptedSystemMessage if the discussion is empty + + try? PersistedDiscussion.insertSystemMessagesIfDiscussionIsEmpty(discussionObjectID: discussion.objectID, markAsRead: true, within: obvContext.context) + + // If overridePreviousPersistedMessage is true, we update any previously stored message from DB. If no such message exists, we create it. + // If overridePreviousPersistedMessage is false, we make sure that no existing PersistedMessageReceived exists in DB. If this is the case, we create the message. + // Note that processing attachments requires overridePreviousPersistedMessage to be true + + if overridePreviousPersistedMessage { - os_log("Updating a previous received message...", log: log, type: .info) + if let previousMessage = try PersistedMessageReceived.get(messageIdentifierFromEngine: obvMessage.messageIdentifierFromEngine, from: persistedContactIdentity) { + + os_log("Updating a previous received message...", log: log, type: .info) + + do { + try previousMessage.update(withMessageJSON: messageJSON, + messageIdentifierFromEngine: obvMessage.messageIdentifierFromEngine, + returnReceiptJSON: returnReceiptJSON, + messageUploadTimestampFromServer: obvMessage.messageUploadTimestampFromServer, + downloadTimestampFromServer: obvMessage.downloadTimestampFromServer, + localDownloadTimestamp: obvMessage.localDownloadTimestamp, + discussion: discussion) + } catch { + os_log("Could not update existing received message: %{public}@", log: log, type: .error, error.localizedDescription) + } + + } else { + + // Create the PersistedMessageReceived + + os_log("Creating a persisted message (overridePreviousPersistedMessage: %{public}@)", log: log, type: .debug, overridePreviousPersistedMessage.description) + let missedMessageCount = updateNextMessageMissedMessageCountAndGetCurrentMissedMessageCount( + discussion: discussion, + contactIdentity: persistedContactIdentity, + senderThreadIdentifier: messageJSON.senderThreadIdentifier, + senderSequenceNumber: messageJSON.senderSequenceNumber) + + guard (try? PersistedMessageReceived(messageUploadTimestampFromServer: obvMessage.messageUploadTimestampFromServer, + downloadTimestampFromServer: obvMessage.downloadTimestampFromServer, + localDownloadTimestamp: obvMessage.localDownloadTimestamp, + messageJSON: messageJSON, + contactIdentity: persistedContactIdentity, + messageIdentifierFromEngine: obvMessage.messageIdentifierFromEngine, + returnReceiptJSON: returnReceiptJSON, + missedMessageCount: missedMessageCount, + discussion: discussion)) != nil + else { + return cancel(withReason: .couldNotCreatePersistedMessageReceived) + } + + } - do { - try previousMessage.update(withMessageJSON: messageJSON, - messageIdentifierFromEngine: obvMessage.messageIdentifierFromEngine, - returnReceiptJSON: returnReceiptJSON, - messageUploadTimestampFromServer: obvMessage.messageUploadTimestampFromServer, - downloadTimestampFromServer: obvMessage.downloadTimestampFromServer, - localDownloadTimestamp: obvMessage.localDownloadTimestamp, - discussion: discussion) - } catch { - os_log("Could not update existing received message: %{public}@", log: log, type: .error, error.localizedDescription) + // Process the attachments within the message + + for obvAttachment in obvMessage.attachments { + do { + try ReceivingMessageAndAttachmentsOperationHelper.processFyleWithinDownloadingAttachment(obvAttachment, + newProgress: nil, + obvEngine: obvEngine, + log: log, + within: obvContext) + } catch { + os_log("Could not process one of the message's attachments: %{public}@", log: log, type: .fault, error.localizedDescription) + // We continue anyway + } } } else { + // Make sure the message does not already exists in DB + + guard try PersistedMessageReceived.get(messageIdentifierFromEngine: obvMessage.messageIdentifierFromEngine, from: persistedContactIdentity) == nil else { + return + } + + // We make sure that message has a body (for now, this message comes from the notification extension, and there is no point in creating a `PersistedMessageReceived` if there is no body. + + guard messageJSON.body?.isEmpty == false else { + return + } + // Create the PersistedMessageReceived os_log("Creating a persisted message (overridePreviousPersistedMessage: %{public}@)", log: log, type: .debug, overridePreviousPersistedMessage.description) @@ -128,7 +173,7 @@ final class CreatePersistedMessageReceivedFromReceivedObvMessageOperation: Conte contactIdentity: persistedContactIdentity, senderThreadIdentifier: messageJSON.senderThreadIdentifier, senderSequenceNumber: messageJSON.senderSequenceNumber) - + guard (try? PersistedMessageReceived(messageUploadTimestampFromServer: obvMessage.messageUploadTimestampFromServer, downloadTimestampFromServer: obvMessage.downloadTimestampFromServer, localDownloadTimestamp: obvMessage.localDownloadTimestamp, @@ -144,100 +189,53 @@ final class CreatePersistedMessageReceivedFromReceivedObvMessageOperation: Conte } - // Process the attachments within the message + /* The following block of code objective allows to auto-read ephemeral received messges if appropriate. + * We first check whether the current user activity is to be within a discussion. If not, + * we never auto-read. + * If she is within a discussion, we consider all inserted received messages that are ephemeral and + * that require user action to be read. For each of these messages, we check that its discussion + * is identical to the one corresponding to the user activity, and that this discussion configuration + * has its auto-read setting set to `true`. + * Finally, if the message ephemerality is more restrictive than that of the discussion, we do not auto-read. + * In that case, and in that case only, we immediately allow reading of the message. + */ - for obvAttachment in obvMessage.attachments { - do { - try ReceivingMessageAndAttachmentsOperationHelper.processFyleWithinDownloadingAttachment(obvAttachment, - newProgress: nil, - obvEngine: obvEngine, - log: log, - within: obvContext) - } catch { - os_log("Could not process one of the message's attachments: %{public}@", log: log, type: .fault, error.localizedDescription) - // We continue anyway + if let currentUserActivityPersistedDiscussionObjectID = currentUserActivityPersistedDiscussionObjectID, AppStateManager.shared.currentState.isInitializedAndActive { + + let insertedReceivedEphemeralMessagesWithUserAction: [PersistedMessageReceived] = obvContext.context.insertedObjects.compactMap({ + guard let receivedMessage = $0 as? PersistedMessageReceived, + receivedMessage.isEphemeralMessageWithUserAction + else { + return nil + } + return receivedMessage + }) + + insertedReceivedEphemeralMessagesWithUserAction.forEach { insertedReceivedEphemeralMessageWithUserAction in + guard insertedReceivedEphemeralMessageWithUserAction.discussion.typedObjectID == currentUserActivityPersistedDiscussionObjectID, + insertedReceivedEphemeralMessageWithUserAction.discussion.autoRead == true + else { + return + } + // Check that the message ephemerality is at least that of the discussion, otherwise, do not auto read + guard insertedReceivedEphemeralMessageWithUserAction.ephemeralityIsAtLeastAsPermissiveThanDiscussionSharedConfiguration else { + return + } + // If we reach this point, we are receiving a message that is readOnce, within a discussion with an auto-read setting that is the one currently shown to the user. In that case, we auto-read the message. + do { + try insertedReceivedEphemeralMessageWithUserAction.allowReading(now: Date()) + } catch { + os_log("We received a read-once message within a discussion with auto-read that is shown on screen. We should auto-read the message, but this failed: %{public}@", log: log, type: .fault, error.localizedDescription) + assertionFailure() + // We continue anyway + } } } - } else { - - guard PersistedMessageReceived.get(messageIdentifierFromEngine: obvMessage.messageIdentifierFromEngine, from: persistedContactIdentity) == nil else { - return - } - - // We make sure that message has a body (for now, this message comes from the notification extension, and there is no point in creating a `PersistedMessageReceived` if there is no body. - - guard messageJSON.body?.isEmpty == false else { - return - } - - // Create the PersistedMessageReceived - - os_log("Creating a persisted message (overridePreviousPersistedMessage: %{public}@)", log: log, type: .debug, overridePreviousPersistedMessage.description) - let missedMessageCount = updateNextMessageMissedMessageCountAndGetCurrentMissedMessageCount( - discussion: discussion, - contactIdentity: persistedContactIdentity, - senderThreadIdentifier: messageJSON.senderThreadIdentifier, - senderSequenceNumber: messageJSON.senderSequenceNumber) - - guard (try? PersistedMessageReceived(messageUploadTimestampFromServer: obvMessage.messageUploadTimestampFromServer, - downloadTimestampFromServer: obvMessage.downloadTimestampFromServer, - localDownloadTimestamp: obvMessage.localDownloadTimestamp, - messageJSON: messageJSON, - contactIdentity: persistedContactIdentity, - messageIdentifierFromEngine: obvMessage.messageIdentifierFromEngine, - returnReceiptJSON: returnReceiptJSON, - missedMessageCount: missedMessageCount, - discussion: discussion)) != nil - else { - return cancel(withReason: .couldNotCreatePersistedMessageReceived) - } - + } catch { + return cancel(withReason: .coreDataError(error: error)) } - /* The following block of code objective allows to auto-read ephemeral received messges if appropriate. - * We first check whether the current user activity is to be within a discussion. If not, - * we never auto-read. - * If she is within a discussion, we consider all inserted received messages that are ephemeral and - * that require user action to be read. For each of these messages, we check that its discussion - * is identical to the one corresponding to the user activity, and that this discussion configuration - * has its auto-read setting set to `true`. - * Finally, if the message ephemerality is more restrictive than that of the discussion, we do not auto-read. - * In that case, and in that case only, we immediately allow reading of the message. - */ - - if let currentUserActivityPersistedDiscussionObjectID = currentUserActivityPersistedDiscussionObjectID, AppStateManager.shared.currentState.isInitializedAndActive { - - let insertedReceivedEphemeralMessagesWithUserAction: [PersistedMessageReceived] = obvContext.context.insertedObjects.compactMap({ - guard let receivedMessage = $0 as? PersistedMessageReceived, - receivedMessage.isEphemeralMessageWithUserAction - else { - return nil - } - return receivedMessage - }) - - insertedReceivedEphemeralMessagesWithUserAction.forEach { insertedReceivedEphemeralMessageWithUserAction in - guard insertedReceivedEphemeralMessageWithUserAction.discussion.typedObjectID == currentUserActivityPersistedDiscussionObjectID, - insertedReceivedEphemeralMessageWithUserAction.discussion.autoRead == true - else { - return - } - // Check that the message ephemerality is at least that of the discussion, otherwise, do not auto read - guard insertedReceivedEphemeralMessageWithUserAction.ephemeralityIsAtLeastAsPermissiveThanDiscussionSharedConfiguration else { - return - } - // If we reach this point, we are receiving a message that is readOnce, within a discussion with an auto-read setting that is the one currently shown to the user. In that case, we auto-read the message. - do { - try insertedReceivedEphemeralMessageWithUserAction.allowReading(now: Date()) - } catch { - os_log("We received a read-once message within a discussion with auto-read that is shown on screen. We should auto-read the message, but this failed: %{public}@", log: log, type: .fault, error.localizedDescription) - assertionFailure() - // We continue anyway - } - } - } - } } diff --git a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/Operations/Sending messages/CreateUnprocessedPersistedMessageSentFromBodyOperation.swift b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/Operations/Sending messages/CreateUnprocessedPersistedMessageSentFromBodyOperation.swift new file mode 100644 index 00000000..0dbdd42b --- /dev/null +++ b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/Operations/Sending messages/CreateUnprocessedPersistedMessageSentFromBodyOperation.swift @@ -0,0 +1,104 @@ +/* + * Olvid for iOS + * Copyright © 2019-2022 Olvid SAS + * + * This file is part of Olvid for iOS. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + + +import Foundation +import os.log +import CoreData +import OlvidUtils +import ObvCrypto + + +final class CreateUnprocessedPersistedMessageSentFromBodyOperation: ContextualOperationWithSpecificReasonForCancel, UnprocessedPersistedMessageSentProvider { + + private let log = OSLog(subsystem: ObvMessengerConstants.logSubsystem, category: String(describing: CreateUnprocessedReplyToPersistedMessageSentFromBodyOperation.self)) + + let textBody: String + let persistedDiscussionObjectID: NSManagedObjectID + + private(set) var persistedMessageSentObjectID: TypeSafeManagedObjectID? + + init(persistedDiscussionObjectID: NSManagedObjectID, textBody: String) { + self.textBody = textBody + self.persistedDiscussionObjectID = persistedDiscussionObjectID + super.init() + } + + override func main() { + + guard let obvContext = self.obvContext else { + return cancel(withReason: .contextIsNil) + } + + obvContext.performAndWait { + do { + guard let discussion = try PersistedDiscussion.get(objectID: persistedDiscussionObjectID, within: obvContext.context) else { + assertionFailure() + return cancel(withReason: .couldNotFindDiscussionInDatabase) + } + + let persistedMessageSent = try PersistedMessageSent(body: textBody, replyTo: nil, fyleJoins: [], discussion: discussion, readOnce: false, visibilityDuration: nil, existenceDuration: nil) + + do { + try obvContext.context.obtainPermanentIDs(for: [persistedMessageSent]) + } catch { + return cancel(withReason: .couldNotObtainPermanentIDForPersistedMessageSent) + } + + self.persistedMessageSentObjectID = persistedMessageSent.typedObjectID + + } catch(let error) { + assertionFailure() + return cancel(withReason: .coreDataError(error: error)) + } + } + } + +} + +enum CreateUnprocessedPersistedMessageSentFromBodyOperationReasonForCancel: LocalizedErrorWithLogType { + + case contextIsNil + case coreDataError(error: Error) + case couldNotFindDiscussionInDatabase + case couldNotObtainPermanentIDForPersistedMessageSent + + var logType: OSLogType { + switch self { + case .contextIsNil: + return .fault + case .coreDataError: + return .fault + case .couldNotFindDiscussionInDatabase: + return .error + case .couldNotObtainPermanentIDForPersistedMessageSent: + return .error + } + } + + var errorDescription: String? { + switch self { + case .contextIsNil: return "Context is nil" + case .couldNotFindDiscussionInDatabase: return "Could not obtain persisted discussion identity in database" + case .coreDataError(error: let error): return "Core Data error: \(error.localizedDescription)" + case .couldNotObtainPermanentIDForPersistedMessageSent: return "Could not obtain persisted permanent ID for PersistedMessageSent" + } + } + +} diff --git a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/Operations/Sending messages/CreateUnprocessedReplyToPersistedMessageSentFromBodyOperation.swift b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/Operations/Sending messages/CreateUnprocessedReplyToPersistedMessageSentFromBodyOperation.swift new file mode 100644 index 00000000..696ebdc8 --- /dev/null +++ b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/Operations/Sending messages/CreateUnprocessedReplyToPersistedMessageSentFromBodyOperation.swift @@ -0,0 +1,120 @@ +/* + * Olvid for iOS + * Copyright © 2019-2022 Olvid SAS + * + * This file is part of Olvid for iOS. + * + * Olvid is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * Olvid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Olvid. If not, see . + */ + + +import Foundation +import os.log +import CoreData +import OlvidUtils +import ObvCrypto + +final class CreateUnprocessedReplyToPersistedMessageSentFromBodyOperation: ContextualOperationWithSpecificReasonForCancel, UnprocessedPersistedMessageSentProvider { + + private let log = OSLog(subsystem: ObvMessengerConstants.logSubsystem, category: String(describing: CreateUnprocessedReplyToPersistedMessageSentFromBodyOperation.self)) + + let textBody: String + let persistedContactObjectID: NSManagedObjectID + let messageIdentifierFromEngine: Data + + private(set) var persistedMessageSentObjectID: TypeSafeManagedObjectID? + + init(persistedContactObjectID: NSManagedObjectID, messageIdentifierFromEngine: Data, textBody: String) { + self.textBody = textBody + self.persistedContactObjectID = persistedContactObjectID + self.messageIdentifierFromEngine = messageIdentifierFromEngine + super.init() + } + + override func main() { + + guard let obvContext = self.obvContext else { + return cancel(withReason: .contextIsNil) + } + + obvContext.performAndWait { + do { + guard let contactIdentity = try PersistedObvContactIdentity.get(objectID: persistedContactObjectID, within: obvContext.context) else { + assertionFailure() + return cancel(withReason: .couldNotFindContactIdentityInDatabase) + } + + // Find message to reply to + guard let messageToReply = try PersistedMessageReceived.get(messageIdentifierFromEngine: messageIdentifierFromEngine, from: contactIdentity) else { + assertionFailure() + return cancel(withReason: .couldNotFindReceivedMessageInDatabase) + } + + // Create replyTo message to send + let persistedMessageSent = try PersistedMessageSent(body: textBody, replyTo: messageToReply, fyleJoins: [], discussion: messageToReply.discussion, readOnce: false, visibilityDuration: nil, existenceDuration: nil) + + do { + try obvContext.context.obtainPermanentIDs(for: [persistedMessageSent]) + } catch { + return cancel(withReason: .couldNotObtainPermanentIDForPersistedMessageSent) + } + + self.persistedMessageSentObjectID = persistedMessageSent.typedObjectID + + } catch { + assertionFailure() + return cancel(withReason: .coreDataError(error: error)) + } + } + } + +} + +enum CreateUnprocessedReplyToPersistedMessageSentFromBodyOperationReasonForCancel: LocalizedErrorWithLogType { + + case contextIsNil + case coreDataError(error: Error) + case couldNotFindPendingReplyToMessageToSendInDatabase + case couldNotObtainPermanentIDForPersistedMessageSent + case couldNotFindContactIdentityInDatabase + case couldNotFindReceivedMessageInDatabase + + var logType: OSLogType { + switch self { + case .contextIsNil: + return .fault + case .coreDataError: + return .fault + case .couldNotFindReceivedMessageInDatabase: + return .error + case .couldNotFindPendingReplyToMessageToSendInDatabase: + return .error + case .couldNotObtainPermanentIDForPersistedMessageSent: + return .error + case .couldNotFindContactIdentityInDatabase: + return .error + } + } + + var errorDescription: String? { + switch self { + case .contextIsNil: return "Context is nil" + case .couldNotFindContactIdentityInDatabase: return "Could not obtain persisted contact identity in database" + case .couldNotFindPendingReplyToMessageToSendInDatabase: return "Could not find pending replyTo message to send in database" + case .coreDataError(error: let error): return "Core Data error: \(error.localizedDescription)" + case .couldNotObtainPermanentIDForPersistedMessageSent: return "Could not obtain persisted permanent ID for PersistedMessageSent" + case .couldNotFindReceivedMessageInDatabase: return "Could not find received message in database" + } + } + +} diff --git a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/Operations/Sending messages/SendUnprocessedPersistedMessageSentOperation.swift b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/Operations/Sending messages/SendUnprocessedPersistedMessageSentOperation.swift index 0f8dc5e1..d11509e1 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/Operations/Sending messages/SendUnprocessedPersistedMessageSentOperation.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/Operations/Sending messages/SendUnprocessedPersistedMessageSentOperation.swift @@ -34,16 +34,20 @@ protocol ExtendedPayloadProvider: Operation { final class SendUnprocessedPersistedMessageSentOperation: ContextualOperationWithSpecificReasonForCancel { + + private enum Input { + case messageObjectID(_: TypeSafeManagedObjectID) + case provider(_: UnprocessedPersistedMessageSentProvider) + } - private let persistedMessageSentObjectID: TypeSafeManagedObjectID? - private let unprocessedPersistedMessageSentProvider: UnprocessedPersistedMessageSentProvider? + private let input: Input + private let extendedPayloadProvider: ExtendedPayloadProvider? private let obvEngine: ObvEngine private let completionHandler: (() -> Void)? init(persistedMessageSentObjectID: TypeSafeManagedObjectID, extendedPayloadProvider: ExtendedPayloadProvider?, obvEngine: ObvEngine, completionHandler: (() -> Void)? = nil) { - self.persistedMessageSentObjectID = persistedMessageSentObjectID - self.unprocessedPersistedMessageSentProvider = nil + self.input = .messageObjectID(persistedMessageSentObjectID) self.obvEngine = obvEngine self.completionHandler = completionHandler self.extendedPayloadProvider = extendedPayloadProvider @@ -51,8 +55,7 @@ final class SendUnprocessedPersistedMessageSentOperation: ContextualOperationWit } init(unprocessedPersistedMessageSentProvider: UnprocessedPersistedMessageSentProvider, extendedPayloadProvider: ExtendedPayloadProvider?, obvEngine: ObvEngine, completionHandler: (() -> Void)? = nil) { - self.persistedMessageSentObjectID = nil - self.unprocessedPersistedMessageSentProvider = unprocessedPersistedMessageSentProvider + self.input = .provider(unprocessedPersistedMessageSentProvider) self.obvEngine = obvEngine self.completionHandler = completionHandler self.extendedPayloadProvider = extendedPayloadProvider @@ -64,18 +67,16 @@ final class SendUnprocessedPersistedMessageSentOperation: ContextualOperationWit override func main() { let persistedMessageSentObjectID: TypeSafeManagedObjectID - - if let _persistedMessageSentObjectID = self.persistedMessageSentObjectID { + + switch input { + case .messageObjectID(let _persistedMessageSentObjectID): persistedMessageSentObjectID = _persistedMessageSentObjectID - } else if let unprocessedPersistedMessageSentProvider = self.unprocessedPersistedMessageSentProvider { - assert(unprocessedPersistedMessageSentProvider.isFinished) - guard let _persistedMessageSentObjectID = unprocessedPersistedMessageSentProvider.persistedMessageSentObjectID else { + case .provider(let provider): + assert(provider.isFinished) + guard let _persistedMessageSentObjectID = provider.persistedMessageSentObjectID else { return cancel(withReason: .persistedMessageSentObjectIDIsNil) } persistedMessageSentObjectID = _persistedMessageSentObjectID - } else { - // This should never happen since either self.persistedMessageSentObjectID or self.op must be non nil - return cancel(withReason: .cannotDeterminePersistedMessageSentObjectID) } guard let obvContext = self.obvContext else { @@ -186,7 +187,6 @@ final class SendUnprocessedPersistedMessageSentOperation: ContextualOperationWit enum SendUnprocessedPersistedMessageSentOperationReasonForCancel: LocalizedErrorWithLogType { - case cannotDeterminePersistedMessageSentObjectID case contextIsNil case persistedMessageSentObjectIDIsNil case couldNotFindPersistedMessageSentInDatabase @@ -208,16 +208,13 @@ enum SendUnprocessedPersistedMessageSentOperationReasonForCancel: LocalizedError .encodingError, .coreDataError, .contextIsNil, - .persistedMessageSentObjectIDIsNil, - .cannotDeterminePersistedMessageSentObjectID: + .persistedMessageSentObjectIDIsNil: return .fault } } var errorDescription: String? { switch self { - case .cannotDeterminePersistedMessageSentObjectID: - return "Cannot determine PersistedMessageSentObjectID" case .persistedMessageSentObjectIDIsNil: return "persistedMessageSentObjectID is nil" case .contextIsNil: diff --git a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/Operations/UpdateDiscussionLocalConfigurationOperation.swift b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/Operations/UpdateDiscussionLocalConfigurationOperation.swift index 72f36c4c..8e42f206 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/Operations/UpdateDiscussionLocalConfigurationOperation.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/Operations/UpdateDiscussionLocalConfigurationOperation.swift @@ -118,33 +118,3 @@ enum UpdateDiscussionLocalConfigurationOperationReasonForCancel: LocalizedErrorW } - - -extension AppDelegate { - - func scheduleBackgroundTaskForUpdatingBadge() { - ObvStack.shared.performBackgroundTaskAndWait { (context) in - let nextExpirationDate: Date? - do { - nextExpirationDate = try PersistedDiscussionLocalConfiguration.getEarliestMuteExpirationDate(laterThan: Date(), within: context) - } catch { - os_log("🤿 We do not schedule any background task for updating badge since there is no mute expiration left", log: log, type: .info) - return - } - guard let nextExpirationDate = nextExpirationDate else { return} - - os_log("🤿 Submit new update badge operation", log: log, type: .info) - let log = UpdateDiscussionLocalConfigurationOperation.log - do { - try BackgroundTasksManager.shared.submit(task: .updateBadge, earliestBeginDate: nextExpirationDate) - } catch { - guard ObvMessengerConstants.isRunningOnRealDevice else { assertionFailure("We should not be scheduling BG tasks on a simulator as they are unsuported"); return } - os_log("🤿 Could not schedule next expiration: %{public}@", log: log, type: .fault, error.localizedDescription) - assertionFailure() - return - } - } - } - - -} diff --git a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/PersistedDiscussionsUpdatesCoordinator.swift b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/PersistedDiscussionsUpdatesCoordinator.swift index 85c45525..a2cf489d 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/PersistedDiscussionsUpdatesCoordinator.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/PersistedDiscussionsUpdatesCoordinator/PersistedDiscussionsUpdatesCoordinator.swift @@ -144,8 +144,8 @@ final class PersistedDiscussionsUpdatesCoordinator { ObvMessengerCoreDataNotification.observeUserWantsToUpdateDiscussionLocalConfiguration { [weak self] (value, localConfigurationObjectID) in self?.processUserWantsToUpdateDiscussionLocalConfigurationNotification(with: value, localConfigurationObjectID: localConfigurationObjectID) }, - ObvMessengerInternalNotification.observeUserWantsToUpdateLocalConfigurationOfDiscussion { [weak self] (value, persistedDiscussionObjectID) in - self?.processUserWantsToUpdateLocalConfigurationOfDiscussionNotification(with: value, persistedDiscussionObjectID: persistedDiscussionObjectID) + ObvMessengerInternalNotification.observeUserWantsToUpdateLocalConfigurationOfDiscussion { [weak self] (value, persistedDiscussionObjectID, completionHandler) in + self?.processUserWantsToUpdateLocalConfigurationOfDiscussionNotification(with: value, persistedDiscussionObjectID: persistedDiscussionObjectID, completionHandler: completionHandler) }, ObvMessengerCoreDataNotification.observePersistedContactWasDeleted { [weak self ] _, _ in self?.processPersistedContactWasDeletedNotification() @@ -165,6 +165,15 @@ final class PersistedDiscussionsUpdatesCoordinator { ObvMessengerCoreDataNotification.observeAOneToOneDiscussionTitleNeedsToBeReset { [weak self] ownedIdentityObjectID in self?.processAOneToOneDiscussionTitleNeedsToBeReset(ownedIdentityObjectID: ownedIdentityObjectID) }, + ObvMessengerInternalNotification.observeUserRepliedToReceivedMessageWithinTheNotificationExtension { [weak self] persistedContactObjectID, messageIdentifierFromEngine, textBody, completionHandler in + self?.processUserRepliedToReceivedMessageWithinTheNotificationExtensionNotification(persistedContactObjectID: persistedContactObjectID, messageIdentifierFromEngine: messageIdentifierFromEngine, textBody: textBody, completionHandler: completionHandler) + }, + ObvMessengerInternalNotification.observeUserRepliedToMissedCallWithinTheNotificationExtension { [weak self] persistedDiscussionObjectID, textBody, completionHandler in + self?.processUserRepliedToMissedCallWithinTheNotificationExtensionNotification(persistedDiscussionObjectID: persistedDiscussionObjectID, textBody: textBody, completionHandler: completionHandler) + }, + ObvMessengerInternalNotification.observeUserWantsToMarkAsReadMessageWithinTheNotificationExtension { [weak self] persistedContactObjectID, messageIdentifierFromEngine, completionHandler in + self?.processUserWantsToMarkAsReadMessageWithinTheNotificationExtensionNotification(persistedContactObjectID: persistedContactObjectID, messageIdentifierFromEngine: messageIdentifierFromEngine, completionHandler: completionHandler) + }, ]) // Internal VoIP notifications @@ -1398,9 +1407,15 @@ extension PersistedDiscussionsUpdatesCoordinator { composedOp.logReasonIfCancelled(log: log) } - private func processUserWantsToUpdateLocalConfigurationOfDiscussionNotification(with value: PersistedDiscussionLocalConfigurationValue, persistedDiscussionObjectID: TypeSafeManagedObjectID) { + private func processUserWantsToUpdateLocalConfigurationOfDiscussionNotification(with value: PersistedDiscussionLocalConfigurationValue, persistedDiscussionObjectID: TypeSafeManagedObjectID, completionHandler: @escaping (Bool) -> Void) { let op = UpdateDiscussionLocalConfigurationOperation(value: value, persistedDiscussionObjectID: persistedDiscussionObjectID) let composedOp = CompositionOfOneContextualOperation(op1: op, contextCreator: ObvStack.shared, log: log, flowId: FlowIdentifier()) + op.completionBlock = { + DispatchQueue.main.async { + completionHandler(!op.isCancelled) + } + } + internalQueue.addOperations([composedOp], waitUntilFinished: true) composedOp.logReasonIfCancelled(log: log) } @@ -1725,6 +1740,60 @@ extension PersistedDiscussionsUpdatesCoordinator { } } + + private func processUserRepliedToReceivedMessageWithinTheNotificationExtensionNotification(persistedContactObjectID: NSManagedObjectID, messageIdentifierFromEngine: Data, textBody: String, completionHandler: @escaping (Bool) -> Void) { + // This call will add the received message decrypted by the notification extension into the database to be sure that we will be able to reply to this message. + bootstrapMessagesDecryptedWithinNotificationExtension() + + let op1 = CreateUnprocessedReplyToPersistedMessageSentFromBodyOperation(persistedContactObjectID: persistedContactObjectID, messageIdentifierFromEngine: messageIdentifierFromEngine, textBody: textBody) + let op2 = MarkAsReadReceivedMessageOperation(persistedContactObjectID: persistedContactObjectID, messageIdentifierFromEngine: messageIdentifierFromEngine) + let op3 = SendUnprocessedPersistedMessageSentOperation(unprocessedPersistedMessageSentProvider: op1, extendedPayloadProvider: nil, obvEngine: obvEngine) { + DispatchQueue.main.async { completionHandler(true) } + } + let composedOp = CompositionOfThreeContextualOperations(op1: op1, op2: op2, op3: op3, contextCreator: ObvStack.shared, log: log, flowId: FlowIdentifier()) + internalQueue.addOperations([composedOp], waitUntilFinished: true) + composedOp.logReasonIfCancelled(log: log) + + guard [op1, op2, op3].allSatisfy({ !$0.isCancelled }) else { + DispatchQueue.main.async { completionHandler(false) } + return + } + } + + + private func processUserRepliedToMissedCallWithinTheNotificationExtensionNotification(persistedDiscussionObjectID: NSManagedObjectID, textBody: String, completionHandler: @escaping (Bool) -> Void) { + + let op1 = CreateUnprocessedPersistedMessageSentFromBodyOperation(persistedDiscussionObjectID: persistedDiscussionObjectID, textBody: textBody) + let op2 = SendUnprocessedPersistedMessageSentOperation(unprocessedPersistedMessageSentProvider: op1, extendedPayloadProvider: nil, obvEngine: obvEngine) { + DispatchQueue.main.async { completionHandler(true) } + } + let composedOp = CompositionOfTwoContextualOperations(op1: op1, op2: op2, contextCreator: ObvStack.shared, log: log, flowId: FlowIdentifier()) + internalQueue.addOperations([composedOp], waitUntilFinished: true) + composedOp.logReasonIfCancelled(log: log) + + guard !op1.isCancelled else { + DispatchQueue.main.async { completionHandler(false) } + return + } + guard !op2.isCancelled else { + DispatchQueue.main.async { completionHandler(false) } + return + } + } + + + private func processUserWantsToMarkAsReadMessageWithinTheNotificationExtensionNotification(persistedContactObjectID: NSManagedObjectID, messageIdentifierFromEngine: Data, completionHandler: @escaping (Bool) -> Void) { + // This call will add the received message decrypted by the notification extension into the database to be sure that we will be able to mark as read to this message. + bootstrapMessagesDecryptedWithinNotificationExtension() + + let op = MarkAsReadReceivedMessageOperation(persistedContactObjectID: persistedContactObjectID, messageIdentifierFromEngine: messageIdentifierFromEngine) + op.completionBlock = { + DispatchQueue.main.async { completionHandler(!op.isCancelled) } + } + let composedOp = CompositionOfOneContextualOperation(op1: op, contextCreator: ObvStack.shared, log: log, flowId: FlowIdentifier()) + internalQueue.addOperations([composedOp], waitUntilFinished: true) + composedOp.logReasonIfCancelled(log: log) + } } diff --git a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/RetentionMessagesCoordinator.swift b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/RetentionMessagesCoordinator.swift index 9e877625..8fa7cf03 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/RetentionMessagesCoordinator.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/RetentionMessagesCoordinator.swift @@ -33,39 +33,9 @@ final class RetentionMessagesCoordinator { private func observeApplyRetentionPoliciesBackgroundTaskWasLaunchedNotifications() { observationTokens.append(ObvMessengerInternalNotification.observeApplyRetentionPoliciesBackgroundTaskWasLaunched { (completion) in - let completionHandler: (Bool) -> Void = { (success) in - DispatchQueue.main.async { - (UIApplication.shared.delegate as? AppDelegate)?.scheduleBackgroundTaskForApplyingRetentionPolicies() - completion(success) - } - } - ObvMessengerInternalNotification.applyAllRetentionPoliciesNow(launchedByBackgroundTask: true, completionHandler: completionHandler) + ObvMessengerInternalNotification.applyAllRetentionPoliciesNow(launchedByBackgroundTask: true, completionHandler: completion) .postOnDispatchQueue() }) } } - - -// MARK: - Extending AppDelegate for managing the background task allowing to wipe expired messages - -extension AppDelegate { - - /// If there exists at least one message expiration in database, this method schedules a background task allowing to perform a wipe of the associated message in the background. - /// This method is called when the app goes in the background. - func scheduleBackgroundTaskForApplyingRetentionPolicies() { - ObvStack.shared.performBackgroundTaskAndWait { (context) in - // If we reach this point, we should schedule a background task for message expiration - do { - try BackgroundTasksManager.shared.submit(task: .applyRetentionPolicies, earliestBeginDate: Date(timeIntervalSinceNow: TimeInterval(3_600))) - } catch { - guard ObvMessengerConstants.isRunningOnRealDevice else { assertionFailure("We should not be scheduling BG tasks on a simulator as they are unsuported"); return } - os_log("🤿 Could not schedule next BG task for applying retention policies: %{public}@", log: log, type: .fault, error.localizedDescription) - assertionFailure() - return - } - } - } - - -} diff --git a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/ObvUserNotificationIdentifier.swift b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/ObvUserNotificationIdentifier.swift index 8d0c59ea..ec872ea3 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/ObvUserNotificationIdentifier.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/ObvUserNotificationIdentifier.swift @@ -156,9 +156,13 @@ enum ObvUserNotificationIdentifier { return .acceptInviteCategory case .newMessage: return .newMessageCategory + case .newMessageNotificationWithHiddenContent: + return .newMessageWithLimitedVisibilityCategory case .missedCall, .shouldGrantRecordPermissionToReceiveIncomingCalls: return .missedCallCategory - case .sasExchange, .mutualTrustConfirmed, .autoconfirmedContactIntroduction, .increaseMediatorTrustLevelRequired, .newMessageNotificationWithHiddenContent, .staticIdentifier, .newReaction, .newReactionNotificationWithHiddenContent: + case .newReaction, .newReactionNotificationWithHiddenContent: + return .newReactionCategory + case .sasExchange, .mutualTrustConfirmed, .autoconfirmedContactIntroduction, .increaseMediatorTrustLevelRequired, .staticIdentifier: return nil } } diff --git a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationAction.swift b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationAction.swift index deceaf50..14f70501 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationAction.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationAction.swift @@ -25,6 +25,9 @@ enum UserNotificationAction: String { case decline = "DECLINE_ACTION" case mute = "MUTE_ACTION" case callBack = "CALL_BACK_ACTION" + case replyTo = "REPLY_TO_ACTION" + case sendMessage = "SEND_MESSAGE_ACTION" + case markAsRead = "MARK_AS_READ_ACTION" } extension UserNotificationAction { @@ -34,6 +37,9 @@ extension UserNotificationAction { case .decline: return CommonString.Word.Decline case .mute: return CommonString.Word.Mute case .callBack: return CommonString.Title.callBack + case .replyTo: return CommonString.Word.Reply + case .sendMessage: return CommonString.Title.sendMessage + case .markAsRead: return CommonString.Title.markAsRead } } @@ -43,6 +49,9 @@ extension UserNotificationAction { case .decline: return [.authenticationRequired, .destructive] case .mute: return [.authenticationRequired] case .callBack: return [.authenticationRequired, .foreground] + case .replyTo: return [.authenticationRequired] + case .sendMessage: return [.authenticationRequired] + case .markAsRead: return [.authenticationRequired] } } @@ -52,16 +61,50 @@ extension UserNotificationAction { case .decline: return .multiply case .mute: return ObvMessengerConstants.muteIcon case .callBack: return .phoneFill + case .replyTo: return .arrowshapeTurnUpLeft2 + case .sendMessage: return .arrowshapeTurnUpLeft2 + case .markAsRead: return .envelopeOpenFill + } + } + + private var textInput: (buttonTitle: String, placeholder: String)? { + switch self { + case .accept, .decline, .mute, .callBack, .markAsRead: return nil + case .replyTo, .sendMessage: return (CommonString.Word.Send, "Aa") } } var action: UNNotificationAction { + if let (buttonTitle, placeholder) = textInput { + return UNTextInputNotificationAction(identifier: rawValue, title: title, options: options, icon: icon, textInputButtonTitle: buttonTitle, textInputPlaceholder: placeholder) + } else { + return UNNotificationAction(identifier: rawValue, title: title, options: options, icon: icon) + } + } + +} + +extension UNNotificationAction { + + convenience init(identifier: String, title: String, options: UNNotificationActionOptions = [], icon: ObvSystemIcon) { if #available(iOS 15.0, *) { let actionIcon = UNNotificationActionIcon(systemImageName: icon.systemName) - return UNNotificationAction(identifier: rawValue, title: title, options: options, icon: actionIcon) + self.init(identifier: identifier, title: title, options: options, icon: actionIcon) } else { - return UNNotificationAction(identifier: rawValue, title: title, options: options) + self.init(identifier: identifier, title: title, options: options) } } } + +extension UNTextInputNotificationAction { + + convenience init(identifier: String, title: String, options: UNNotificationActionOptions = [], icon: ObvSystemIcon, textInputButtonTitle: String, textInputPlaceholder: String) { + if #available(iOS 15.0, *) { + let actionIcon = UNNotificationActionIcon(systemImageName: icon.systemName) + self.init(identifier: identifier, title: title, options: options, icon: actionIcon, textInputButtonTitle: textInputButtonTitle, textInputPlaceholder: textInputPlaceholder) + } else { + self.init(identifier: identifier, title: title, options: options, textInputButtonTitle: textInputButtonTitle, textInputPlaceholder: textInputPlaceholder) + } + } +} diff --git a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationCategory.swift b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationCategory.swift index 414326fd..3364a6eb 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationCategory.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationCategory.swift @@ -25,37 +25,61 @@ import UserNotifications enum UserNotificationCategory: CaseIterable { case acceptInviteCategory case newMessageCategory + case newMessageWithLimitedVisibilityCategory case missedCallCategory + case newReactionCategory - func getIdentifier() -> String { + var identifier: String { switch self { case .acceptInviteCategory: return "acceptInviteCategory" case .newMessageCategory: return "newMessageCategory" + case .newMessageWithLimitedVisibilityCategory: + return "newMessageWithLimitedVisibilityCategory" case .missedCallCategory: return "missedCallCategory" + case .newReactionCategory: + return "newReactionCategory" } } - + func getCategory() -> UNNotificationCategory { switch self { case .acceptInviteCategory: - return UNNotificationCategory(identifier: self.getIdentifier(), - actions: [UserNotificationAction.accept.action, UserNotificationAction.decline.action], + return UNNotificationCategory(identifier: identifier, + actions: [.accept, .decline], intentIdentifiers: [], options: [.customDismissAction]) case .newMessageCategory: - return UNNotificationCategory(identifier: self.getIdentifier(), - actions: [UserNotificationAction.mute.action], + return UNNotificationCategory(identifier: identifier, + actions: [.mute, .replyTo, .markAsRead], + intentIdentifiers: [], + options: [.customDismissAction]) + case .newMessageWithLimitedVisibilityCategory: + return UNNotificationCategory(identifier: identifier, + actions: [.mute], intentIdentifiers: [], options: [.customDismissAction]) case .missedCallCategory: - return UNNotificationCategory(identifier: self.getIdentifier(), - actions: [UserNotificationAction.callBack.action], + return UNNotificationCategory(identifier: identifier, + actions: [.callBack, .sendMessage], + intentIdentifiers: [], + options: [.customDismissAction]) + case .newReactionCategory: + return UNNotificationCategory(identifier: identifier, + actions: [.mute], intentIdentifiers: [], options: [.customDismissAction]) } } } + +extension UNNotificationCategory { + + convenience init(identifier: String, actions: [UserNotificationAction], intentIdentifiers: [String], options: UNNotificationCategoryOptions = []) { + self.init(identifier: identifier, actions: actions.map({ $0.action }), intentIdentifiers: intentIdentifiers, options: options) + } + +} diff --git a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationCenterDelegate.swift b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationCenterDelegate.swift index cfbbffcd..3bac8180 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationCenterDelegate.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationCenterDelegate.swift @@ -20,6 +20,7 @@ import UIKit import UserNotifications import os.log +import CoreData final class UserNotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate { @@ -44,8 +45,7 @@ final class UserNotificationCenterDelegate: NSObject, UNUserNotificationCenterDe // MARK: - UNUserNotificationCenterDelegate extension UserNotificationCenterDelegate { - - + func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { assert(Thread.isMainThread) @@ -193,10 +193,22 @@ extension UserNotificationCenterDelegate { } guard let action = UserNotificationAction(rawValue: response.actionIdentifier) else { - // This happens, e.g., when tapping a message notification. In that case, the action identifier is UNNotificationDefaultActionIdentifier - assert(response.actionIdentifier == UNNotificationDefaultActionIdentifier) - completionHandler(false) - return + switch response.actionIdentifier { + case UNNotificationDismissActionIdentifier: + // If the user simply dismissed the notification, we consider that the action was handled + completionHandler(true) + return + case UNNotificationDefaultActionIdentifier: + // If the user tapped the notification, it means she wishes to open Olvid and navigate to the discussion. + // We consider that this notification is not handled and complete with `false`. The caller of this method will handle the rest. + completionHandler(false) + return + default: + // This is not expected + assertionFailure() + completionHandler(false) + return + } } let userInfo = response.notification.request.content.userInfo @@ -213,18 +225,9 @@ extension UserNotificationCenterDelegate { handleInvitationActions(action: action, persistedInvitationUuid: persistedInvitationUuid, completionHandler: completionHandler) case .mute: guard let persistedDiscussionObjectURIAsString = userInfo[UserNotificationKeys.persistedDiscussionObjectURI] as? String, - let persistedDiscussionObjectURI = URL(string: persistedDiscussionObjectURIAsString) - else { - assertionFailure() - completionHandler(false) - return - } - guard let objectID = ObvStack.shared.managedObjectID(forURIRepresentation: persistedDiscussionObjectURI) else { - assertionFailure() - completionHandler(false) - return - } - guard let persistedGroupDiscussionEntityName = PersistedGroupDiscussion.entity().name, + let persistedDiscussionObjectURI = URL(string: persistedDiscussionObjectURIAsString), + let objectID = ObvStack.shared.managedObjectID(forURIRepresentation: persistedDiscussionObjectURI), + let persistedGroupDiscussionEntityName = PersistedGroupDiscussion.entity().name, let persistedOneToOneDiscussionEntityName = PersistedOneToOneDiscussion.entity().name, let persistedDiscussionEntityName = PersistedDiscussion.entity().name else { @@ -251,6 +254,39 @@ extension UserNotificationCenterDelegate { return } handleCallBackAction(callUUID: callUUID, completionHandler: completionHandler) + case .replyTo: + guard let messageIdentifierFromEngineAsString = userInfo[UserNotificationKeys.messageIdentifierFromEngine] as? String, + let messageIdentifierFromEngine = Data(hexString: messageIdentifierFromEngineAsString), + let persistedContactObjectURIAsString = userInfo[UserNotificationKeys.persistedContactObjectURI] as? String, + let persistedContactObjectURI = URL(string: persistedContactObjectURIAsString), + let persistedContactObjectID = ObvStack.shared.managedObjectID(forURIRepresentation: persistedContactObjectURI), + let textResponse = response as? UNTextInputNotificationResponse else { + assertionFailure() + completionHandler(false) + return + } + handleReplyToMessageAction(messageIdentifierFromEngine: messageIdentifierFromEngine, persistedContactObjectID: persistedContactObjectID, textBody: textResponse.userText, completionHandler: completionHandler) + case .sendMessage: + guard let persistedDiscussionObjectURIAsString = userInfo[UserNotificationKeys.persistedDiscussionObjectURI] as? String, + let persistedDiscussionObjectURI = URL(string: persistedDiscussionObjectURIAsString), + let persistedDiscussionObjectID = ObvStack.shared.managedObjectID(forURIRepresentation: persistedDiscussionObjectURI), + let textResponse = response as? UNTextInputNotificationResponse else { + assertionFailure() + completionHandler(false) + return + } + handleSendMessageAction(persistedDiscussionObjectID: persistedDiscussionObjectID, textBody: textResponse.userText, completionHandler: completionHandler) + case .markAsRead: + guard let messageIdentifierFromEngineAsString = userInfo[UserNotificationKeys.messageIdentifierFromEngine] as? String, + let messageIdentifierFromEngine = Data(hexString: messageIdentifierFromEngineAsString), + let persistedContactObjectURIAsString = userInfo[UserNotificationKeys.persistedContactObjectURI] as? String, + let persistedContactObjectURI = URL(string: persistedContactObjectURIAsString), + let persistedContactObjectID = ObvStack.shared.managedObjectID(forURIRepresentation: persistedContactObjectURI) else { + assertionFailure() + completionHandler(false) + return + } + handleMarkAsReadAction(messageIdentifierFromEngine: messageIdentifierFromEngine, persistedContactObjectID: persistedContactObjectID, completionHandler: completionHandler) } } @@ -258,9 +294,8 @@ extension UserNotificationCenterDelegate { private func handleMuteActions(persistedDiscussionObjectID: TypeSafeManagedObjectID, completionHandler: @escaping (Bool) -> Void) { ObvMessengerInternalNotification.userWantsToUpdateLocalConfigurationOfDiscussion( value: .muteNotificationsDuration(muteNotificationsDuration: .oneHour), - persistedDiscussionObjectID: persistedDiscussionObjectID - ).postOnDispatchQueue() - completionHandler(true) + persistedDiscussionObjectID: persistedDiscussionObjectID, + completionHandler: completionHandler).postOnDispatchQueue() } @@ -270,9 +305,8 @@ extension UserNotificationCenterDelegate { let contacts = item.logContacts.compactMap { $0.contactIdentity?.typedObjectID } ObvMessengerInternalNotification.userWantsToCallButWeShouldCheckSheIsAllowedTo(contactIDs: contacts, groupId: try? item.getGroupId()).postOnDispatchQueue() } - DispatchQueue.main.async { - completionHandler(true) - } + // The action launch the app in foreground to perform the call, we can terminate the action now + DispatchQueue.main.async { completionHandler(true) } } } @@ -294,84 +328,42 @@ extension UserNotificationCenterDelegate { return } + let acceptInvite: Bool + switch action { + case .accept: + acceptInvite = true + case .decline: + _self.waitUntilApplicationIconBadgeNumberWasUpdatedNotification() + acceptInvite = false + case .mute, .callBack, .replyTo, .sendMessage, .markAsRead: + assertionFailure() + DispatchQueue.main.async { completionHandler(false) } + return + } + guard let obvDialog = persistedInvitation.obvDialog else { assertionFailure(); return } switch obvDialog.category { case .acceptInvite: var localDialog = obvDialog - switch action { - case .accept: - try? localDialog.setResponseToAcceptInvite(acceptInvite: true) - _self.appDelegate.obvEngine.respondTo(localDialog) - DispatchQueue.main.async { completionHandler(true) } - return - case .decline: - _self.waitUntilApplicationIconBadgeNumberWasUpdatedNotification() - try? localDialog.setResponseToAcceptInvite(acceptInvite: false) - _self.appDelegate.obvEngine.respondTo(localDialog) - DispatchQueue.main.async { completionHandler(true) } - return - case .mute, .callBack: - assertionFailure() - DispatchQueue.main.async { completionHandler(false) } - return - } + try? localDialog.setResponseToAcceptInvite(acceptInvite: acceptInvite) + _self.appDelegate.obvEngine.respondTo(localDialog) + DispatchQueue.main.async { completionHandler(true) } case .acceptMediatorInvite: var localDialog = obvDialog - switch action { - case .accept: - try? localDialog.setResponseToAcceptMediatorInvite(acceptInvite: true) - _self.appDelegate.obvEngine.respondTo(localDialog) - DispatchQueue.main.async { completionHandler(true) } - return - case .decline: - _self.waitUntilApplicationIconBadgeNumberWasUpdatedNotification() - try? localDialog.setResponseToAcceptMediatorInvite(acceptInvite: false) - _self.appDelegate.obvEngine.respondTo(localDialog) - DispatchQueue.main.async { completionHandler(true) } - return - case .mute, .callBack: - assertionFailure() - DispatchQueue.main.async { completionHandler(false) } - return - } + try? localDialog.setResponseToAcceptMediatorInvite(acceptInvite: acceptInvite) + _self.appDelegate.obvEngine.respondTo(localDialog) + DispatchQueue.main.async { completionHandler(true) } + return case .acceptGroupInvite: var localDialog = obvDialog - switch action { - case .accept: - try? localDialog.setResponseToAcceptGroupInvite(acceptInvite: true) - _self.appDelegate.obvEngine.respondTo(localDialog) - DispatchQueue.main.async { completionHandler(true) } - return - case .decline: - _self.waitUntilApplicationIconBadgeNumberWasUpdatedNotification() - try? localDialog.setResponseToAcceptGroupInvite(acceptInvite: false) - _self.appDelegate.obvEngine.respondTo(localDialog) - DispatchQueue.main.async { completionHandler(true) } - return - case .mute, .callBack: - assertionFailure() - DispatchQueue.main.async { completionHandler(false) } - return - } + try? localDialog.setResponseToAcceptGroupInvite(acceptInvite: acceptInvite) + _self.appDelegate.obvEngine.respondTo(localDialog) + DispatchQueue.main.async { completionHandler(true) } case .oneToOneInvitationReceived: var localDialog = obvDialog - switch action { - case .accept: - try? localDialog.setResponseToOneToOneInvitationReceived(invitationAccepted: true) - _self.appDelegate.obvEngine.respondTo(localDialog) - DispatchQueue.main.async { completionHandler(true) } - return - case .decline: - _self.waitUntilApplicationIconBadgeNumberWasUpdatedNotification() - try? localDialog.setResponseToOneToOneInvitationReceived(invitationAccepted: false) - _self.appDelegate.obvEngine.respondTo(localDialog) - DispatchQueue.main.async { completionHandler(true) } - return - case .mute, .callBack: - assertionFailure() - DispatchQueue.main.async { completionHandler(false) } - return - } + try? localDialog.setResponseToOneToOneInvitationReceived(invitationAccepted: acceptInvite) + _self.appDelegate.obvEngine.respondTo(localDialog) + DispatchQueue.main.async { completionHandler(true) } default: assertionFailure() DispatchQueue.main.async { completionHandler(false) } @@ -380,7 +372,27 @@ extension UserNotificationCenterDelegate { } } - + + + private func handleReplyToMessageAction(messageIdentifierFromEngine: Data, persistedContactObjectID: NSManagedObjectID, textBody: String, completionHandler: @escaping (Bool) -> Void) { + ObvStack.shared.performBackgroundTask { (context) in + ObvMessengerInternalNotification.userRepliedToReceivedMessageWithinTheNotificationExtension(persistedContactObjectID: persistedContactObjectID, messageIdentifierFromEngine: messageIdentifierFromEngine, textBody: textBody, completionHandler: completionHandler).postOnDispatchQueue() + } + } + + + private func handleSendMessageAction(persistedDiscussionObjectID: NSManagedObjectID, textBody: String, completionHandler: @escaping (Bool) -> Void) { + ObvStack.shared.performBackgroundTask { (context) in + ObvMessengerInternalNotification.userRepliedToMissedCallWithinTheNotificationExtension(persistedDiscussionObjectID: persistedDiscussionObjectID, textBody: textBody, completionHandler: completionHandler).postOnDispatchQueue() + } + } + + private func handleMarkAsReadAction(messageIdentifierFromEngine: Data, persistedContactObjectID: NSManagedObjectID, completionHandler: @escaping (Bool) -> Void) { + ObvStack.shared.performBackgroundTask { (context) in + ObvMessengerInternalNotification.userWantsToMarkAsReadMessageWithinTheNotificationExtension(persistedContactObjectID: persistedContactObjectID, messageIdentifierFromEngine: messageIdentifierFromEngine, completionHandler: completionHandler).postOnDispatchQueue() + } + } + private func waitUntilApplicationIconBadgeNumberWasUpdatedNotification() { diff --git a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationCreator.swift b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationCreator.swift index ffadc7f8..78466769 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationCreator.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationCreator.swift @@ -28,10 +28,12 @@ struct UserNotificationKeys { static let id = "id" static let deepLink = "deepLink" static let persistedDiscussionObjectURI = "persistedDiscussionObjectURI" + static let persistedContactObjectURI = "persistedContactObjectURI" static let reactionTimestamp = "reactionTimestamp" static let callUUID = "callUUID" static let messageIdentifierForNotification = "messageIdentifierForNotification" static let persistedInvitationUUID = "persistedInvitationUUID" + static let messageIdentifierFromEngine = "messageIdentifierFromEngine" static let reactionIdentifierForNotification = "reactionIdentifierForNotification" } @@ -129,6 +131,7 @@ struct UserNotificationCreator { /// This static method creates a new message notification. static func createNewMessageNotification(body: String?, + isEphemeralMessageWithUserAction: Bool, messageIdentifierFromEngine: Data, contact: PersistedObvContactIdentity, attachmentsFileNames: [String], @@ -148,7 +151,11 @@ struct UserNotificationCreator { case .no: - notificationId = ObvUserNotificationIdentifier.newMessage(messageIdentifierFromEngine: messageIdentifierFromEngine) + if isEphemeralMessageWithUserAction { + notificationId = .newMessageNotificationWithHiddenContent + } else { + notificationId = .newMessage(messageIdentifierFromEngine: messageIdentifierFromEngine) + } notificationContent.title = contact.customOrFullDisplayName if discussion is PersistedGroupDiscussion { @@ -175,6 +182,8 @@ struct UserNotificationCreator { notificationContent.userInfo[UserNotificationKeys.deepLink] = deepLink.url.absoluteString notificationContent.userInfo[UserNotificationKeys.persistedDiscussionObjectURI] = discussion.typedObjectID.uriRepresentation().absoluteString notificationContent.userInfo[UserNotificationKeys.messageIdentifierForNotification] = notificationId.getIdentifier() + notificationContent.userInfo[UserNotificationKeys.persistedContactObjectURI] = contact.typedObjectID.uriRepresentation().absoluteString + notificationContent.userInfo[UserNotificationKeys.messageIdentifierFromEngine] = messageIdentifierFromEngine.hexString() if #available(iOS 15.0, *) { incomingMessageIntent = buildSendMessageIntent(notificationContent: notificationContent, contact: contact, discussion: discussion, showGroupName: true, urlForStoringPNGThumbnail: urlForStoringPNGThumbnail) @@ -436,7 +445,7 @@ struct UserNotificationCreator { return createReactionNotification(message: message, contact: contact, emoji: reaction.emoji, reactionTimestamp: reaction.timestamp) } - static func createReactionNotification(message: PersistedMessage, contact: PersistedObvContactIdentity, emoji: String, reactionTimestamp: Date) -> (notificationId: ObvUserNotificationIdentifier, notificationContent: UNNotificationContent) { + static func createReactionNotification(message: PersistedMessage, contact: PersistedObvContactIdentity, emoji: String, reactionTimestamp: Date) -> (notificationId: ObvUserNotificationIdentifier, notificationContent: UNNotificationContent)? { let hideNotificationContent = ObvMessengerSettings.Privacy.hideNotificationContent @@ -449,9 +458,19 @@ struct UserNotificationCreator { switch hideNotificationContent { case .no: - notificationId = .newReaction(messageURI: message.objectID.uriRepresentation(), contactURI: contact.objectID.uriRepresentation()) - notificationContent.body = String.localizedStringWithFormat(NSLocalizedString("MESSAGE_REACTION_NOTIFICATION_%@_%@", comment: ""), emoji, message.textBody ?? "") + if let sentMessage = message as? PersistedMessageSent, + sentMessage.isEphemeralMessageWithLimitedVisibility { + notificationId = .newReactionNotificationWithHiddenContent + notificationContent.body = String.localizedStringWithFormat(NSLocalizedString("MESSAGE_REACTION_NOTIFICATION_%@", comment: ""), emoji) + } else if let textBody = message.textBody { + notificationId = .newReaction(messageURI: message.objectID.uriRepresentation(), contactURI: contact.objectID.uriRepresentation()) + notificationContent.body = String.localizedStringWithFormat(NSLocalizedString("MESSAGE_REACTION_NOTIFICATION_%@_%@", comment: ""), emoji, textBody) + } else { + notificationId = .newReactionNotificationWithHiddenContent + notificationContent.body = String.localizedStringWithFormat(NSLocalizedString("MESSAGE_REACTION_NOTIFICATION_%@", comment: ""), emoji) + } + if #available(iOS 15.0, *) { sendMessageIntent = buildSendMessageIntent(notificationContent: notificationContent, contact: contact, @@ -466,6 +485,7 @@ struct UserNotificationCreator { notificationContent.userInfo[UserNotificationKeys.deepLink] = deepLink.url.absoluteString notificationContent.userInfo[UserNotificationKeys.reactionTimestamp] = reactionTimestamp notificationContent.userInfo[UserNotificationKeys.reactionIdentifierForNotification] = notificationId.getIdentifier() + notificationContent.userInfo[UserNotificationKeys.persistedDiscussionObjectURI] = discussion.objectID.uriRepresentation().absoluteString case .partially: @@ -502,7 +522,7 @@ struct UserNotificationCreator { // We only set a category if the user does not hide the notification content: // Since we use categories to provide interaction within the notification (like accepting or rejectecting an invitation), it would make no sense if the notification does not display any content. if let category = notificationId.getCategory(), hideNotificationContent == .no { - notificationContent.categoryIdentifier = category.getIdentifier() + notificationContent.categoryIdentifier = category.identifier } notificationContent.userInfo[UserNotificationKeys.id] = notificationId.id.rawValue } diff --git a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationsBadgesCoordinator.swift b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationsBadgesCoordinator.swift index 89c6b367..a32c810c 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationsBadgesCoordinator.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationsBadgesCoordinator.swift @@ -123,15 +123,9 @@ extension UserNotificationsBadgesCoordinator { private func observeUpdateBadgeBackgroundTaskWasLaunchedNotifications() { notificationTokens.append(ObvMessengerInternalNotification.observeUpdateBadgeBackgroundTaskWasLaunched() { (completion) in - let completionHandler: (Bool) -> Void = { (success) in - DispatchQueue.main.async { - (UIApplication.shared.delegate as? AppDelegate)?.scheduleBackgroundTaskForUpdatingBadge() - completion(success) - } - } self.recomputeAllBadges() os_log("🤿 Update badge task has been done in background", log: self.log, type: .info) - completionHandler(true) + completion(true) }) } diff --git a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationsCoordinator.swift b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationsCoordinator.swift index 47dce624..849297f0 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationsCoordinator.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/Coordinators/UserNotificationCoordinators/UserNotificationsCoordinator.swift @@ -161,6 +161,7 @@ extension UserNotificationsCoordinator { let discussion = messageReceived.discussion let (notificationId, notificationContent) = UserNotificationCreator.createNewMessageNotification( body: messageReceived.textBody ?? "", + isEphemeralMessageWithUserAction: messageReceived.isEphemeralMessageWithUserAction, messageIdentifierFromEngine: messageReceived.messageIdentifierFromEngine, contact: contactIdentity, attachmentsFileNames: [], @@ -231,6 +232,7 @@ extension UserNotificationsCoordinator { let discussion = newMessage.discussion let (notificationId, notificationContent) = UserNotificationCreator.createNewMessageNotification( body: newMessage.textBody ?? "", + isEphemeralMessageWithUserAction: newMessage.isEphemeralMessageWithUserAction, messageIdentifierFromEngine: newMessage.messageIdentifierFromEngine, contact: contactIdentity, attachmentsFileNames: [], diff --git a/iOSClient/ObvMessenger/ObvMessenger/CoreData/PersistedDiscussion/PersistedDiscussion.swift b/iOSClient/ObvMessenger/ObvMessenger/CoreData/PersistedDiscussion/PersistedDiscussion.swift index 1f1f4b55..24b7f70f 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/CoreData/PersistedDiscussion/PersistedDiscussion.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/CoreData/PersistedDiscussion/PersistedDiscussion.swift @@ -197,12 +197,14 @@ extension PersistedDiscussion { } } + static func insertSystemMessagesIfDiscussionIsEmpty(discussionObjectID: NSManagedObjectID, markAsRead: Bool, within context: NSManagedObjectContext) throws { - guard context.concurrencyType != .mainQueueConcurrencyType else { throw NSError() } - guard let discussion = try PersistedDiscussion.get(objectID: discussionObjectID, within: context) else { throw NSError() } + guard context.concurrencyType != .mainQueueConcurrencyType else { throw Self.makeError(message: "insertSystemMessagesIfDiscussionIsEmpty expects to be on background context") } + guard let discussion = try PersistedDiscussion.get(objectID: discussionObjectID, within: context) else { throw Self.makeError(message: "Could not find discussion") } try discussion.insertSystemMessagesIfDiscussionIsEmpty(markAsRead: markAsRead) } + func getAllActiveParticipants() throws -> (ownCryptoId: ObvCryptoId, contactCryptoIds: Set) { let contactCryptoIds: Set let ownCryptoId: ObvCryptoId diff --git a/iOSClient/ObvMessenger/ObvMessenger/CoreData/PersistedMessage/PendingMessageReaction.swift b/iOSClient/ObvMessenger/ObvMessenger/CoreData/PersistedMessage/PendingMessageReaction.swift index e2453df5..08670464 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/CoreData/PersistedMessage/PendingMessageReaction.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/CoreData/PersistedMessage/PendingMessageReaction.swift @@ -30,6 +30,8 @@ final class PendingMessageReaction: NSManagedObject { private static func makeError(message: String) -> Error { NSError(domain: String(describing: Self.self), code: 0, userInfo: [NSLocalizedFailureReasonErrorKey: message]) } private func makeError(message: String) -> Error { PendingMessageReaction.makeError(message: message) } + // MARK: - Attributes + @NSManaged private(set) var emoji: String? @NSManaged private var senderIdentifier: Data @NSManaged private var senderSequenceNumber: Int diff --git a/iOSClient/ObvMessenger/ObvMessenger/CoreData/PersistedMessage/PersistedMessage.swift b/iOSClient/ObvMessenger/ObvMessenger/CoreData/PersistedMessage/PersistedMessage.swift index e01f14e0..b738bcea 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/CoreData/PersistedMessage/PersistedMessage.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/CoreData/PersistedMessage/PersistedMessage.swift @@ -141,10 +141,10 @@ class PersistedMessage: NSManagedObject { var isWiped: Bool { isLocallyWiped || isRemoteWiped } - /// `true` when this instance can be edited after being sent. + /// In general, a message cannot be edited. Note that we expect `PersistedMessageSent` and `PersistedMessageReceived` to override this variable in return `true` when appropriate. var textBodyCanBeEdited: Bool { false } - /// Shall only be called from the overriding method in `PersistedMessageSent` + @objc func editTextBody(newTextBody: String?) throws { guard self.textBodyCanBeEdited else { throw makeError(message: "The text body of this message cannot be edited now") @@ -152,6 +152,7 @@ class PersistedMessage: NSManagedObject { self.body = newTextBody } + var isEdited: Bool { self.metadata.first(where: { $0.kind == .edited }) != nil } diff --git a/iOSClient/ObvMessenger/ObvMessenger/CoreData/PersistedMessage/PersistedMessageReceived.swift b/iOSClient/ObvMessenger/ObvMessenger/CoreData/PersistedMessage/PersistedMessageReceived.swift index 174b473f..98dfeb25 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/CoreData/PersistedMessage/PersistedMessageReceived.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/CoreData/PersistedMessage/PersistedMessageReceived.swift @@ -66,6 +66,10 @@ final class PersistedMessageReceived: PersistedMessage { @NSManaged private var messageRepliedToIdentifier: PendingRepliedTo? @NSManaged private var unsortedFyleMessageJoinWithStatus: Set + + private var userInfoForDeletion: [String: Any]? + private var changedKeys = Set() + // MARK: - Computed variables override var kind: PersistedMessageKind { .received } @@ -122,9 +126,7 @@ final class PersistedMessageReceived: PersistedMessage { return unsortedFyleMessageJoinWithStatus.sorted(by: { $0.numberFromEngine < $1.numberFromEngine }) } } - - private var userInfoForDeletion: [String: Any]? - + var returnReceipt: ReturnReceiptJSON? { guard let serializedReturnReceipt = self.serializedReturnReceipt else { return nil } do { @@ -135,7 +137,6 @@ final class PersistedMessageReceived: PersistedMessage { } } - private var changedKeys = Set() var isEphemeralMessage: Bool { self.readOnce || self.visibilityDuration != nil || self.initialExistenceDuration != nil @@ -165,6 +166,16 @@ final class PersistedMessageReceived: PersistedMessage { try addMetadata(kind: .edited, date: messageUploadTimestampFromServer) } + + /// `true` when this instance can be edited after being received + override var textBodyCanBeEdited: Bool { + guard self.discussion is PersistedOneToOneDiscussion || self.discussion is PersistedGroupDiscussion else { return false } + guard !self.isLocallyWiped else { return false } + guard !self.isRemoteWiped else { return false } + return true + } + + func updateMissedMessageCount(with missedMessageCount: Int) { self.missedMessageCount = missedMessageCount } @@ -621,6 +632,7 @@ extension PersistedMessageReceived { request.fetchBatchSize = 10 return try context.fetch(request) } + static func get(messageIdentifierFromEngine: Data, from contact: ObvContactIdentity, within context: NSManagedObjectContext) throws -> PersistedMessageReceived? { guard let persistedContact = try? PersistedObvContactIdentity.get(persisted: contact, whereOneToOneStatusIs: .any, within: context) else { return nil } @@ -633,16 +645,17 @@ extension PersistedMessageReceived { } - static func get(messageIdentifierFromEngine: Data, from persistedContact: PersistedObvContactIdentity) -> PersistedMessageReceived? { - guard let context = persistedContact.managedObjectContext else { return nil } + static func get(messageIdentifierFromEngine: Data, from persistedContact: PersistedObvContactIdentity) throws -> PersistedMessageReceived? { + guard let context = persistedContact.managedObjectContext else { throw Self.makeError(message: "PersistedObvContactIdentity's context is nil") } let request: NSFetchRequest = PersistedMessageReceived.fetchRequest() request.predicate = NSPredicate(format: "%K == %@ AND %K == %@", messageIdentifierFromEngineKey, messageIdentifierFromEngine as CVarArg, contactIdentityKey, persistedContact) request.fetchLimit = 1 - do { return try context.fetch(request).first } catch { return nil } + return try context.fetch(request).first } + static func get(senderSequenceNumber: Int, senderThreadIdentifier: UUID, contactIdentity: Data, discussion: PersistedDiscussion) throws -> PersistedMessageReceived? { guard let context = discussion.managedObjectContext else { throw makeError(message: "Could not find context") } let request: NSFetchRequest = PersistedMessageReceived.fetchRequest() @@ -686,10 +699,10 @@ extension PersistedMessageReceived { } - static func getPersistedMessageReceived(with objectID: NSManagedObjectID, within context: NSManagedObjectContext) -> PersistedMessageReceived? { + static func getPersistedMessageReceived(with objectID: TypeSafeManagedObjectID, within context: NSManagedObjectContext) -> PersistedMessageReceived? { let persistedMessageReceived: PersistedMessageReceived do { - guard let res = try context.existingObject(with: objectID) as? PersistedMessageReceived else { throw NSError() } + guard let res = try context.existingObject(with: objectID.objectID) as? PersistedMessageReceived else { throw NSError() } persistedMessageReceived = res } catch { return nil @@ -791,7 +804,7 @@ extension PersistedMessageReceived { static func batchDeletePendingRepliedToEntriesOlderThan(_ date: Date, within context: NSManagedObjectContext) throws { - try PendingRepliedTo.batchDeleteEntriesOlderThan(Date(timeIntervalSinceNow: -TimeInterval(months: 1)), within: context) + try PendingRepliedTo.batchDeleteEntriesOlderThan(date, within: context) } diff --git a/iOSClient/ObvMessenger/ObvMessenger/CoreData/PersistedMessage/PersistedMessageSent.swift b/iOSClient/ObvMessenger/ObvMessenger/CoreData/PersistedMessage/PersistedMessageSent.swift index b8306965..17cd9722 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/CoreData/PersistedMessage/PersistedMessageSent.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/CoreData/PersistedMessage/PersistedMessageSent.swift @@ -167,6 +167,11 @@ final class PersistedMessageSent: PersistedMessage { var isEphemeralMessage: Bool { readOnce || existenceDuration != nil || visibilityDuration != nil } + + var isEphemeralMessageWithLimitedVisibility: Bool { + self.readOnce || self.visibilityDuration != nil + } + /// `true` when this instance can be edited after being sent override var textBodyCanBeEdited: Bool { diff --git a/iOSClient/ObvMessenger/ObvMessenger/Info.plist b/iOSClient/ObvMessenger/ObvMessenger/Info.plist index a31ffb81..1d7dbd3f 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Info.plist +++ b/iOSClient/ObvMessenger/ObvMessenger/Info.plist @@ -4,10 +4,7 @@ BGTaskSchedulerPermittedIdentifiers - io.olvid.clean.expired.messages - io.olvid.apply.retention.policies - io.olvid.update.badge - io.list.messages.on.server + io.olvid.background.tasks CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) diff --git a/iOSClient/ObvMessenger/ObvMessenger/Initialization/AppInitializer.swift b/iOSClient/ObvMessenger/ObvMessenger/Initialization/AppInitializer.swift index a25c3ece..a0519b0b 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Initialization/AppInitializer.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/Initialization/AppInitializer.swift @@ -309,9 +309,9 @@ final class AppInitializer { let log = self.log internalQueue.addOperation { [weak self] in let tag = UUID() - os_log("We are performing a background fetch. We tag it as @{public}@", log: log, type: .info, tag.uuidString) + os_log("We are performing a background fetch. We tag it as %{public}@", log: log, type: .info, tag.uuidString) let completionHandlerForEngine: (UIBackgroundFetchResult) -> Void = { (result) in - os_log("Calling the completion handler of the background fetch tagged as @{public}@. The result is %{public}@", log: log, type: .info, tag.uuidString, result.debugDescription) + os_log("Calling the completion handler of the background fetch tagged as %{public}@. The result is %{public}@", log: log, type: .info, tag.uuidString, result.debugDescription) switch result { case .newData, .noData: success(true) @@ -331,21 +331,3 @@ final class AppInitializer { } } - - - -extension AppDelegate { - - func scheduleBackgroundTaskForListingMessagesOnServer() { - let earliestBeginDate = Date(timeIntervalSinceNow: TimeInterval(minutes: 15)) - do { - try BackgroundTasksManager.shared.submit(task: .listMessagesOnServer, earliestBeginDate: earliestBeginDate) - } catch { - guard ObvMessengerConstants.isRunningOnRealDevice else { assertionFailure("We should not be scheduling BG tasks on a simulator as they are unsuported"); return } - os_log("🤿 Could not schedule next expiration: %{public}@", log: log, type: .fault, error.localizedDescription) - assertionFailure() - return - } - } - -} diff --git a/iOSClient/ObvMessenger/ObvMessenger/Localization/CommonString.swift b/iOSClient/ObvMessenger/ObvMessenger/Localization/CommonString.swift index df1ce4b5..9ce8b194 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Localization/CommonString.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/Localization/CommonString.swift @@ -148,6 +148,8 @@ struct CommonString { static let deleteOwnReaction = NSLocalizedString("DELETE_OWN_REACTION", comment: "Title") static let contactsAndGroups = NSLocalizedString("CONTACTS_AND_GROUPS", comment: "Title") static let contactsSortOrder = NSLocalizedString("CONTACTS_SORT_ORDER", comment: "Title") + static let sendMessage = NSLocalizedString("SEND_MESSAGE", comment: "Title") + static let markAsRead = NSLocalizedString("MARK_AS_READ", comment: "Title") } static let deletedContact = NSLocalizedString("A (now deleted) contact", comment: "Can serve as a name in the sentence %@ accepted to join this group") diff --git a/iOSClient/ObvMessenger/ObvMessenger/Main/Discussions/NewSingleDiscussion/MessageReactionsView.swift b/iOSClient/ObvMessenger/ObvMessenger/Main/Discussions/NewSingleDiscussion/MessageReactionsView.swift index a4b0aead..158241c5 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Main/Discussions/NewSingleDiscussion/MessageReactionsView.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/Main/Discussions/NewSingleDiscussion/MessageReactionsView.swift @@ -55,6 +55,8 @@ final fileprivate class MessageReactionsListViewModel: ObservableObject { private(set) var messageInViewContext: PersistedMessage @Published var changed: Bool // This allows to "force" the refresh of the view private var observationTokens = [NSObjectProtocol]() + @ObservedObject var preferredEmojisList = ObvMessengerPreferredEmojisListObservable() + private let notificationGenerator = UINotificationFeedbackGenerator() fileprivate weak var delegate: MessageReactionsListViewModelDelegate? @@ -98,6 +100,11 @@ final fileprivate class MessageReactionsListViewModel: ObservableObject { } }) } + + func successHaptic() { + notificationGenerator.notificationOccurred(.success) + } + } fileprivate enum MessageReactionSender: Equatable, Hashable { @@ -164,6 +171,14 @@ fileprivate class MessageReaction: Identifiable, Hashable, Comparable { } } + var isOwnedReaction: Bool { + switch sender { + case .contact: + return false + case .owned: + return true + } + } } @@ -174,8 +189,10 @@ struct MessageReactionsListView: View { var body: some View { MessageReactionsListInnerView(reactions: model.reactions, - reactionsAndCount: model.reactionAndCount, - userWantsToDeleteItsReaction: model.userWantsToDeleteItsReaction) + reactionsAndCount: model.reactionAndCount, + preferredEmojiList: model.preferredEmojisList, + userWantsToDeleteItsReaction: model.userWantsToDeleteItsReaction, + successHaptic: model.successHaptic) } } @@ -185,23 +202,58 @@ struct MessageReactionsListInnerView: View { fileprivate let reactions: [MessageReaction] let reactionsAndCount: [ReactionAndCount] let userWantsToDeleteItsReaction: () -> Void + let successHaptic: () -> Void + + @ObservedObject var preferredEmojiList: ObvMessengerPreferredEmojisListObservable - fileprivate init(reactions: [MessageReaction], reactionsAndCount: [ReactionAndCount], userWantsToDeleteItsReaction: @escaping () -> Void) { + fileprivate init(reactions: [MessageReaction], + reactionsAndCount: [ReactionAndCount], + preferredEmojiList: ObvMessengerPreferredEmojisListObservable, + userWantsToDeleteItsReaction: @escaping () -> Void, + successHaptic: @escaping () -> Void) { self.reactions = reactions self.reactionsAndCount = reactionsAndCount + self.preferredEmojiList = preferredEmojiList self.userWantsToDeleteItsReaction = userWantsToDeleteItsReaction + self.successHaptic = successHaptic } var body: some View { - VStack(alignment: .center) { - List { - ForEach(reactions) { reaction in - MessageReactionView(reaction: reaction, - userWantsToDeleteItsReaction: userWantsToDeleteItsReaction) + GeometryReader { geo in + VStack(alignment: .center, spacing: 0) { + List { + Section { + ForEach(0.. Void + let successHaptic: () -> Void + @ObservedObject var preferredEmojiList: ObvMessengerPreferredEmojisListObservable + + var reaction: MessageReaction { + reactions[index] + } var body: some View { HStack { @@ -238,15 +310,36 @@ fileprivate struct MessageReactionView: View { date: reaction.date) } Spacer() - if case .owned = reaction.sender { - Button { + Button { + switch reaction.sender { + case .contact: + if preferredEmojiList.emojis.contains(reaction.emoji) { + withAnimation { + preferredEmojiList.emojis.removeAll { $0 == reaction.emoji } + } + } else { + withAnimation { + preferredEmojiList.emojis.append(reaction.emoji) + } + } + successHaptic() + case .owned: userWantsToDeleteItsReaction() - } label: { + } + } label: { + switch reaction.sender { + case .contact: + if preferredEmojiList.emojis.contains(reaction.emoji) { + Image(systemIcon: .starFill) + } else { + Image(systemIcon: .star) + } + case .owned: Image(systemIcon: .heartSlashFill) } - // Avoid to execute the action when the user tap on every elements of the HStack - .buttonStyle(BorderlessButtonStyle()) } + // Avoid to execute the action when the user tap on every elements of the HStack + .buttonStyle(BorderlessButtonStyle()) Text(reaction.emoji) } } diff --git a/iOSClient/ObvMessenger/ObvMessenger/Main/Discussions/NewSingleDiscussion/cells/CommonCellSubviews/MultipleReactionsView.swift b/iOSClient/ObvMessenger/ObvMessenger/Main/Discussions/NewSingleDiscussion/cells/CommonCellSubviews/MultipleReactionsView.swift index 27c2e953..ae3e8bdb 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Main/Discussions/NewSingleDiscussion/cells/CommonCellSubviews/MultipleReactionsView.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/Main/Discussions/NewSingleDiscussion/cells/CommonCellSubviews/MultipleReactionsView.swift @@ -48,7 +48,7 @@ struct ReactionAndCount: Equatable, Hashable, Comparable, Identifiable { } -final class MultipleReactionsView: ViewForOlvidStack { +final class MultipleReactionsView: UIView { func setReactions(to reactions: [ReactionAndCount], diff --git a/iOSClient/ObvMessenger/ObvMessenger/Main/Discussions/NewSingleDiscussion/cells/ReceivedMessageCell.swift b/iOSClient/ObvMessenger/ObvMessenger/Main/Discussions/NewSingleDiscussion/cells/ReceivedMessageCell.swift index fabe5fa9..d483b5db 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Main/Discussions/NewSingleDiscussion/cells/ReceivedMessageCell.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/Main/Discussions/NewSingleDiscussion/cells/ReceivedMessageCell.swift @@ -1000,9 +1000,9 @@ fileprivate final class ReceivedMessageCellContentView: UIView, UIContentView, U */ if newConfig.readingRequiresUserAction { - multipleReactionsView.showInStack = false + multipleReactionsView.isHidden = true } else { - multipleReactionsView.showInStack = true + multipleReactionsView.isHidden = false if newConfig.reactionAndCounts.isEmpty { multipleReactionsView.setReactions(to: [ReactionAndCount(emoji: "", count: 1)], messageID: messageObjectID?.downcast) multipleReactionsView.alpha = 0.0 diff --git a/iOSClient/ObvMessenger/ObvMessenger/ObvMessengerInternalNotification.swift b/iOSClient/ObvMessenger/ObvMessenger/ObvMessengerInternalNotification.swift index c3bd8ae3..99f73abf 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/ObvMessengerInternalNotification.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/ObvMessengerInternalNotification.swift @@ -108,7 +108,7 @@ enum ObvMessengerInternalNotification { case requestAllHardLinksToFyles(fyleElements: [FyleElement], completionHandler: (([HardLinkToFyle?]) -> Void)) case userWantsToRemoveDraftFyleJoin(draftFyleJoinObjectID: TypeSafeManagedObjectID) case userWantsToChangeContactsSortOrder(ownedCryptoId: ObvCryptoId, sortOrder: ContactsSortOrder) - case userWantsToUpdateLocalConfigurationOfDiscussion(value: PersistedDiscussionLocalConfigurationValue, persistedDiscussionObjectID: TypeSafeManagedObjectID) + case userWantsToUpdateLocalConfigurationOfDiscussion(value: PersistedDiscussionLocalConfigurationValue, persistedDiscussionObjectID: TypeSafeManagedObjectID, completionHandler: (Bool) -> Void) case discussionLocalConfigurationHasBeenUpdated(newValue: PersistedDiscussionLocalConfigurationValue, localConfigurationObjectID: TypeSafeManagedObjectID) case audioInputHasBeenActivated(label: String, activate: () -> Void) case aViewRequiresObvMutualScanUrl(remoteIdentity: Data, ownedCryptoId: ObvCryptoId, completionHandler: ((ObvMutualScanUrl) -> Void)) @@ -141,6 +141,9 @@ enum ObvMessengerInternalNotification { case uiRequiresSignedOwnedDetails(ownedIdentityCryptoId: ObvCryptoId, completion: (SignedUserDetails?) -> Void) case listMessagesOnServerBackgroundTaskWasLaunched(completionHandler: (Bool) -> Void) case userWantsToSendOneToOneInvitationToContact(ownedCryptoId: ObvCryptoId, contactCryptoId: ObvCryptoId) + case userRepliedToReceivedMessageWithinTheNotificationExtension(persistedContactObjectID: NSManagedObjectID, messageIdentifierFromEngine: Data, textBody: String, completionHandler: (Bool) -> Void) + case userRepliedToMissedCallWithinTheNotificationExtension(persistedDiscussionObjectID: NSManagedObjectID, textBody: String, completionHandler: (Bool) -> Void) + case userWantsToMarkAsReadMessageWithinTheNotificationExtension(persistedContactObjectID: NSManagedObjectID, messageIdentifierFromEngine: Data, completionHandler: (Bool) -> Void) private enum Name { case messagesAreNotNewAnymore @@ -250,6 +253,9 @@ enum ObvMessengerInternalNotification { case uiRequiresSignedOwnedDetails case listMessagesOnServerBackgroundTaskWasLaunched case userWantsToSendOneToOneInvitationToContact + case userRepliedToReceivedMessageWithinTheNotificationExtension + case userRepliedToMissedCallWithinTheNotificationExtension + case userWantsToMarkAsReadMessageWithinTheNotificationExtension private var namePrefix: String { String(describing: ObvMessengerInternalNotification.self) } @@ -369,6 +375,9 @@ enum ObvMessengerInternalNotification { case .uiRequiresSignedOwnedDetails: return Name.uiRequiresSignedOwnedDetails.name case .listMessagesOnServerBackgroundTaskWasLaunched: return Name.listMessagesOnServerBackgroundTaskWasLaunched.name case .userWantsToSendOneToOneInvitationToContact: return Name.userWantsToSendOneToOneInvitationToContact.name + case .userRepliedToReceivedMessageWithinTheNotificationExtension: return Name.userRepliedToReceivedMessageWithinTheNotificationExtension.name + case .userRepliedToMissedCallWithinTheNotificationExtension: return Name.userRepliedToMissedCallWithinTheNotificationExtension.name + case .userWantsToMarkAsReadMessageWithinTheNotificationExtension: return Name.userWantsToMarkAsReadMessageWithinTheNotificationExtension.name } } } @@ -698,10 +707,11 @@ enum ObvMessengerInternalNotification { "ownedCryptoId": ownedCryptoId, "sortOrder": sortOrder, ] - case .userWantsToUpdateLocalConfigurationOfDiscussion(value: let value, persistedDiscussionObjectID: let persistedDiscussionObjectID): + case .userWantsToUpdateLocalConfigurationOfDiscussion(value: let value, persistedDiscussionObjectID: let persistedDiscussionObjectID, completionHandler: let completionHandler): info = [ "value": value, "persistedDiscussionObjectID": persistedDiscussionObjectID, + "completionHandler": completionHandler, ] case .discussionLocalConfigurationHasBeenUpdated(newValue: let newValue, localConfigurationObjectID: let localConfigurationObjectID): info = [ @@ -835,6 +845,25 @@ enum ObvMessengerInternalNotification { "ownedCryptoId": ownedCryptoId, "contactCryptoId": contactCryptoId, ] + case .userRepliedToReceivedMessageWithinTheNotificationExtension(persistedContactObjectID: let persistedContactObjectID, messageIdentifierFromEngine: let messageIdentifierFromEngine, textBody: let textBody, completionHandler: let completionHandler): + info = [ + "persistedContactObjectID": persistedContactObjectID, + "messageIdentifierFromEngine": messageIdentifierFromEngine, + "textBody": textBody, + "completionHandler": completionHandler, + ] + case .userRepliedToMissedCallWithinTheNotificationExtension(persistedDiscussionObjectID: let persistedDiscussionObjectID, textBody: let textBody, completionHandler: let completionHandler): + info = [ + "persistedDiscussionObjectID": persistedDiscussionObjectID, + "textBody": textBody, + "completionHandler": completionHandler, + ] + case .userWantsToMarkAsReadMessageWithinTheNotificationExtension(persistedContactObjectID: let persistedContactObjectID, messageIdentifierFromEngine: let messageIdentifierFromEngine, completionHandler: let completionHandler): + info = [ + "persistedContactObjectID": persistedContactObjectID, + "messageIdentifierFromEngine": messageIdentifierFromEngine, + "completionHandler": completionHandler, + ] } return info } @@ -1501,12 +1530,13 @@ enum ObvMessengerInternalNotification { } } - static func observeUserWantsToUpdateLocalConfigurationOfDiscussion(object obj: Any? = nil, queue: OperationQueue? = nil, block: @escaping (PersistedDiscussionLocalConfigurationValue, TypeSafeManagedObjectID) -> Void) -> NSObjectProtocol { + static func observeUserWantsToUpdateLocalConfigurationOfDiscussion(object obj: Any? = nil, queue: OperationQueue? = nil, block: @escaping (PersistedDiscussionLocalConfigurationValue, TypeSafeManagedObjectID, @escaping (Bool) -> Void) -> Void) -> NSObjectProtocol { let name = Name.userWantsToUpdateLocalConfigurationOfDiscussion.name return NotificationCenter.default.addObserver(forName: name, object: obj, queue: queue) { (notification) in let value = notification.userInfo!["value"] as! PersistedDiscussionLocalConfigurationValue let persistedDiscussionObjectID = notification.userInfo!["persistedDiscussionObjectID"] as! TypeSafeManagedObjectID - block(value, persistedDiscussionObjectID) + let completionHandler = notification.userInfo!["completionHandler"] as! (Bool) -> Void + block(value, persistedDiscussionObjectID, completionHandler) } } @@ -1779,4 +1809,35 @@ enum ObvMessengerInternalNotification { } } + static func observeUserRepliedToReceivedMessageWithinTheNotificationExtension(object obj: Any? = nil, queue: OperationQueue? = nil, block: @escaping (NSManagedObjectID, Data, String, @escaping (Bool) -> Void) -> Void) -> NSObjectProtocol { + let name = Name.userRepliedToReceivedMessageWithinTheNotificationExtension.name + return NotificationCenter.default.addObserver(forName: name, object: obj, queue: queue) { (notification) in + let persistedContactObjectID = notification.userInfo!["persistedContactObjectID"] as! NSManagedObjectID + let messageIdentifierFromEngine = notification.userInfo!["messageIdentifierFromEngine"] as! Data + let textBody = notification.userInfo!["textBody"] as! String + let completionHandler = notification.userInfo!["completionHandler"] as! (Bool) -> Void + block(persistedContactObjectID, messageIdentifierFromEngine, textBody, completionHandler) + } + } + + static func observeUserRepliedToMissedCallWithinTheNotificationExtension(object obj: Any? = nil, queue: OperationQueue? = nil, block: @escaping (NSManagedObjectID, String, @escaping (Bool) -> Void) -> Void) -> NSObjectProtocol { + let name = Name.userRepliedToMissedCallWithinTheNotificationExtension.name + return NotificationCenter.default.addObserver(forName: name, object: obj, queue: queue) { (notification) in + let persistedDiscussionObjectID = notification.userInfo!["persistedDiscussionObjectID"] as! NSManagedObjectID + let textBody = notification.userInfo!["textBody"] as! String + let completionHandler = notification.userInfo!["completionHandler"] as! (Bool) -> Void + block(persistedDiscussionObjectID, textBody, completionHandler) + } + } + + static func observeUserWantsToMarkAsReadMessageWithinTheNotificationExtension(object obj: Any? = nil, queue: OperationQueue? = nil, block: @escaping (NSManagedObjectID, Data, @escaping (Bool) -> Void) -> Void) -> NSObjectProtocol { + let name = Name.userWantsToMarkAsReadMessageWithinTheNotificationExtension.name + return NotificationCenter.default.addObserver(forName: name, object: obj, queue: queue) { (notification) in + let persistedContactObjectID = notification.userInfo!["persistedContactObjectID"] as! NSManagedObjectID + let messageIdentifierFromEngine = notification.userInfo!["messageIdentifierFromEngine"] as! Data + let completionHandler = notification.userInfo!["completionHandler"] as! (Bool) -> Void + block(persistedContactObjectID, messageIdentifierFromEngine, completionHandler) + } + } + } diff --git a/iOSClient/ObvMessenger/ObvMessenger/ObvMessengerInternalNotification.yml b/iOSClient/ObvMessenger/ObvMessenger/ObvMessengerInternalNotification.yml index 66023703..aaba0035 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/ObvMessengerInternalNotification.yml +++ b/iOSClient/ObvMessenger/ObvMessenger/ObvMessengerInternalNotification.yml @@ -262,6 +262,7 @@ notifications: params: - {name: value, type: PersistedDiscussionLocalConfigurationValue} - {name: persistedDiscussionObjectID, type: TypeSafeManagedObjectID} + - {name: completionHandler, type: (Bool) -> Void, escaping: true} - name: discussionLocalConfigurationHasBeenUpdated params: - {name: newValue, type: PersistedDiscussionLocalConfigurationValue} @@ -362,3 +363,19 @@ notifications: params: - {name: ownedCryptoId, type: ObvCryptoId} - {name: contactCryptoId, type: ObvCryptoId} +- name: userRepliedToReceivedMessageWithinTheNotificationExtension + params: + - {name: persistedContactObjectID, type: NSManagedObjectID} + - {name: messageIdentifierFromEngine, type: Data} + - {name: textBody, type: String} + - {name: completionHandler, type: (Bool) -> Void, escaping: true} +- name: userRepliedToMissedCallWithinTheNotificationExtension + params: + - {name: persistedDiscussionObjectID, type: NSManagedObjectID} + - {name: textBody, type: String} + - {name: completionHandler, type: (Bool) -> Void, escaping: true} +- name: userWantsToMarkAsReadMessageWithinTheNotificationExtension + params: + - {name: persistedContactObjectID, type: NSManagedObjectID} + - {name: messageIdentifierFromEngine, type: Data} + - {name: completionHandler, type: (Bool) -> Void, escaping: true} diff --git a/iOSClient/ObvMessenger/ObvMessenger/Singletons/BackgroundTasksManager.swift b/iOSClient/ObvMessenger/ObvMessenger/Singletons/BackgroundTasksManager.swift index 6e2060b2..144b5f39 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Singletons/BackgroundTasksManager.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/Singletons/BackgroundTasksManager.swift @@ -20,6 +20,7 @@ import UIKit import BackgroundTasks import os.log +import CoreData final class BackgroundTasksManager { @@ -28,8 +29,10 @@ final class BackgroundTasksManager { private let log = OSLog(subsystem: ObvMessengerConstants.logSubsystem, category: String(describing: BackgroundTasksManager.self)) + static let identifier = "io.olvid.background.tasks" + // Also used in info.plist in "Permitted background task scheduler identifiers" - enum ObvBackgroundTask: String, CaseIterable, CustomStringConvertible { + private enum ObvBackgroundTask: String, CaseIterable, CustomStringConvertible { case cleanExpiredMessages = "io.olvid.clean.expired.messages" case applyRetentionPolicies = "io.olvid.apply.retention.policies" @@ -51,38 +54,116 @@ final class BackgroundTasksManager { } } - } - - - private init() { - let log = self.log - // Register all background tasks - os_log("🤿 Registering all background tasks", log: log, type: .info) - for obvTask in ObvBackgroundTask.allCases { - BGTaskScheduler.shared.register(forTaskWithIdentifier: obvTask.identifier, using: nil) { (backgroundTask) in - ObvDisplayableLogs.shared.log("Background Task '\(obvTask.description)' executes") - switch obvTask { + func executes() async -> Bool { + await withCheckedContinuation { cont in + switch self { case .cleanExpiredMessages: - ObvMessengerInternalNotification.cleanExpiredMessagesBackgroundTaskWasLaunched { [weak self] (success) in - self?.commonCompletion(obvTask: obvTask, backgroundTask: backgroundTask, success: success) + ObvMessengerInternalNotification.cleanExpiredMessagesBackgroundTaskWasLaunched { (success) in + cont.resume(returning: success) }.postOnDispatchQueue() case .applyRetentionPolicies: - ObvMessengerInternalNotification.applyRetentionPoliciesBackgroundTaskWasLaunched { [weak self] (success) in - self?.commonCompletion(obvTask: obvTask, backgroundTask: backgroundTask, success: success) + ObvMessengerInternalNotification.applyRetentionPoliciesBackgroundTaskWasLaunched { (success) in + cont.resume(returning: success) }.postOnDispatchQueue() case .updateBadge: - ObvMessengerInternalNotification.updateBadgeBackgroundTaskWasLaunched { [weak self] (success) in - self?.commonCompletion(obvTask: obvTask, backgroundTask: backgroundTask, success: success) + ObvMessengerInternalNotification.updateBadgeBackgroundTaskWasLaunched { (success) in + cont.resume(returning: success) }.postOnDispatchQueue() case .listMessagesOnServer: - ObvMessengerInternalNotification.listMessagesOnServerBackgroundTaskWasLaunched { [weak self] (success) in - self?.commonCompletion(obvTask: obvTask, backgroundTask: backgroundTask, success: success) + ObvMessengerInternalNotification.listMessagesOnServerBackgroundTaskWasLaunched { (success) in + cont.resume(returning: success) }.postOnDispatchQueue() } } } } + private init() { + let log = self.log + os_log("🤿 Registering background task", log: log, type: .info) + BGTaskScheduler.shared.register(forTaskWithIdentifier: BackgroundTasksManager.identifier, using: nil) { backgroundTask in + ObvDisplayableLogs.shared.log("Background Task executes") + + Task { + _ = await withTaskGroup(of: Bool.self, body: { taskGroup in + for task in ObvBackgroundTask.allCases { + taskGroup.addTask(priority: nil) { + let success = await task.executes() + os_log("🤿 Background Task '%{public}@' did complete. Success is: %{public}@", log: log, type: .info, task.description, success.description) + ObvDisplayableLogs.shared.log("Background Task '\(task.description)' did complete. Success is: \(success.description)") + return success + } + } + let atLeastOneTaskSuccessed = await taskGroup.contains(true) + os_log("🤿 All Background Tasks did complete. Success is: %{public}@", log: log, type: .info, atLeastOneTaskSuccessed.description) + ObvDisplayableLogs.shared.log("All Background Tasks did complete. Success is: \(atLeastOneTaskSuccessed.description)") + backgroundTask.setTaskCompleted(success: atLeastOneTaskSuccessed) + }) + + self.scheduleBackgroundTasks() + } + } + } + + private func earliestBeginDate(for task: ObvBackgroundTask, context: NSManagedObjectContext) -> Date? { + switch task { + case .cleanExpiredMessages: + do { + guard let expiration = try PersistedMessageExpiration.getEarliestExpiration(laterThan: Date(), within: context) else { + os_log("🤿 We do not schedule any background task for message expiration since there is no expiration left", log: log, type: .info) + return nil + } + return expiration.expirationDate + } catch { + os_log("🤿 We could not get earliest message expiration: %{public}@", log: log, type: .fault, error.localizedDescription) + assertionFailure() + return nil + } + case .applyRetentionPolicies: + return Date(timeIntervalSinceNow: TimeInterval(hours: 1)) + case .updateBadge: + do { + guard let expiration = try PersistedDiscussionLocalConfiguration.getEarliestMuteExpirationDate(laterThan: Date(), within: context) else { + os_log("🤿 We do not schedule any background task for mute expiration since there is no expiration left", log: log, type: .info) + return nil + } + return expiration + } catch { + os_log("🤿 We could not get earliest mute expiration: %{public}@", log: log, type: .fault, error.localizedDescription) + assertionFailure() + return nil + } + case .listMessagesOnServer: + return Date(timeIntervalSinceNow: TimeInterval(minutes: 15)) + } + + } + + func scheduleBackgroundTasks() { + // We do not schedule BG tasks when running in the simulator as they are not supported + guard ObvMessengerConstants.isRunningOnRealDevice else { return } + // We make sure the app was initialized. Otherwise, the shared stack is not garanteed to exist. Accessing it would crash the app. + guard AppStateManager.shared.currentState.isInitialized else { return } + ObvStack.shared.performBackgroundTaskAndWait { (context) in + var earliestBeginDate = Date.distantFuture + for task in ObvBackgroundTask.allCases { + if let date = self.earliestBeginDate(for: task, context: context) { + earliestBeginDate = min(date, earliestBeginDate) + } + } + let request = BGAppRefreshTaskRequest(identifier: Self.identifier) + request.earliestBeginDate = earliestBeginDate + ObvDisplayableLogs.shared.log("Submitting background task with earliest begin date \(String(describing: earliestBeginDate.description))") + do { + try BGTaskScheduler.shared.submit(request) + } catch let error { + os_log("🤿 Could not schedule background task: %{public}@", log: log, type: .fault, error.localizedDescription) + } + os_log("🤿 Background tasks was scheduled", log: log, type: .info) + } + + } + private func commonCompletion(obvTask: ObvBackgroundTask, backgroundTask: BGTask, success: Bool) { os_log("🤿 Background Task '%{public}' did complete. Success is: %{public}@", log: log, type: .info, obvTask.description, success.description) @@ -94,16 +175,6 @@ final class BackgroundTasksManager { func cancelAllPendingBGTask() { BGTaskScheduler.shared.cancelAllTaskRequests() } - - - func submit(task: ObvBackgroundTask, earliestBeginDate: Date?) throws { - // ObvDisplayableLogs.shared.log("Submitting background task '\(task.description)' with earliest begin date \(String(describing: earliestBeginDate?.description))") - // We do not schedule BG tasks when running in the simulator as they are not supported - guard ObvMessengerConstants.isRunningOnRealDevice else { return } - let request = BGAppRefreshTaskRequest(identifier: task.identifier) - request.earliestBeginDate = earliestBeginDate - try BGTaskScheduler.shared.submit(request) - } } diff --git a/iOSClient/ObvMessenger/ObvMessenger/Singletons/ObvSystemIcon.swift b/iOSClient/ObvMessenger/ObvMessenger/Singletons/ObvSystemIcon.swift index 2f18955c..44330893 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/Singletons/ObvSystemIcon.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/Singletons/ObvSystemIcon.swift @@ -61,6 +61,7 @@ enum ObvSystemIcon { case earBadgeCheckmark case ellipsisCircle case ellipsisCircleFill + case envelopeOpenFill case exclamationmarkCircle case exclamationmarkShieldFill case eyeFill @@ -132,6 +133,8 @@ enum ObvSystemIcon { case speakerSlashFill case squareAndArrowUp case squareAndPencil + case star + case starFill case textBubbleFill case timer case trash @@ -460,6 +463,10 @@ enum ObvSystemIcon { } else { return "link" } + case .star: + return "star" + case .starFill: + return "star.fill" case .heartSlashFill: return "heart.slash.fill" case .circle: @@ -470,6 +477,8 @@ enum ObvSystemIcon { return "doc.fill" case .rectangleCompressVertical: return "rectangle.compress.vertical" + case .envelopeOpenFill: + return "envelope.open.fill" } } } diff --git a/iOSClient/ObvMessenger/ObvMessenger/UIElements/EmojiPickerView.swift b/iOSClient/ObvMessenger/ObvMessenger/UIElements/EmojiPickerView.swift index 547bb334..824dd512 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/UIElements/EmojiPickerView.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/UIElements/EmojiPickerView.swift @@ -116,7 +116,7 @@ fileprivate struct InnerEmojiView: View { @Binding var showVariantsView: String? @Binding var draggedEmoji: String? - static let defaultFontSize: CGFloat = 35.0 + static let defaultFontSize: CGFloat = 33.0 private var showBackground: Bool { guard let selectedEmoji = selectedEmoji else { return false } @@ -179,26 +179,6 @@ fileprivate struct InnerEmojiView: View { } } -@available(iOS 15.0, *) -fileprivate struct Positions: PreferenceKey { - static var defaultValue: [String: Anchor] = [:] - static func reduce(value: inout [String: Anchor], nextValue: () -> [String: Anchor]) { - value.merge(nextValue(), uniquingKeysWith: { current, _ in - return current }) - } -} - -@available(iOS 15.0, *) -fileprivate struct PositionReader: View { - let tag: String - var body: some View { - Color.clear - .anchorPreference(key: Positions.self, value: .center) { (anchor) in - [tag: anchor] - } - } -} - @available(iOS 15.0, *) fileprivate struct EmojiView: View { @@ -278,8 +258,8 @@ fileprivate struct VariantEmojiPickerView: View { var body: some View { VStack(spacing: 2) { if !showPickers { - LazyHGrid(rows: [EmojiPickerInnerView.gridItem], - spacing: EmojiPickerInnerView.gridSpacing) { + LazyHGrid(rows: [EmojiUtils.gridItem], + spacing: EmojiUtils.gridSpacing) { ForEach(emojisToShow, id: \.self) { emoji in InnerEmojiView(emoji: emoji, selectAction: selectAction, @@ -294,7 +274,7 @@ fileprivate struct VariantEmojiPickerView: View { } } } else { - HStack(spacing: EmojiPickerInnerView.gridSpacing) { + HStack(spacing: EmojiUtils.gridSpacing) { ForEach(emojisToShow, id: \.self) { emoji in InnerEmojiView(emoji: emoji, selectAction: selectAction, @@ -338,9 +318,9 @@ fileprivate struct VariantEmojiPickerView: View { } } } - .frame(width: showPickers ? Self.widthColWithPicker * EmojiPickerInnerView.gridColumnSize : - CGFloat(emojis.count + 1) * EmojiPickerInnerView.gridColumnSize, - height: showPickers ? Self.heightColWithPicker * EmojiPickerInnerView.gridColumnSize : EmojiPickerInnerView.gridColumnSize) + .frame(width: showPickers ? Self.widthColWithPicker * EmojiUtils.gridColumnSize : + CGFloat(emojis.count + 1) * EmojiUtils.gridColumnSize, + height: showPickers ? Self.heightColWithPicker * EmojiUtils.gridColumnSize : EmojiUtils.gridColumnSize) .padding(2) .background { RoundedRectangle(cornerRadius: 16.0) @@ -372,20 +352,249 @@ extension EmojiGroup { } } +@available(iOS 15.0, *) +struct EmojiUtils { + + static let gridItemSize: CGFloat = 36 + static let gridSpacing: CGFloat = 5 + static let gridColumnSize: CGFloat = gridItemSize + gridSpacing + + typealias ScrollCorrectionType = String? + + static let gridItem: GridItem = .init(.fixed(gridItemSize), spacing: gridSpacing) + + static func computeRowsCount(geometry: GeometryProxy) -> Int { + let height = geometry.size.height + return Int(height / Self.gridColumnSize) + } + + fileprivate static var allEmojis: [Emoji] { + EmojiList.allEmojis.map { Emoji(defaultEmoji: $0) } + } + + static var none: String { " " } + static func isNone(_ s: String) -> Bool { s == none } + + static func scrollTo(geometry: GeometryProxy, + scrollViewProxy: ScrollViewProxy, + emoji: String, + detector: CurrentValueSubject) { + guard geometry.size.width > 0 else { return } + /// Stop current scrolling correction + detector.send(nil) + /// Computes the number of columns + let cols = (geometry.size.width / Self.gridColumnSize).rounded(.down) + /// Compute the left and right padding needs to be centered + let padding = (geometry.size.width - cols * Self.gridColumnSize) / 2 + /// UnitPoint takes a ratio betwen the parent and child position + let x = padding / geometry.size.width + withAnimation { + scrollViewProxy.scrollTo(emoji, anchor: UnitPoint(x: x, y: 1)) + } + } + + static func scrollTo(geometry: GeometryProxy, + scrollViewProxy: ScrollViewProxy, + emojis: [String], + emoji: String, + rowsCount: Int, + detector: CurrentValueSubject) { + guard rowsCount > 0 else { return } + guard geometry.size.width > 0 else { return } + + /// Stop current scrolling correction + detector.send(nil) + /// Computes the number of columns + let cols = (geometry.size.width / Self.gridColumnSize).rounded(.down) + let padding = (geometry.size.width - cols * Self.gridColumnSize) / 2 + + guard let emojiIndex = emojis.firstIndex(of: emoji) else { return } + + let emojiCol = CGFloat(emojiIndex) / CGFloat(rowsCount) + let colsCount = (CGFloat(emojis.count) / CGFloat(rowsCount)).rounded(.up) + + let trailingCols = colsCount - emojiCol + var emojiToScroll = emoji + if CGFloat(trailingCols) < cols { + let delta = cols - CGFloat(trailingCols) + let emojiToScrollIndex = emojiIndex - Int(delta) * rowsCount + guard 0 <= emojiToScrollIndex && emojiToScrollIndex < emojis.count else { return } + emojiToScroll = emojis[emojiToScrollIndex] + } + + /// Compute the left and right padding needs to be centered + /// UnitPoint takes a ratio betwen the parent and child position + let x = padding / geometry.size.width + withAnimation { + scrollViewProxy.scrollTo(emojiToScroll, anchor: UnitPoint(x: x, y: 1)) + } + } +} + +@available(iOS 15.0, *) +enum PreferredEmojiPickerInnerViewInput { + case normal(selectedEmoji: Binding, draggedEmoji: Binding, selectAction: () -> Void, haptic: () -> Void) + case viewer(emojiToShow: String?, addAdditionalSpace: Bool) + + var selectedEmoji: Binding { + switch self { + case .normal(let selectedEmoji, _, _, _): + return selectedEmoji + case .viewer: + return .constant(nil) + } + } + + var draggedEmoji: Binding { + switch self { + case .normal(_, let draggedEmoji, _, _): + return draggedEmoji + case .viewer: + return .constant(nil) + } + } + + var selectAction: () -> Void { + switch self { + case .normal(_, _, let selectAction, _): + return selectAction + case .viewer: + return { } + } + } + + var haptic: () -> Void { + switch self { + case .normal(_, _, _, let haptic): + return haptic + case .viewer: + return { } + } + } + + var addAdditionalSpace: Bool { + switch self { + case .normal: + return true + case .viewer(_, let addAdditionalSpace): + return addAdditionalSpace + } + } +} + +@available(iOS 15.0, *) +struct PreferredEmojiPickerInnerView: View { + + let input: PreferredEmojiPickerInnerViewInput + + @ObservedObject var preferredEmojiList: ObvMessengerPreferredEmojisListObservable + private let scrollCorrectionDetector: CurrentValueSubject + private let scrollCorrectionPublisher: AnyPublisher + + + init(input: PreferredEmojiPickerInnerViewInput, + preferredEmojiList: ObvMessengerPreferredEmojisListObservable) { + self.input = input + self.preferredEmojiList = preferredEmojiList + + do { + let detector = CurrentValueSubject(nil) + self.scrollCorrectionPublisher = detector + .debounce(for: .seconds(0.2), scheduler: DispatchQueue.main) + .dropFirst() + .eraseToAnyPublisher() + self.scrollCorrectionDetector = detector + } + } + + var body: some View { + GeometryReader { geometry in + HStack { + ScrollView(.horizontal, showsIndicators: true) { + HStack { + ScrollViewReader { scrollViewProxy in + LazyHGrid(rows: [EmojiUtils.gridItem], + spacing: EmojiUtils.gridSpacing) { + ReorderableForEach(items: $preferredEmojiList.emojis, + draggedItem: input.draggedEmoji, + haptic: input.haptic, + none: input.addAdditionalSpace ? EmojiUtils.none : nil) { emoji in + InnerEmojiView(emoji: emoji, + selectAction: input.selectAction, + hasVariants: false, + isPreferredView: true, + haptic: input.haptic, + isNone: EmojiUtils.isNone, + selectedEmoji: input.selectedEmoji, + showVariantsView: .init(get: { nil }, set: { _ in }), + draggedEmoji: input.draggedEmoji) + .background(PositionReader(tag: emoji)) + .id(emoji) + } + } + .padding() + .onPreferenceChange(Positions.self) { positions in + /// Send the emoji at the top left + var values = positions.map { ($0.key, geometry[$0.value]) } + .filter { $0.1.x >= 0 } + values.sort(by: { l, r in l.1.x <= r.1.x }) + if let (emoji, _) = values.first { + scrollCorrectionDetector.send(emoji) + } + } + .onReceive(scrollCorrectionPublisher) { + guard let emoji = $0 else { return } + /// Scroll to the top left emoji with corrected position + EmojiUtils.scrollTo(geometry: geometry, scrollViewProxy: scrollViewProxy, emoji: emoji, detector: scrollCorrectionDetector) + } + .onAppear { + switch input { + case .normal(selectedEmoji: let selectedEmoji, _, _, _): + if let selectedEmoji = selectedEmoji.wrappedValue, + preferredEmojiList.emojis.contains(selectedEmoji) { + EmojiUtils.scrollTo(geometry: geometry, + scrollViewProxy: scrollViewProxy, + emojis: preferredEmojiList.emojis, + emoji: selectedEmoji, + rowsCount: 1, + detector: scrollCorrectionDetector) + } else if let first = preferredEmojiList.emojis.first { + EmojiUtils.scrollTo(geometry: geometry, + scrollViewProxy: scrollViewProxy, + emoji: first, + detector: scrollCorrectionDetector) + } + case .viewer(emojiToShow: let emojiToShow, _): + if let emojiToShow = emojiToShow { + scrollViewProxy.scrollTo(emojiToShow) + } + } + } + } + if preferredEmojiList.emojis.isEmpty { + Text("DRAG_AND_DROP_TO_CONFIGURE_PREFERRED_EMOJIS_LIST") + .font(Font.system(.callout, design: .rounded).weight(.bold)) + } + } + } + } + } + .frame(height: EmojiUtils.gridColumnSize * 2) + } +} + @available(iOS 15.0, *) struct EmojiPickerInnerView: View { @Binding var selectedEmoji: String? let selectAction: () -> Void let haptic: () -> Void + @ObservedObject var preferredEmojiList: ObvMessengerPreferredEmojisListObservable // The emoji at the top left - typealias ScrollCorrectionType = String? - let allEmojisScrollCorrectionDetector: CurrentValueSubject - let allEmojisScrollCorrectionPublisher: AnyPublisher - let preferredScrollCorrectionDetector: CurrentValueSubject - let preferredScrollCorrectionPublisher: AnyPublisher + private let scrollCorrectionDetector: CurrentValueSubject + private let scrollCorrectionPublisher: AnyPublisher init(selectedEmoji: Binding, selectAction: @escaping () -> Void, @@ -397,22 +606,14 @@ struct EmojiPickerInnerView: View { self.preferredEmojiList = preferredEmojiList do { - let allEmojisDetector = CurrentValueSubject(nil) - self.allEmojisScrollCorrectionPublisher = allEmojisDetector + let detector = CurrentValueSubject(nil) + self.scrollCorrectionPublisher = detector .debounce(for: .seconds(0.2), scheduler: DispatchQueue.main) .dropFirst() .eraseToAnyPublisher() - self.allEmojisScrollCorrectionDetector = allEmojisDetector + self.scrollCorrectionDetector = detector } - do { - let preferredDetector = CurrentValueSubject(nil) - self.preferredScrollCorrectionPublisher = preferredDetector - .debounce(for: .seconds(0.2), scheduler: DispatchQueue.main) - .dropFirst() - .eraseToAnyPublisher() - self.preferredScrollCorrectionDetector = preferredDetector - } } @State private var selectedGroup: EmojiGroup = EmojiGroup.allCases.first! @@ -432,99 +633,24 @@ struct EmojiPickerInnerView: View { @State private var draggedEmoji: String? = nil @State private var rowsCount: Int? = nil - fileprivate static let gridItemSize: CGFloat = 35 - fileprivate static let gridSpacing: CGFloat = 5 - fileprivate static let gridColumnSize: CGFloat = gridItemSize + gridSpacing - - fileprivate static let gridItem: GridItem = .init(.fixed(Self.gridItemSize), spacing: Self.gridSpacing) - - func computeRowsCount(geometry: GeometryProxy) -> Int { - let height = geometry.size.height - return Int(height / Self.gridColumnSize) - } - - private var allEmojis: [Emoji] { - EmojiList.allEmojis.map { Emoji(defaultEmoji: $0) } - } - - private var none: String { " " } - private func isNone(_ s: String) -> Bool { s == none } - var body: some View { VStack(alignment: .center) { - GeometryReader { geometry in - HStack { - ScrollView(.horizontal, showsIndicators: true) { - HStack { - ScrollViewReader { scrollViewProxy in - LazyHGrid(rows: [Self.gridItem], - spacing: Self.gridSpacing) { - ReorderableForEach(items: $preferredEmojiList.emojis, - draggedItem: $draggedEmoji, - haptic: haptic, - none: none) { emoji in - InnerEmojiView(emoji: emoji, - selectAction: selectAction, - hasVariants: false, - isPreferredView: true, - haptic: haptic, - isNone: isNone, - selectedEmoji: $selectedEmoji, - showVariantsView: .init(get: { nil }, set: { _ in }), - draggedEmoji: $draggedEmoji) - .background(PositionReader(tag: emoji)) - .id(emoji) - } - } - .padding() - .onPreferenceChange(Positions.self) { positions in - /// Send the emoji at the top left - var values = positions.map { ($0.key, geometry[$0.value]) } - .filter { $0.1.x >= 0 } - values.sort(by: { l, r in l.1.x <= r.1.x }) - if let (emoji, _) = values.first { - preferredScrollCorrectionDetector.send(emoji) - } - } - .onReceive(preferredScrollCorrectionPublisher) { - guard let emoji = $0 else { return } - /// Scroll to the top left emoji with corrected position - scrollTo(geometry: geometry, scrollViewProxy: scrollViewProxy, emoji: emoji, detector: preferredScrollCorrectionDetector) - } - .onAppear { - if let selectedEmoji = selectedEmoji, - preferredEmojiList.emojis.contains(selectedEmoji) { - scrollTo(geometry: geometry, - scrollViewProxy: scrollViewProxy, - emojis: preferredEmojiList.emojis, - emoji: selectedEmoji, - rowsCount: 1, - detector: preferredScrollCorrectionDetector) - } else if let first = preferredEmojiList.emojis.first { - scrollTo(geometry: geometry, scrollViewProxy: scrollViewProxy, emoji: first, detector: preferredScrollCorrectionDetector) - } - } - } - if preferredEmojiList.emojis.isEmpty { - Text("DRAP_AND_DROP_TO_CONFIGURE_PREFERRED_EMOJIS_LIST") - .font(Font.system(.callout, design: .rounded).weight(.bold)) - } - } - } - } - } - .frame(height: Self.gridColumnSize * 2) + PreferredEmojiPickerInnerView(input: .normal(selectedEmoji: $selectedEmoji, + draggedEmoji: $draggedEmoji, + selectAction: selectAction, + haptic: haptic), + preferredEmojiList: preferredEmojiList) Divider() GeometryReader { geometry in ScrollView(.horizontal, showsIndicators: true) { ScrollViewReader { scrollViewProxy in - LazyHGrid(rows: [GridItem](repeating: Self.gridItem, - count: rowsCount ?? computeRowsCount(geometry: geometry)), - spacing: Self.gridSpacing) { - ForEach(allEmojis) { emoji in + LazyHGrid(rows: [GridItem](repeating: EmojiUtils.gridItem, + count: rowsCount ?? EmojiUtils.computeRowsCount(geometry: geometry)), + spacing: EmojiUtils.gridSpacing) { + ForEach(EmojiUtils.allEmojis) { emoji in EmojiView(emoji: emoji, selectAction: selectAction, haptic: haptic, - isNone: isNone, + isNone: EmojiUtils.isNone, selectedEmoji: $selectedEmoji, showVariantsView: $showVariantsView, draggedEmoji: $draggedEmoji) @@ -534,7 +660,7 @@ struct EmojiPickerInnerView: View { .padding() .onChange(of: scrollToGroup) { group in guard let group = group else { return } - scrollTo(geometry: geometry, scrollViewProxy: scrollViewProxy, emoji: group.firstEmoji, detector: allEmojisScrollCorrectionDetector) + EmojiUtils.scrollTo(geometry: geometry, scrollViewProxy: scrollViewProxy, emoji: group.firstEmoji, detector: scrollCorrectionDetector) scrollToGroup = nil } .onPreferenceChange(Positions.self) { positions in @@ -543,7 +669,7 @@ struct EmojiPickerInnerView: View { .filter { $0.1.x >= 0 } values.sort(by: { l, r in l.1.x <= r.1.x && l.1.y <= r.1.y }) if let (emoji, _) = values.first { - allEmojisScrollCorrectionDetector.send(emoji) + scrollCorrectionDetector.send(emoji) } if let (emoji, _) = values.last { if let position = EmojiList.allEmojis.firstIndex(of: emoji), @@ -555,16 +681,16 @@ struct EmojiPickerInnerView: View { self.draggedEmoji = nil } if self.rowsCount == nil { - let rowCount = computeRowsCount(geometry: geometry) + let rowCount = EmojiUtils.computeRowsCount(geometry: geometry) if rowCount > 0 { self.rowsCount = rowCount } } } - .onReceive(allEmojisScrollCorrectionPublisher) { + .onReceive(scrollCorrectionPublisher) { guard let emoji = $0 else { return } /// Scroll to the top left emoji with corrected position - scrollTo(geometry: geometry, scrollViewProxy: scrollViewProxy, emoji: emoji, detector: allEmojisScrollCorrectionDetector) + EmojiUtils.scrollTo(geometry: geometry, scrollViewProxy: scrollViewProxy, emoji: emoji, detector: scrollCorrectionDetector) } .onAppear { /// Set the current position of the scroll and set the current group @@ -588,17 +714,17 @@ struct EmojiPickerInnerView: View { selectedGroup = group } if let representative = representative { - scrollTo(geometry: geometry, + EmojiUtils.scrollTo(geometry: geometry, scrollViewProxy: scrollViewProxy, - emojis: allEmojis.map({$0.defaultEmoji}), + emojis: EmojiUtils.allEmojis.map({$0.defaultEmoji}), emoji: representative, - rowsCount: rowsCount ?? computeRowsCount(geometry: geometry), - detector: allEmojisScrollCorrectionDetector) + rowsCount: rowsCount ?? EmojiUtils.computeRowsCount(geometry: geometry), + detector: scrollCorrectionDetector) } } else if let firstGroup = EmojiGroup.allCases.first { selectedGroup = firstGroup if let first = EmojiList.allEmojis.first { - scrollTo(geometry: geometry, scrollViewProxy: scrollViewProxy, emoji: first, detector: allEmojisScrollCorrectionDetector) + EmojiUtils.scrollTo(geometry: geometry, scrollViewProxy: scrollViewProxy, emoji: first, detector: scrollCorrectionDetector) } } } @@ -606,7 +732,7 @@ struct EmojiPickerInnerView: View { } .onAppear { guard self.rowsCount == nil else { return } - let rowCount = computeRowsCount(geometry: geometry) + let rowCount = EmojiUtils.computeRowsCount(geometry: geometry) if rowCount > 0 { self.rowsCount = rowCount } @@ -623,14 +749,14 @@ struct EmojiPickerInnerView: View { ZStack { /// Show a comics like arrow above the selected emojis Rectangle() - .frame(width: Self.gridColumnSize / 2, height: Self.gridColumnSize / 2) + .frame(width: EmojiUtils.gridColumnSize / 2, height: EmojiUtils.gridColumnSize / 2) .foregroundColor(.gray) .rotationEffect(.degrees(45)) - .position(x: arrowPosition.x, y: arrowPosition.y + Self.gridColumnSize / 3) + .position(x: arrowPosition.x, y: arrowPosition.y + EmojiUtils.gridColumnSize / 3) VariantEmojiPickerView(emojis: emojisToShow, selectAction: selectAction, haptic: haptic, - isNone: isNone, + isNone: EmojiUtils.isNone, selectedEmoji: $selectedEmoji, draggedEmoji: $draggedEmoji) .position(variantsViewPosition) @@ -664,61 +790,6 @@ struct EmojiPickerInnerView: View { .background(.thinMaterial) } - fileprivate func scrollTo(geometry: GeometryProxy, - scrollViewProxy: ScrollViewProxy, - emoji: String, - detector: CurrentValueSubject) { - guard geometry.size.width > 0 else { return } - /// Stop current scrolling correction - detector.send(nil) - /// Computes the number of columns - let cols = (geometry.size.width / Self.gridColumnSize).rounded(.down) - /// Compute the left and right padding needs to be centered - let padding = (geometry.size.width - cols * Self.gridColumnSize) / 2 - /// UnitPoint takes a ratio betwen the parent and child position - let x = padding / geometry.size.width - withAnimation { - scrollViewProxy.scrollTo(emoji, anchor: UnitPoint(x: x, y: 1)) - } - } - - fileprivate func scrollTo(geometry: GeometryProxy, - scrollViewProxy: ScrollViewProxy, - emojis: [String], - emoji: String, - rowsCount: Int, - detector: CurrentValueSubject) { - guard rowsCount > 0 else { return } - guard geometry.size.width > 0 else { return } - - /// Stop current scrolling correction - detector.send(nil) - /// Computes the number of columns - let cols = (geometry.size.width / Self.gridColumnSize).rounded(.down) - let padding = (geometry.size.width - cols * Self.gridColumnSize) / 2 - - guard let emojiIndex = emojis.firstIndex(of: emoji) else { return } - - let emojiCol = CGFloat(emojiIndex) / CGFloat(rowsCount) - let colsCount = (CGFloat(emojis.count) / CGFloat(rowsCount)).rounded(.up) - - let trailingCols = colsCount - emojiCol - var emojiToScroll = emoji - if CGFloat(trailingCols) < cols { - let delta = cols - CGFloat(trailingCols) - let emojiToScrollIndex = emojiIndex - Int(delta) * rowsCount - guard 0 <= emojiToScrollIndex && emojiToScrollIndex < emojis.count else { return } - emojiToScroll = emojis[emojiToScrollIndex] - } - - /// Compute the left and right padding needs to be centered - /// UnitPoint takes a ratio betwen the parent and child position - let x = padding / geometry.size.width - withAnimation { - scrollViewProxy.scrollTo(emojiToScroll, anchor: UnitPoint(x: x, y: 1)) - } - } - /// Returns the a pair of position of the given emoji, the first position of the variants view /// The code takes care that the bouds of the variants view are inside the view regardless of the position of the selected variant. fileprivate func getPosition(geometry: GeometryProxy, emoji: String, emojisCount: Int, positions: [String: Anchor]) -> (CGPoint, CGPoint)? { @@ -730,19 +801,19 @@ struct EmojiPickerInnerView: View { /// Compute the current trailing and leading space between the bound of the view and the frame let cols = viewWithPicker ? 4 : emojisCount let trailingCols: Int = cols / 2 - let trailing = frame.maxX - point.x - Self.gridColumnSize * CGFloat(trailingCols) + let trailing = frame.maxX - point.x - EmojiUtils.gridColumnSize * CGFloat(trailingCols) let leadingCol = cols - trailingCols - let leading = point.x - Self.gridColumnSize * CGFloat(leadingCol) - frame.minX + let leading = point.x - EmojiUtils.gridColumnSize * CGFloat(leadingCol) - frame.minX /// Look is some space is negative, we should shift to view the show it entirely var indexDelta = 0 if trailing < 0 { - indexDelta = Int((trailing / Self.gridColumnSize).rounded(.up)) + indexDelta = Int((trailing / EmojiUtils.gridColumnSize).rounded(.up)) } else if leading < 0 { - indexDelta = Int((-leading / Self.gridColumnSize).rounded(.up)) + indexDelta = Int((-leading / EmojiUtils.gridColumnSize).rounded(.up)) } - let xCorrection = (CGFloat(indexDelta) - 0.5) * Self.gridColumnSize - let yCorrection = -(viewWithPicker ? 0.5 + VariantEmojiPickerView.heightColWithPicker / 2 : 1.0) * Self.gridColumnSize + let xCorrection = (CGFloat(indexDelta) - 0.5) * EmojiUtils.gridColumnSize + let yCorrection = -(viewWithPicker ? 0.5 + VariantEmojiPickerView.heightColWithPicker / 2 : 1.0) * EmojiUtils.gridColumnSize return (CGPoint(x: point.x + xCorrection, y: point.y + yCorrection), - CGPoint(x: point.x, y: point.y - Self.gridColumnSize)) + CGPoint(x: point.x, y: point.y - EmojiUtils.gridColumnSize)) } } diff --git a/iOSClient/ObvMessenger/ObvMessenger/UIElements/ReorderableForEach.swift b/iOSClient/ObvMessenger/ObvMessenger/UIElements/ReorderableForEach.swift index d66aadbd..065500dc 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/UIElements/ReorderableForEach.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/UIElements/ReorderableForEach.swift @@ -29,7 +29,7 @@ struct ItemView: View { let item: Item let content: (Item) -> Content - let none: Item + let none: Item? @Binding var items: [Item] @Binding var currentDrop: Item? @@ -56,14 +56,14 @@ struct ItemView: View { struct ReorderableForEach: View { @Binding private var items: [Item] @Binding private var draggedItem: Item? - private let none: Item + private let none: Item? private let content: (Item) -> Content private let haptic: () -> Void init(items: Binding<[Item]>, draggedItem: Binding, haptic: @escaping () -> Void, - none: Item, + none: Item?, @ViewBuilder content: @escaping (Item) -> Content) { self._items = items self._draggedItem = draggedItem @@ -74,15 +74,17 @@ struct ReorderableForEach: View { @State private var currentDrop: Item? - /// List of items with an aditional none element - private var itemsWithLastSpace: [Item] { + /// List of items with an additional none element + private var itemsWithAdditionalSpace: [Item] { var result = items - result += [none] + if let none = none { + result += [none] + } return result } var body: some View { - ForEach(itemsWithLastSpace) { item in + ForEach(itemsWithAdditionalSpace) { item in ItemView(item: item, content: content, none: none, @@ -127,7 +129,7 @@ struct ReorderableForEach: View { @available(iOS 15, *) struct DropRelocateDelegate: DropDelegate { let item: Item - let none: Item + let none: Item? let haptic: () -> Void @Binding var items: [Item] @Binding var draggedItem: Item? diff --git a/iOSClient/ObvMessenger/ObvMessenger/UIElements/SwiftUIUtils.swift b/iOSClient/ObvMessenger/ObvMessenger/UIElements/SwiftUIUtils.swift index 41dd7a35..e49d4827 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/UIElements/SwiftUIUtils.swift +++ b/iOSClient/ObvMessenger/ObvMessenger/UIElements/SwiftUIUtils.swift @@ -78,3 +78,31 @@ struct DottedCircle: View { .frame(width: radius * 2, height: radius * 2) } } + +@available(iOS 13.0, *) +struct Positions: PreferenceKey { + static var defaultValue: [String: Anchor] = [:] + static func reduce(value: inout [String: Anchor], nextValue: () -> [String: Anchor]) { + value.merge(nextValue(), uniquingKeysWith: { current, _ in + return current }) + } +} + +@available(iOS 13.0, *) +struct PositionReader: View { + let tag: String + var body: some View { + Color.clear + .anchorPreference(key: Positions.self, value: .center) { (anchor) in + [tag: anchor] + } + } +} + +@available(iOS 13.0, *) +extension Task where Success == Never, Failure == Never { + static func sleep(seconds: Double) async throws { + let duration = UInt64(seconds * 1_000_000_000) + try await Task.sleep(nanoseconds: duration) + } +} diff --git a/iOSClient/ObvMessenger/ObvMessenger/en.lproj/Localizable.strings b/iOSClient/ObvMessenger/ObvMessenger/en.lproj/Localizable.strings index fe4f5b5d..5b540269 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/en.lproj/Localizable.strings +++ b/iOSClient/ObvMessenger/ObvMessenger/en.lproj/Localizable.strings @@ -718,6 +718,8 @@ Olvid's security policy requires you to re-validate the identity of %2@ by excha "Mark all as read" = "Mark all as read"; +"MARK_AS_READ" = "Mark as read"; + "Deleted message" = "Deleted message"; "Contact Introduction Performed" = "Contact Introduction Performed"; @@ -2061,7 +2063,7 @@ Olvid's security policy requires you to re-validate the identity of %2@ by excha "CLEANING_IN_PROGRESS" = "Cleaning in progress"; "CLEANING_TERMINATED" = "Cleaning is terminated"; -"DRAP_AND_DROP_TO_CONFIGURE_PREFERRED_EMOJIS_LIST" = "Drop your favorite emojis here!"; +"DRAG_AND_DROP_TO_CONFIGURE_PREFERRED_EMOJIS_LIST" = "Drop your favorite reactions here!"; "CONTACT_IS_NOT_ACTIVE_EXPLANATION_TITLE" = "Contact revoked by your company's identity provider"; @@ -2141,6 +2143,8 @@ Olvid's security policy requires you to re-validate the identity of %2@ by excha "OPEN_SOURCE_LICENCES" = "Open Source Licences"; +"HOW_TO_ADD_REACTION_TO_A_MESSAGE" = "You can double a tap a message to add a reaction."; + "RESET_COMPOSE_MESSAGE_VIEW_ACTIONS_ORDER" = "Reset buttons order to default"; "UNAVAILABLE_MESSAGE" = "Unavailable message"; @@ -2172,6 +2176,7 @@ Olvid's security policy requires you to re-validate the identity of %2@ by excha "SNACK_BAR_DETAILS_BODY_IOS_VERSION_ACCEPTABLE" = "To make sure you use the latest version of iOS, go to Settings > General, then tap Software Update."; "MESSAGE_REACTION_NOTIFICATION_%@_%@" = "Reacted %@ to: %@"; +"MESSAGE_REACTION_NOTIFICATION_%@" = "Reacted %@ to your message"; "NEW_REACTION" = "New reaction"; "TAP_TO_SEE_THE_REACTION" = "Tap to see the reaction."; @@ -2261,6 +2266,8 @@ Olvid's security policy requires you to re-validate the identity of %2@ by excha "Choose" = "Choose"; +"SEND_MESSAGE" = "Send message"; + "FAILED" = "Failed 😢"; "CALL_INITIALISATION_NOT_SUPPORTED" = "Secure calls are not supported"; @@ -2270,3 +2277,9 @@ Olvid's security policy requires you to re-validate the identity of %2@ by excha "Choose" = "Choose"; "YOUR_MESSAGE" = "Your message..."; + +"HOW_TO_ADD_MESSAGE_REACTION" = "Double tap a message to add a reaction."; + +"HOW_TO_ADD_REACTION_TO_PREFFERED" = "Add a star to a reaction to add it to your favorite reactions."; + +"HOW_TO_REMOVE_OWN_REACTION" = "Tap to remove your reaction."; diff --git a/iOSClient/ObvMessenger/ObvMessenger/fr.lproj/Localizable.strings b/iOSClient/ObvMessenger/ObvMessenger/fr.lproj/Localizable.strings index 04109135..929664f8 100644 --- a/iOSClient/ObvMessenger/ObvMessenger/fr.lproj/Localizable.strings +++ b/iOSClient/ObvMessenger/ObvMessenger/fr.lproj/Localizable.strings @@ -403,6 +403,9 @@ /* Action title */ "Mark all as read" = "Tout marquer comme lu"; +/* Action title */ +"MARK_AS_READ" = "Marquer comme lu"; + /* Table view group header */ "Maximum size for automatic downloads" = "Taille maximum pour téléchargement automatique"; @@ -2093,7 +2096,7 @@ "CLEANING_IN_PROGRESS" = "Nettoyage en cours"; "CLEANING_TERMINATED" = "Nettoyage terminé"; -"DRAP_AND_DROP_TO_CONFIGURE_PREFERRED_EMOJIS_LIST" = "Déposez ici vos émojis préférés !"; +"DRAG_AND_DROP_TO_CONFIGURE_PREFERRED_EMOJIS_LIST" = "Déposez ici vos réactions préférés !"; "CONTACT_IS_NOT_ACTIVE_EXPLANATION_TITLE" = "Contact revoqué par le fournisseur d'identités de votre société"; @@ -2173,6 +2176,8 @@ "OPEN_SOURCE_LICENCES" = "Licences Open Source"; +"HOW_TO_ADD_REACTION_TO_A_MESSAGE" = "Vous pouvez appuyer deux fois sur un message pour ajouter une reaction."; + "RESET_COMPOSE_MESSAGE_VIEW_ACTIONS_ORDER" = "Réinitialiser l'ordre des boutons"; "UNAVAILABLE_MESSAGE" = "Message non disponible"; @@ -2204,6 +2209,7 @@ "SNACK_BAR_DETAILS_BODY_IOS_VERSION_ACCEPTABLE" = "Pour vous assurez que vous utilisez la dernière version d'iOS, accédez à l'App Réglages sur votre appareil. Allez dans Général puis touchez Mise à jour logicielle."; "MESSAGE_REACTION_NOTIFICATION_%@_%@" = "A réagi %@ à : %@"; +"MESSAGE_REACTION_NOTIFICATION_%@" = "A réagi %@ à votre message"; "NEW_REACTION" = "Nouvelle réaction"; "TAP_TO_SEE_THE_REACTION" = "Appuyez pour voir la réaction."; @@ -2293,6 +2299,8 @@ "Choose" = "Choisir"; +"SEND_MESSAGE" = "Envoyer un message"; + "FAILED" = "Échec 😢"; "CALL_INITIALISATION_NOT_SUPPORTED" = "Appels non supportés"; @@ -2302,3 +2310,9 @@ "Choose" = "Choisir"; "YOUR_MESSAGE" = "Votre message..."; + +"HOW_TO_ADD_MESSAGE_REACTION" = "Tapez deux fois sur le message pour ajouter votre réaction."; + +"HOW_TO_ADD_REACTION_TO_PREFFERED" = "Ajoutez une étoile à une réaction pour l'ajouter à vos réactions préférées."; + +"HOW_TO_REMOVE_OWN_REACTION" = "Tappez pour supprimer votre réaction."; diff --git a/iOSClient/ObvMessenger/ObvMessengerNotificationServiceExtension/NotificationService.swift b/iOSClient/ObvMessenger/ObvMessengerNotificationServiceExtension/NotificationService.swift index f0545e78..c62e0659 100644 --- a/iOSClient/ObvMessenger/ObvMessengerNotificationServiceExtension/NotificationService.swift +++ b/iOSClient/ObvMessenger/ObvMessengerNotificationServiceExtension/NotificationService.swift @@ -168,6 +168,7 @@ class NotificationService: UNNotificationServiceExtension { let badge = incrAndGetBadge() let (_, notificationContent) = UserNotificationCreator.createNewMessageNotification( body: messageReceived.textBody ?? UserNotificationCreator.Strings.NewPersistedMessageReceivedMinimal.body, + isEphemeralMessageWithUserAction: messageReceived.isEphemeralMessageWithUserAction, messageIdentifierFromEngine: messageReceived.messageIdentifierFromEngine, contact: contact, attachmentsFileNames: [], @@ -319,8 +320,11 @@ class NotificationService: UNNotificationServiceExtension { if let messageJSON = persistedItemJSON.message { let textBody: String? - if let expiration = messageJSON.expiration, - expiration.visibilityDuration != nil || expiration.readOnce { + var isEphemeralMessageWithUserAction = false + if let expiration = messageJSON.expiration, expiration.visibilityDuration != nil || expiration.readOnce { + isEphemeralMessageWithUserAction = true + } + if isEphemeralMessageWithUserAction { textBody = NSLocalizedString("EPHEMERAL_MESSAGE", comment: "") } else { textBody = messageJSON.body @@ -328,6 +332,7 @@ class NotificationService: UNNotificationServiceExtension { let badge = incrAndGetBadge() let (_, notificationContent) = UserNotificationCreator.createNewMessageNotification( body: textBody ?? UserNotificationCreator.Strings.NewPersistedMessageReceivedMinimal.body, + isEphemeralMessageWithUserAction: isEphemeralMessageWithUserAction, messageIdentifierFromEngine: encryptedPushNotification.messageIdentifierFromEngine, contact: persistedContactIdentity, attachmentsFileNames: [], @@ -342,9 +347,9 @@ class NotificationService: UNNotificationServiceExtension { guard message is PersistedMessageSent, !message.isWiped else { return } if let emoji = reactionJSON.emoji { - let (_, notificationContent) = UserNotificationCreator.createReactionNotification(message: message, contact: persistedContactIdentity, emoji: emoji, reactionTimestamp: obvMessage.messageUploadTimestampFromServer) - - self?.fullAttemptContent = notificationContent + if let (_, notificationContent) = UserNotificationCreator.createReactionNotification(message: message, contact: persistedContactIdentity, emoji: emoji, reactionTimestamp: obvMessage.messageUploadTimestampFromServer) { + self?.fullAttemptContent = notificationContent + } } else { // Nothing can be done: we are not able to remove the notification from the extension and we cannot wake up the app. }