From e8b18ab4d2b29e37c323f0c8457c6e5b622b9768 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Thu, 6 Jul 2023 14:17:57 +0200 Subject: [PATCH] fix: Correctly load Draft body --- .../New Message/ComposeMessageView.swift | 114 ++--------- .../Signatures/SignaturesManager.swift | 82 -------- MailCore/Cache/DraftContentManager.swift | 178 ++++++++++++++++++ MailCore/Cache/MailboxManager.swift | 22 --- 4 files changed, 198 insertions(+), 198 deletions(-) delete mode 100644 Mail/Views/New Message/Signatures/SignaturesManager.swift create mode 100644 MailCore/Cache/DraftContentManager.swift diff --git a/Mail/Views/New Message/ComposeMessageView.swift b/Mail/Views/New Message/ComposeMessageView.swift index 69a910fac..3c3d34423 100644 --- a/Mail/Views/New Message/ComposeMessageView.swift +++ b/Mail/Views/New Message/ComposeMessageView.swift @@ -63,14 +63,11 @@ struct ComposeMessageView: View { @LazyInjectService private var matomo: MatomoUtils @LazyInjectService private var draftManager: DraftManager - @State private var isLoadingContent: Bool + @State private var isLoadingContent = true @State private var isShowingCancelAttachmentsError = false @State private var autocompletionType: ComposeViewFieldType? @State private var editorFocus = false - /// Something to track the initial loading of a default signature - @StateObject private var signatureManager: SignaturesManager - @StateObject private var mailboxManager: MailboxManager @StateObject private var attachmentsManager: AttachmentsManager @StateObject private var alert = NewMessageAlert() @@ -83,7 +80,9 @@ struct ComposeMessageView: View { } } - let messageReply: MessageReply? + private let messageReply: MessageReply? + private let draftContentManager: DraftContentManager + private let mailboxManager: MailboxManager private var isSendButtonDisabled: Bool { let disabledState = draft.identityId == nil @@ -93,7 +92,7 @@ struct ComposeMessageView: View { return disabledState } - // MAK: - Int + // MARK: - Init init(draft: Draft, mailboxManager: MailboxManager, messageReply: MessageReply? = nil) { self.messageReply = messageReply @@ -101,14 +100,17 @@ struct ComposeMessageView: View { Self.saveNewDraftInRealm(mailboxManager.getRealm(), draft: draft) _draft = StateRealmObject(wrappedValue: draft) - _isLoadingContent = State(wrappedValue: (draft.messageUid != nil && draft.remoteUUID.isEmpty) || messageReply != nil) + draftContentManager = DraftContentManager( + incompleteDraft: draft, + messageReply: messageReply, + mailboxManager: mailboxManager + ) - _signatureManager = StateObject(wrappedValue: SignaturesManager(mailboxManager: mailboxManager)) - _mailboxManager = StateObject(wrappedValue: mailboxManager) + self.mailboxManager = mailboxManager _attachmentsManager = StateObject(wrappedValue: AttachmentsManager(draft: draft, mailboxManager: mailboxManager)) } - // MAK: - View + // MARK: - View var body: some View { NavigationView { @@ -139,7 +141,7 @@ struct ComposeMessageView: View { VStack(spacing: 0) { ComposeMessageHeaderView(draft: draft, focusedField: _focusedField, autocompletionType: $autocompletionType) - if autocompletionType == nil { + if autocompletionType == nil && !isLoadingContent { ComposeMessageBodyView( draft: draft, isLoadingContent: $isLoadingContent, @@ -153,21 +155,15 @@ struct ComposeMessageView: View { } } .task { - await prepareCompleteDraft() - } - .task { - await prepareReplyForwardBodyAndAttachments() - } - .onChange(of: signatureManager.loadingSignatureState) { state in - switch state { - case .success: - setSignature() - case .error: + do { + isLoadingContent = true + try await draftContentManager.prepareCompleteDraft() + attachmentsManager.completeUploadedAttachments() + isLoadingContent = false + } catch { // Unable to get signatures, "An error occurred" and close modal. IKSnackBar.showSnackBar(message: MailError.unknownError.localizedDescription) dismiss() - case .progress: - break } } .background(MailResourcesAsset.backgroundColor.swiftUIColor) @@ -183,7 +179,7 @@ struct ComposeMessageView: View { draftManager.syncDraft(mailboxManager: mailboxManager) } .overlay { - if isLoadingContent || signatureManager.loadingSignatureState == .progress { + if isLoadingContent { progressView } } @@ -252,76 +248,6 @@ struct ComposeMessageView: View { realm.add(draft, update: .modified) } } - - private func prepareCompleteDraft() async { - guard draft.messageUid != nil && draft.remoteUUID.isEmpty else { return } - - do { - try await mailboxManager.draft(partialDraft: draft) - isLoadingContent = false - } catch { - dismiss() - IKSnackBar.showSnackBar(message: MailError.unknownError.localizedDescription) - } - } - - private func prepareReplyForwardBodyAndAttachments() async { - guard let messageReply else { return } - - let prepareTask = Task.detached { - try await prepareBody(message: messageReply.message, replyMode: messageReply.replyMode) - try await prepareAttachments(message: messageReply.message, replyMode: messageReply.replyMode) - } - - do { - _ = try await prepareTask.value - - isLoadingContent = false - } catch { - dismiss() - IKSnackBar.showSnackBar(message: MailError.unknownError.localizedDescription) - } - } - - private func prepareBody(message: Message, replyMode: ReplyMode) async throws { - if !message.fullyDownloaded { - try await mailboxManager.message(message: message) - } - - guard let freshMessage = message.thaw() else { return } - freshMessage.realm?.refresh() - $draft.body.wrappedValue = Draft.replyingBody(message: freshMessage, replyMode: replyMode) - } - - private func prepareAttachments(message: Message, replyMode: ReplyMode) async throws { - guard replyMode == .forward else { return } - let attachments = try await mailboxManager.apiFetcher.attachmentsToForward( - mailbox: mailboxManager.mailbox, - message: message - ).attachments - - for attachment in attachments { - $draft.attachments.append(attachment) - } - attachmentsManager.completeUploadedAttachments() - } - - private func setSignature() { - guard draft.identityId == nil || draft.identityId?.isEmpty == true else { - return - } - - guard let defaultSignature = mailboxManager.getStoredSignatures().defaultSignature else { - return - } - - let body = $draft.body.wrappedValue - let signedBody = defaultSignature.appendSignature(to: body) - - // At this point we have signatures in base up to date, we use the default one. - $draft.identityId.wrappedValue = "\(defaultSignature.id)" - $draft.body.wrappedValue = signedBody - } } struct ComposeMessageView_Previews: PreviewProvider { diff --git a/Mail/Views/New Message/Signatures/SignaturesManager.swift b/Mail/Views/New Message/Signatures/SignaturesManager.swift deleted file mode 100644 index 22bafb0c3..000000000 --- a/Mail/Views/New Message/Signatures/SignaturesManager.swift +++ /dev/null @@ -1,82 +0,0 @@ -/* - Infomaniak Mail - iOS App - Copyright (C) 2022 Infomaniak Network SA - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program 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 General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -import MailCore -import Sentry -import SwiftUI - -final class SignaturesManager: ObservableObject { - /// Represents the loading state - enum SignaturesLoadingState: Equatable { - static func == (lhs: SignaturesManager.SignaturesLoadingState, rhs: SignaturesManager.SignaturesLoadingState) -> Bool { - switch (lhs, rhs) { - case (.success, .success): - return true - case (.progress, .progress): - return true - case (.error(let left), .error(let right)): - return left == right - default: - return false - } - } - - case success - case progress - case error(_ wrapping: NSError) - } - - @Published var loadingSignatureState: SignaturesLoadingState = .progress - - private let mailboxManager: MailboxManager - init(mailboxManager: MailboxManager) { - self.mailboxManager = mailboxManager - - loadRemoteSignatures() - } - - /// Load the signatures every time at init, set `doneLoadingDefaultSignature` to true when done - private func loadRemoteSignatures() { - Task { - do { - // load all signatures every time - try await mailboxManager.refreshAllSignatures() - - // If after a refresh we have no default signature we bail - guard mailboxManager.getStoredSignatures().defaultSignature != nil else { - throw MailError.defaultSignatureMissing - } - - await MainActor.run { - loadingSignatureState = .success - } - } catch { - await MainActor.run { - loadingSignatureState = .error(error as NSError) - } - - SentrySDK.capture(message: "We failed to fetch Signatures. This will close the Editor.") { scope in - scope.setExtras([ - "errorMessage": error.localizedDescription, - "error": "\(error)" - ]) - } - } - } - } -} diff --git a/MailCore/Cache/DraftContentManager.swift b/MailCore/Cache/DraftContentManager.swift new file mode 100644 index 000000000..db4c8dea4 --- /dev/null +++ b/MailCore/Cache/DraftContentManager.swift @@ -0,0 +1,178 @@ +/* + Infomaniak Mail - iOS App + Copyright (C) 2022 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import Foundation +import Sentry + +public class DraftContentManager: ObservableObject { + struct CompleteDraftResult { + let body: String + let attachments: [Attachment] + let shouldAddSignatureText: Bool + } + + let messageReply: MessageReply? + let mailboxManager: MailboxManager + let incompleteDraft: Draft + + public init(incompleteDraft: Draft, messageReply: MessageReply?, mailboxManager: MailboxManager) { + self.incompleteDraft = incompleteDraft.freezeIfNeeded() + self.messageReply = messageReply + self.mailboxManager = mailboxManager + } + + public func prepareCompleteDraft() async throws { + async let draftBodyResult = try await loadCompleteDraftBody() + async let signature = try await loadDefaultRemoteSignature() + + try await writeCompleteDraft( + completeBody: draftBodyResult.body, + signature: signature, + shouldAddSignatureText: draftBodyResult.shouldAddSignatureText, + attachments: draftBodyResult.attachments + ) + } + + private func loadCompleteDraftBody() async throws -> CompleteDraftResult { + var completeDraftBody: String + var attachments = [Attachment]() + let shouldAddSignatureText: Bool + + if let messageReply { + // New draft created either with reply or forward + async let completeDraftReplyingBody = try await loadReplyingBody( + message: messageReply.message, + replyMode: messageReply.replyMode + ) + async let replyingAttachments = try await loadReplyingAttachments( + message: messageReply.message, + replyMode: messageReply.replyMode + ) + + completeDraftBody = try await completeDraftReplyingBody + attachments = try await replyingAttachments + shouldAddSignatureText = true + } else if incompleteDraft.messageUid != nil && incompleteDraft.remoteUUID.isEmpty { + // Draft loaded remotely + completeDraftBody = try await loadCompleteDraftIfNeeded() + shouldAddSignatureText = false + } else if !incompleteDraft.remoteUUID.isEmpty { + // Draft loaded remotely but we have it locally + completeDraftBody = incompleteDraft.body + shouldAddSignatureText = false + } else { + // New draft + completeDraftBody = "" + shouldAddSignatureText = true + } + + return CompleteDraftResult( + body: completeDraftBody, + attachments: attachments, + shouldAddSignatureText: shouldAddSignatureText + ) + } + + private func writeCompleteDraft( + completeBody: String, + signature: Signature, + shouldAddSignatureText: Bool, + attachments: [Attachment] + ) throws { + let realm = mailboxManager.getRealm() + guard let liveIncompleteDraft = realm.object(ofType: Draft.self, forPrimaryKey: incompleteDraft.localUUID) else { + throw MailError.unknownError + } + + try? realm.write { + if liveIncompleteDraft.identityId == nil || liveIncompleteDraft.identityId?.isEmpty == true { + liveIncompleteDraft.identityId = "\(signature.id)" + if shouldAddSignatureText { + liveIncompleteDraft.body = signature.appendSignature(to: completeBody) + } + } else { + liveIncompleteDraft.body = completeBody + } + + for attachment in attachments { + liveIncompleteDraft.attachments.append(attachment) + } + } + } + + private func loadDefaultRemoteSignature() async throws -> Signature { + do { + // load all signatures every time + try await mailboxManager.refreshAllSignatures() + + // If after a refresh we have no default signature we bail + guard let defaultSignature = mailboxManager.getStoredSignatures().defaultSignature else { + throw MailError.defaultSignatureMissing + } + + return defaultSignature.freezeIfNeeded() + } catch { + SentrySDK.capture(message: "We failed to fetch Signatures. This will close the Editor.") { scope in + scope.setExtras([ + "errorMessage": error.localizedDescription, + "error": "\(error)" + ]) + } + throw error + } + } + + private func loadReplyingBody(message: Message, replyMode: ReplyMode) async throws -> String { + if !message.fullyDownloaded { + try await mailboxManager.message(message: message) + } + + guard let freshMessage = message.thaw() else { throw MailError.unknownError } + freshMessage.realm?.refresh() + return Draft.replyingBody(message: freshMessage, replyMode: replyMode) + } + + private func loadReplyingAttachments(message: Message, replyMode: ReplyMode) async throws -> [Attachment] { + guard replyMode == .forward else { return [] } + let attachments = try await mailboxManager.apiFetcher.attachmentsToForward( + mailbox: mailboxManager.mailbox, + message: message + ).attachments + + return attachments + } + + private func loadCompleteDraftIfNeeded() async throws -> String { + guard let associatedMessage = mailboxManager.getRealm() + .object(ofType: Message.self, forPrimaryKey: incompleteDraft.messageUid)?.freeze() + else { throw MailError.localMessageNotFound } + + let remoteDraft = try await mailboxManager.apiFetcher.draft(from: associatedMessage) + + remoteDraft.localUUID = incompleteDraft.localUUID + remoteDraft.action = .save + remoteDraft.delay = incompleteDraft.delay + + let realm = mailboxManager.getRealm() + try? realm.safeWrite { + realm.add(remoteDraft.detached(), update: .modified) + } + + return remoteDraft.body + } +} diff --git a/MailCore/Cache/MailboxManager.swift b/MailCore/Cache/MailboxManager.swift index d96fb4b76..2af728a4c 100644 --- a/MailCore/Cache/MailboxManager.swift +++ b/MailCore/Cache/MailboxManager.swift @@ -1118,28 +1118,6 @@ public class MailboxManager: ObservableObject { return realm.objects(Draft.self).where { $0.action != nil } } - public func draft(partialDraft: Draft) async throws { - guard let associatedMessage = getRealm().object(ofType: Message.self, forPrimaryKey: partialDraft.messageUid)?.freeze() - else { throw MailError.localMessageNotFound } - - // Get from API - let draft = try await apiFetcher.draft(from: associatedMessage) - - await backgroundRealm.execute { realm in - draft.localUUID = partialDraft.localUUID - draft.action = .save - - // We made sure beforehand to have an up to date signature. - // If the server does not return an identityId, we want to keep the original one - draft.identityId = partialDraft.identityId ?? draft.identityId - draft.delay = partialDraft.delay - - try? realm.safeWrite { - realm.add(draft.detached(), update: .modified) - } - } - } - public func draft(messageUid: String, using realm: Realm? = nil) -> Draft? { let realm = realm ?? getRealm() return realm.objects(Draft.self).where { $0.messageUid == messageUid }.first