diff --git a/Mail/Views/Thread/Message/MessageBannerHeaderView.swift b/Mail/Views/Thread/Message/MessageBannerHeaderView.swift index 4392cd6cd4..9f86550319 100644 --- a/Mail/Views/Thread/Message/MessageBannerHeaderView.swift +++ b/Mail/Views/Thread/Message/MessageBannerHeaderView.swift @@ -30,6 +30,7 @@ struct MessageBannerHeaderView: View { @EnvironmentObject private var mailboxManager: MailboxManager @State private var isUnsubscribeSuccessful = false + @State private var isAcknowledgeSuccessful = false let banners: [MessageBanner] @@ -79,6 +80,22 @@ struct MessageBannerHeaderView: View { asyncAction: unsubscribeAction ) } + case .acknowledge: + if !isAcknowledgeSuccessful && message.hasPendingAcknowledgement { + MessageHeaderAsyncActionView( + icon: MailResourcesAsset.envelope.swiftUIImage, + message: MailResourcesStrings.Localizable.acknowledgementMessage, + actionTitle: MailResourcesStrings.Localizable.sendConfirmationAction, + showBottomSeparator: showBottomSeparator, + asyncAction: acknowledgeAction + ) + } else { + MessageHeaderActionView( + icon: MailResourcesAsset.check.swiftUIImage, + message: MailResourcesStrings.Localizable.acknowledgementMessageSent, + showBottomSeparator: showBottomSeparator + ) {} + } } } } @@ -106,6 +123,24 @@ struct MessageBannerHeaderView: View { snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarUnsubscribeFailure) } } + + private func acknowledgeAction() async { + @InjectService var snackbarPresenter: IKSnackBarPresentable + do { + try await mailboxManager.apiFetcher.acknowledgeMessage(messageResource: message.resource) + try mailboxManager.transactionExecutor.writeTransaction { realm in + if let live = realm.object(ofType: Message.self, forPrimaryKey: message.uid) { + live.acknowledgeStatus = .acknowledged + } + } + snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarAcknowledgementSuccess) + withAnimation { + isAcknowledgeSuccessful = true + } + } catch { + snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarAcknowledgementFailure) + } + } } #Preview { diff --git a/Mail/Views/Thread/Message/MessageSubHeaderView.swift b/Mail/Views/Thread/Message/MessageSubHeaderView.swift index 16e21a5eb9..4fabcbf4b4 100644 --- a/Mail/Views/Thread/Message/MessageSubHeaderView.swift +++ b/Mail/Views/Thread/Message/MessageSubHeaderView.swift @@ -58,6 +58,10 @@ struct MessageSubHeaderView: View { result.append(.encrypted) } + if message.hasAcknowledgement { + result.append(.acknowledge) + } + return result } diff --git a/MailCore/API/Endpoint/Endpoint.swift b/MailCore/API/Endpoint/Endpoint.swift index 8bab70c1a8..63798f2024 100644 --- a/MailCore/API/Endpoint/Endpoint.swift +++ b/MailCore/API/Endpoint/Endpoint.swift @@ -340,4 +340,8 @@ public extension Endpoint { static func unsubscribe(resource: String) -> Endpoint { return .resource(resource).appending(path: "/unsubscribeFromList") } + + static func acknowledge(resource: String) -> Endpoint { + return .resource(resource).appending(path: "/acknowledge") + } } diff --git a/MailCore/API/MailApiFetcher/MailApiFetcher+Extended.swift b/MailCore/API/MailApiFetcher/MailApiFetcher+Extended.swift index 7e668b1f64..78c628e913 100644 --- a/MailCore/API/MailApiFetcher/MailApiFetcher+Extended.swift +++ b/MailCore/API/MailApiFetcher/MailApiFetcher+Extended.swift @@ -223,4 +223,8 @@ public extension MailApiFetcher { func unsubscribe(messageResource: String) async throws { let _: Empty = try await perform(request: authenticatedRequest(.unsubscribe(resource: messageResource), method: .post)) } + + func acknowledgeMessage(messageResource: String) async throws { + let _: Empty = try await perform(request: authenticatedRequest(.acknowledge(resource: messageResource), method: .get)) + } } diff --git a/MailCore/Cache/MailboxManager/MailboxManager+Thread.swift b/MailCore/Cache/MailboxManager/MailboxManager+Thread.swift index 75a1d9976f..6db6b7efc8 100644 --- a/MailCore/Cache/MailboxManager/MailboxManager+Thread.swift +++ b/MailCore/Cache/MailboxManager/MailboxManager+Thread.swift @@ -528,7 +528,7 @@ public extension MailboxManager { } private func upsertMessage(_ message: Message, oldMessage: Message, threadsToUpdate: inout Set, using realm: Realm) { - keepCacheAttributes(for: message, keepProperties: .standard, using: realm) + keepCacheAttributes(for: message, keepProperties: [.standard, .acknowledge], using: realm) realm.add(message, update: .modified) threadsToUpdate.formUnion(oldMessage.threads) diff --git a/MailCore/Cache/MailboxManager/MailboxManager.swift b/MailCore/Cache/MailboxManager/MailboxManager.swift index 3bfda2dc13..8a4b89f09d 100644 --- a/MailCore/Cache/MailboxManager/MailboxManager.swift +++ b/MailCore/Cache/MailboxManager/MailboxManager.swift @@ -75,7 +75,7 @@ public final class MailboxManager: ObservableObject, MailboxManageable { let realmName = "\(mailbox.userId)-\(mailbox.mailboxId).realm" realmConfiguration = Realm.Configuration( fileURL: MailboxManager.constants.rootDocumentsURL.appendingPathComponent(realmName), - schemaVersion: 47, + schemaVersion: 48, migrationBlock: { migration, oldSchemaVersion in // No migration needed from 0 to 16 if oldSchemaVersion < 17 { @@ -187,6 +187,7 @@ public final class MailboxManager: ObservableObject, MailboxManageable { static let localSafeDisplay = MessagePropertiesOptions(rawValue: 1 << 3) static let reactions = MessagePropertiesOptions(rawValue: 1 << 4) static let calendarEventResponse = MessagePropertiesOptions(rawValue: 1 << 5) + static let acknowledge = MessagePropertiesOptions(rawValue: 1 << 6) static let standard: MessagePropertiesOptions = [ .fullyDownloaded, @@ -228,6 +229,10 @@ public final class MailboxManager: ObservableObject, MailboxManageable { if keepProperties.contains(.calendarEventResponse), let calendarEventResponse = savedMessage.calendarEventResponse { message.calendarEventResponse = calendarEventResponse.detached() } + if keepProperties.contains(.acknowledge), + message.acknowledge == nil { + message.acknowledge = savedMessage.acknowledge + } } func keepCacheAttributes( diff --git a/MailCore/Models/Message.swift b/MailCore/Models/Message.swift index 41317542cb..a42e00849b 100644 --- a/MailCore/Models/Message.swift +++ b/MailCore/Models/Message.swift @@ -83,6 +83,11 @@ public enum MessageDKIM: String, Codable, PersistableEnum { case notSigned = "not_signed" } +public enum AcknowledgeStatus: String { + case pending + case acknowledged +} + public struct MessageActionResult: Codable { public var flagged: Int } @@ -163,6 +168,7 @@ public final class Message: Object, Decodable, ObjectKeyIdentifiable { @Persisted public var encrypted: Bool @Persisted public var encryptionPassword: String @Persisted public var cryptPasswordValidity: Date? + @Persisted var acknowledge: String? @Persisted private var headers: MessageHeaders? /// Threads where the message can be found @Persisted(originProperty: "messages") var threads: LinkingObjects @@ -289,6 +295,22 @@ public final class Message: Object, Decodable, ObjectKeyIdentifiable { return !reactionMessages.where { $0.seen == false }.isEmpty } + public var acknowledgeStatus: AcknowledgeStatus? { + get { + return AcknowledgeStatus(rawValue: acknowledge ?? "") + } set { + acknowledge = newValue?.rawValue + } + } + + public var hasAcknowledgement: Bool { + return hasPendingAcknowledgement || acknowledgeStatus == .acknowledged + } + + public var hasPendingAcknowledgement: Bool { + return acknowledgeStatus == .pending + } + public func fromMe(currentMailboxEmail: String) -> Bool { return from.contains { $0.isMe(currentMailboxEmail: currentMailboxEmail) } } @@ -379,6 +401,7 @@ public final class Message: Object, Decodable, ObjectKeyIdentifiable { case emojiReaction case emojiReactionNotAllowedReason case headers + case acknowledge } override init() { @@ -459,6 +482,7 @@ public final class Message: Object, Decodable, ObjectKeyIdentifiable { ) headers = try? values.decodeIfPresent(MessageHeaders.self, forKey: .headers) + acknowledge = try values.decodeIfPresent(String.self, forKey: .acknowledge) } public convenience init( @@ -498,7 +522,8 @@ public final class Message: Object, Decodable, ObjectKeyIdentifiable { snoozeUuid: String? = nil, snoozeEndDate: Date? = nil, emojiReaction: String? = nil, - emojiReactionNotAllowedReason: EmojiReactionNotAllowedReason? = nil + emojiReactionNotAllowedReason: EmojiReactionNotAllowedReason? = nil, + acknowledge: String? = nil ) { self.init() @@ -540,6 +565,7 @@ public final class Message: Object, Decodable, ObjectKeyIdentifiable { self.snoozeEndDate = snoozeEndDate self.emojiReaction = emojiReaction self.emojiReactionNotAllowedReason = emojiReactionNotAllowedReason + self.acknowledge = acknowledge } public func toThread() -> Thread { diff --git a/MailCore/Models/MessageBanner.swift b/MailCore/Models/MessageBanner.swift index cc815029ba..86f631d907 100644 --- a/MailCore/Models/MessageBanner.swift +++ b/MailCore/Models/MessageBanner.swift @@ -28,6 +28,7 @@ public enum MessageBanner: Equatable, Identifiable, Hashable { case displayContent case encrypted case unsubscribeLink + case acknowledge public static func == (lhs: MessageBanner, rhs: MessageBanner) -> Bool { switch (lhs, rhs) { @@ -41,6 +42,8 @@ public enum MessageBanner: Equatable, Identifiable, Hashable { return true case (.unsubscribeLink, .unsubscribeLink): return true + case (.acknowledge, .acknowledge): + return true default: return false } @@ -58,6 +61,8 @@ public extension [MessageBanner] { return !contains(.encrypted) case .unsubscribeLink: return true + case .acknowledge: + return true default: return false } diff --git a/MailCoreUI/Helpers/PreviewHelper.swift b/MailCoreUI/Helpers/PreviewHelper.swift index 4b20804153..4f370434db 100644 --- a/MailCoreUI/Helpers/PreviewHelper.swift +++ b/MailCoreUI/Helpers/PreviewHelper.swift @@ -123,7 +123,8 @@ public enum PreviewHelper { scheduled: false, forwarded: false, flagged: false, - hasUnsubscribeLink: true) + hasUnsubscribeLink: true, + acknowledge: "pending") public static let sampleMessages = Array( repeating: PreviewHelper.sampleMessage, diff --git a/MailResources/Localizable/de.lproj/Localizable.strings b/MailResources/Localizable/de.lproj/Localizable.strings index d78190f185..e1c906bc8f 100644 --- a/MailResources/Localizable/de.lproj/Localizable.strings +++ b/MailResources/Localizable/de.lproj/Localizable.strings @@ -16,6 +16,12 @@ /* loco:642179d9733cd4523d03dcf2 */ "accentColorSystemTitle" = "Farbe des Systems"; +/* loco:6936f775de93800154038b75 */ +"acknowledgementMessage" = "Der Absender hat eine Lesebestätigung angefordert."; + +/* loco:6936f7e6835ef86e27063eb6 */ +"acknowledgementMessageSent" = "Es wurde eine Lesebestätigung an den Absender gesendet."; + /* loco:629f0e2244ac887a51141312 */ "actionArchive" = "Archiv"; @@ -1360,6 +1366,9 @@ /* loco:62ac871d2502053d7b0cbb32 */ "send" = "Senden Sie"; +/* loco:6936f6d2c3bd045a2001d752 */ +"sendConfirmationAction" = "Senden Sie die Bestätigung"; + /* loco:627e401ec1c7c02de9181292 */ "sentFolder" = "Gesendete Nachrichten"; @@ -1627,6 +1636,12 @@ /* loco:652fd443ec75b05b000d43f2 */ "snackBarAccountDeleted" = "Gelöschtes Konto"; +/* loco:6937d843b9fb15c2510859d2 */ +"snackbarAcknowledgementFailure" = "Senden fehlgeschlagen"; + +/* loco:6937d715da67a52ff201eed2 */ +"snackbarAcknowledgementSuccess" = "Lesebestätigung gesendet"; + /* loco:63f3961e7c16fb607809d632 */ "snackbarBlockUserConfirmation" = "Absender ist jetzt blockiert"; diff --git a/MailResources/Localizable/en.lproj/Localizable.strings b/MailResources/Localizable/en.lproj/Localizable.strings index ad3a82384d..8d91677252 100644 --- a/MailResources/Localizable/en.lproj/Localizable.strings +++ b/MailResources/Localizable/en.lproj/Localizable.strings @@ -16,6 +16,12 @@ /* loco:642179d9733cd4523d03dcf2 */ "accentColorSystemTitle" = "System color"; +/* loco:6936f775de93800154038b75 */ +"acknowledgementMessage" = "The sender requested a read receipt."; + +/* loco:6936f7e6835ef86e27063eb6 */ +"acknowledgementMessageSent" = "A read receipt has been sent to the sender."; + /* loco:629f0e2244ac887a51141312 */ "actionArchive" = "Archive"; @@ -1360,6 +1366,9 @@ /* loco:62ac871d2502053d7b0cbb32 */ "send" = "Send"; +/* loco:6936f6d2c3bd045a2001d752 */ +"sendConfirmationAction" = "Send the confirmation"; + /* loco:627e401ec1c7c02de9181292 */ "sentFolder" = "Sent messages"; @@ -1627,6 +1636,12 @@ /* loco:652fd443ec75b05b000d43f2 */ "snackBarAccountDeleted" = "Account deleted"; +/* loco:6937d843b9fb15c2510859d2 */ +"snackbarAcknowledgementFailure" = "Failed to send"; + +/* loco:6937d715da67a52ff201eed2 */ +"snackbarAcknowledgementSuccess" = "Read receipt sent"; + /* loco:63f3961e7c16fb607809d632 */ "snackbarBlockUserConfirmation" = "Sender is now blocked"; diff --git a/MailResources/Localizable/es.lproj/Localizable.strings b/MailResources/Localizable/es.lproj/Localizable.strings index 856ea94f97..02d91bfe1f 100644 --- a/MailResources/Localizable/es.lproj/Localizable.strings +++ b/MailResources/Localizable/es.lproj/Localizable.strings @@ -16,6 +16,12 @@ /* loco:642179d9733cd4523d03dcf2 */ "accentColorSystemTitle" = "Color del sistema"; +/* loco:6936f775de93800154038b75 */ +"acknowledgementMessage" = "El remitente ha solicitado una confirmación de lectura."; + +/* loco:6936f7e6835ef86e27063eb6 */ +"acknowledgementMessageSent" = "Se ha enviado una confirmación de lectura al remitente."; + /* loco:629f0e2244ac887a51141312 */ "actionArchive" = "Archivo"; @@ -1357,6 +1363,9 @@ /* loco:62ac871d2502053d7b0cbb32 */ "send" = "Enviar"; +/* loco:6936f6d2c3bd045a2001d752 */ +"sendConfirmationAction" = "Enviar la confirmación"; + /* loco:627e401ec1c7c02de9181292 */ "sentFolder" = "Mensajes enviados"; @@ -1624,6 +1633,12 @@ /* loco:652fd443ec75b05b000d43f2 */ "snackBarAccountDeleted" = "Cuenta eliminada"; +/* loco:6937d843b9fb15c2510859d2 */ +"snackbarAcknowledgementFailure" = "Error al enviar"; + +/* loco:6937d715da67a52ff201eed2 */ +"snackbarAcknowledgementSuccess" = "Recibo de lectura enviado"; + /* loco:63f3961e7c16fb607809d632 */ "snackbarBlockUserConfirmation" = "El remitente está bloqueado"; diff --git a/MailResources/Localizable/fr.lproj/Localizable.strings b/MailResources/Localizable/fr.lproj/Localizable.strings index 64bf8fa913..ea4364d02c 100644 --- a/MailResources/Localizable/fr.lproj/Localizable.strings +++ b/MailResources/Localizable/fr.lproj/Localizable.strings @@ -16,6 +16,12 @@ /* loco:642179d9733cd4523d03dcf2 */ "accentColorSystemTitle" = "Couleur du système"; +/* loco:6936f775de93800154038b75 */ +"acknowledgementMessage" = "L’expéditeur a demandé une confirmation de lecture."; + +/* loco:6936f7e6835ef86e27063eb6 */ +"acknowledgementMessageSent" = "Une confirmation de lecture a été envoyée à l’expéditeur."; + /* loco:629f0e2244ac887a51141312 */ "actionArchive" = "Archiver"; @@ -1360,6 +1366,9 @@ /* loco:62ac871d2502053d7b0cbb32 */ "send" = "Envoyer"; +/* loco:6936f6d2c3bd045a2001d752 */ +"sendConfirmationAction" = "Envoyer la confirmation"; + /* loco:627e401ec1c7c02de9181292 */ "sentFolder" = "Messages envoyés"; @@ -1627,6 +1636,12 @@ /* loco:652fd443ec75b05b000d43f2 */ "snackBarAccountDeleted" = "Compte supprimé"; +/* loco:6937d843b9fb15c2510859d2 */ +"snackbarAcknowledgementFailure" = "Échec de l’envoi"; + +/* loco:6937d715da67a52ff201eed2 */ +"snackbarAcknowledgementSuccess" = "Accusé de lecture envoyé"; + /* loco:63f3961e7c16fb607809d632 */ "snackbarBlockUserConfirmation" = "L’expéditeur est maintenant bloqué"; diff --git a/MailResources/Localizable/it.lproj/Localizable.strings b/MailResources/Localizable/it.lproj/Localizable.strings index 6b42d2eb34..5a0f89fac7 100644 --- a/MailResources/Localizable/it.lproj/Localizable.strings +++ b/MailResources/Localizable/it.lproj/Localizable.strings @@ -16,6 +16,12 @@ /* loco:642179d9733cd4523d03dcf2 */ "accentColorSystemTitle" = "Colore del sistema"; +/* loco:6936f775de93800154038b75 */ +"acknowledgementMessage" = "Il mittente ha richiesto una ricevuta di lettura."; + +/* loco:6936f7e6835ef86e27063eb6 */ +"acknowledgementMessageSent" = "È stata inviata una ricevuta di lettura al mittente."; + /* loco:629f0e2244ac887a51141312 */ "actionArchive" = "Archivia"; @@ -1360,6 +1366,9 @@ /* loco:62ac871d2502053d7b0cbb32 */ "send" = "Invia"; +/* loco:6936f6d2c3bd045a2001d752 */ +"sendConfirmationAction" = "Inviare la conferma"; + /* loco:627e401ec1c7c02de9181292 */ "sentFolder" = "Messaggi inviati"; @@ -1627,6 +1636,12 @@ /* loco:652fd443ec75b05b000d43f2 */ "snackBarAccountDeleted" = "Account cancellato"; +/* loco:6937d843b9fb15c2510859d2 */ +"snackbarAcknowledgementFailure" = "Invio non riuscito"; + +/* loco:6937d715da67a52ff201eed2 */ +"snackbarAcknowledgementSuccess" = "Ricevuta di lettura inviata"; + /* loco:63f3961e7c16fb607809d632 */ "snackbarBlockUserConfirmation" = "Il mittente è ora bloccato";