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