diff --git a/Mail/Utils/DraftUtils.swift b/Mail/Utils/DraftUtils.swift index a5b4c706d..8eff13de6 100644 --- a/Mail/Utils/DraftUtils.swift +++ b/Mail/Utils/DraftUtils.swift @@ -17,28 +17,55 @@ */ import Foundation +import InfomaniakCoreUI +import InfomaniakDI import MailCore import SwiftUI @MainActor enum DraftUtils { - public static func editDraft(from thread: Thread, mailboxManager: MailboxManageable, editedDraft: Binding) { + public static func editDraft( + from thread: Thread, + mailboxManager: MailboxManageable, + composeMessageIntent: Binding + ) { guard let message = thread.messages.first else { return } // If we already have the draft locally, present it directly if let draft = mailboxManager.draft(messageUid: message.uid, using: nil)?.detached() { - editedDraft.wrappedValue = EditedDraft.existing(draft: draft) + matomoOpenDraft(isLoadedRemotely: false) + composeMessageIntent.wrappedValue = ComposeMessageIntent.existing(draft: draft, originMailboxManager: mailboxManager) } else { - DraftUtils.editDraft(from: message, mailboxManager: mailboxManager, editedDraft: editedDraft) + DraftUtils.editDraft(from: message, mailboxManager: mailboxManager, composeMessageIntent: composeMessageIntent) } } - public static func editDraft(from message: Message, mailboxManager: MailboxManageable, editedDraft: Binding) { + public static func editDraft( + from message: Message, + mailboxManager: MailboxManageable, + composeMessageIntent: Binding + ) { // If we already have the draft locally, present it directly if let draft = mailboxManager.draft(messageUid: message.uid, using: nil)?.detached() { - editedDraft.wrappedValue = EditedDraft.existing(draft: draft) + matomoOpenDraft(isLoadedRemotely: false) + composeMessageIntent.wrappedValue = ComposeMessageIntent.existing(draft: draft, originMailboxManager: mailboxManager) // Draft comes from API, we will update it after showing the ComposeMessageView } else { - editedDraft.wrappedValue = EditedDraft.existing(draft: Draft(messageUid: message.uid)) + matomoOpenDraft(isLoadedRemotely: true) + composeMessageIntent.wrappedValue = ComposeMessageIntent.existingRemote( + messageUid: message.uid, + originMailboxManager: mailboxManager + ) } } + + private static func matomoOpenDraft(isLoadedRemotely: Bool) { + @InjectService var matomo: MatomoUtils + matomo.track(eventWithCategory: .newMessage, name: "openFromDraft") + matomo.track( + eventWithCategory: .newMessage, + action: .data, + name: "openLocalDraft", + value: !isLoadedRemotely + ) + } } diff --git a/Mail/Utils/ShortcutModifier.swift b/Mail/Utils/ShortcutModifier.swift index 8ee30d048..4aea6e3b0 100644 --- a/Mail/Utils/ShortcutModifier.swift +++ b/Mail/Utils/ShortcutModifier.swift @@ -98,7 +98,7 @@ struct ShortcutModifier: ViewModifier { private func shortcutNewMessage() { matomo.track(eventWithCategory: .keyboardShortcutActions, action: .input, name: "newMessage") - mainViewState.editedDraft = EditedDraft.new() + mainViewState.composeMessageIntent = .new(originMailboxManager: viewModel.mailboxManager) } private func shortcutRefresh() { diff --git a/Mail/Views/AI Writer/AIModel.swift b/Mail/Views/AI Writer/AIModel.swift index 163cd5cfd..257ee34c1 100644 --- a/Mail/Views/AI Writer/AIModel.swift +++ b/Mail/Views/AI Writer/AIModel.swift @@ -49,7 +49,6 @@ final class AIModel: ObservableObject { private let mailboxManager: MailboxManager private let draftContentManager: DraftContentManager private let draft: Draft - private var messageReply: MessageReply? private var contextId: String? private var recipientsList: String? @@ -72,15 +71,13 @@ final class AIModel: ObservableObject { } } - var isReplying: Bool { - messageReply?.isReplying == true - } + var isReplying: Bool - init(mailboxManager: MailboxManager, draftContentManager: DraftContentManager, editedDraft: EditedDraft) { + init(mailboxManager: MailboxManager, draftContentManager: DraftContentManager, draft: Draft, isReplying: Bool) { self.mailboxManager = mailboxManager self.draftContentManager = draftContentManager - draft = editedDraft.detachedDraft - messageReply = editedDraft.messageReply + self.draft = draft + self.isReplying = isReplying } } @@ -194,7 +191,7 @@ extension AIModel { // If the context is too long, we must remove it so that the user can use // the AI assistant without context for future trials if self.error == .contextMaxSyntaxTokensReached { - messageReply = nil + isReplying = false } } diff --git a/Mail/Views/AI Writer/Prompt/AIPromptView.swift b/Mail/Views/AI Writer/Prompt/AIPromptView.swift index ad791a66f..8a9efeea1 100644 --- a/Mail/Views/AI Writer/Prompt/AIPromptView.swift +++ b/Mail/Views/AI Writer/Prompt/AIPromptView.swift @@ -127,6 +127,7 @@ struct AIPromptView: View { AIPromptView(aiModel: AIModel( mailboxManager: PreviewHelper.sampleMailboxManager, draftContentManager: PreviewHelper.sampleDraftContentManager, - editedDraft: EditedDraft.new() + draft: Draft(), + isReplying: false )) } diff --git a/Mail/Views/AI Writer/Proposition/AIPropositionMenu.swift b/Mail/Views/AI Writer/Proposition/AIPropositionMenu.swift index 53de7c519..dcf8d6cef 100644 --- a/Mail/Views/AI Writer/Proposition/AIPropositionMenu.swift +++ b/Mail/Views/AI Writer/Proposition/AIPropositionMenu.swift @@ -88,6 +88,7 @@ struct FixedMenuOrderModifier: ViewModifier { AIPropositionMenu(aiModel: AIModel( mailboxManager: PreviewHelper.sampleMailboxManager, draftContentManager: PreviewHelper.sampleDraftContentManager, - editedDraft: .new() + draft: Draft(), + isReplying: false )) } diff --git a/Mail/Views/AI Writer/Proposition/AIPropositionView.swift b/Mail/Views/AI Writer/Proposition/AIPropositionView.swift index 9e21398ad..551b65ea0 100644 --- a/Mail/Views/AI Writer/Proposition/AIPropositionView.swift +++ b/Mail/Views/AI Writer/Proposition/AIPropositionView.swift @@ -153,6 +153,7 @@ struct AIPropositionView: View { AIPropositionView(aiModel: AIModel( mailboxManager: PreviewHelper.sampleMailboxManager, draftContentManager: PreviewHelper.sampleDraftContentManager, - editedDraft: .new() + draft: Draft(), + isReplying: false )) } diff --git a/Mail/Views/Bottom sheets/Actions/ContactActionsView/ContactActionView.swift b/Mail/Views/Bottom sheets/Actions/ContactActionsView/ContactActionView.swift index a0c0bca98..0e26b2352 100644 --- a/Mail/Views/Bottom sheets/Actions/ContactActionsView/ContactActionView.swift +++ b/Mail/Views/Bottom sheets/Actions/ContactActionsView/ContactActionView.swift @@ -61,7 +61,7 @@ struct ContactActionView: View { private func writeEmail() { dismiss() - mainViewState.editedDraft = EditedDraft.writing(to: recipient) + mainViewState.composeMessageIntent = .writeTo(recipient: recipient, originMailboxManager: mailboxManager) } private func addToContacts() { diff --git a/Mail/Views/New Message/ComposeMessageBodyView.swift b/Mail/Views/New Message/ComposeMessageBodyView.swift index f320ca5ae..bb860587c 100644 --- a/Mail/Views/New Message/ComposeMessageBodyView.swift +++ b/Mail/Views/New Message/ComposeMessageBodyView.swift @@ -39,7 +39,7 @@ struct ComposeMessageBodyView: View { let messageReply: MessageReply? private var isRemoteContentBlocked: Bool { - return UserDefaults.shared.displayExternalContent == .askMe && messageReply?.message.localSafeDisplay == false + return UserDefaults.shared.displayExternalContent == .askMe && messageReply?.frozenMessage.localSafeDisplay == false } var body: some View { diff --git a/Mail/Views/New Message/ComposeMessageIntentView.swift b/Mail/Views/New Message/ComposeMessageIntentView.swift new file mode 100644 index 000000000..b8e861305 --- /dev/null +++ b/Mail/Views/New Message/ComposeMessageIntentView.swift @@ -0,0 +1,114 @@ +/* + 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 InfomaniakDI +import MailCore +import NavigationBackport +import RealmSwift +import SwiftUI + +struct ComposeMessageIntentView: View { + @LazyInjectService private var accountManager: AccountManager + @LazyInjectService private var snackbarPresenter: SnackBarPresentable + + @Environment(\.dismiss) private var dismiss + + @State private var draft: Draft? + @State private var mailboxManager: MailboxManager? + @State private var messageReply: MessageReply? + + let composeMessageIntent: ComposeMessageIntent + + var body: some View { + NBNavigationStack { + if let draft, + let mailboxManager { + ComposeMessageView(draft: draft, mailboxManager: mailboxManager, messageReply: messageReply) + } else { + ProgressView() + .progressViewStyle(.circular) + } + } + .interactiveDismissDisabled() + .task(id: composeMessageIntent) { + await initFromIntent() + } + } + + func initFromIntent() async { + guard let mailboxManager = accountManager.getMailboxManager( + for: composeMessageIntent.mailboxId, + userId: composeMessageIntent.userId + ) else { + dismiss() + snackbarPresenter.show(message: MailError.unknownError.errorDescription ?? "") + return + } + + var draftToWrite: Draft? + switch composeMessageIntent.type { + case .new: + draftToWrite = Draft(localUUID: UUID().uuidString) + case .existing(let existingDraftLocalUUID): + draftToWrite = mailboxManager.draft(localUuid: existingDraftLocalUUID) + case .existingRemote(let messageUid): + draftToWrite = Draft(messageUid: messageUid) + case .mailTo(let mailToURLComponents): + draftToWrite = Draft.mailTo(urlComponents: mailToURLComponents) + case .writeTo(let recipient): + draftToWrite = Draft.writing(to: recipient) + case .reply(let messageUid, let replyMode): + if let frozenMessage = mailboxManager.getRealm().object(ofType: Message.self, forPrimaryKey: messageUid)?.freeze() { + let messageReply = MessageReply(frozenMessage: frozenMessage, replyMode: replyMode) + self.messageReply = messageReply + draftToWrite = Draft.replying( + reply: messageReply, + currentMailboxEmail: mailboxManager.mailbox.email + ) + } + } + + if let draftToWrite { + let draftLocalUUID = draftToWrite.localUUID + writeDraftToRealm(mailboxManager.getRealm(), draft: draftToWrite) + + Task { @MainActor in + draft = mailboxManager.draft(localUuid: draftLocalUUID) + self.mailboxManager = mailboxManager + } + } else { + dismiss() + snackbarPresenter.show(message: MailError.localMessageNotFound.errorDescription ?? "") + } + } + + func writeDraftToRealm(_ realm: Realm, draft: Draft) { + try? realm.write { + draft.action = draft.action == nil && draft.remoteUUID.isEmpty ? .initialSave : .save + draft.delay = UserDefaults.shared.cancelSendDelay.rawValue + + realm.add(draft, update: .modified) + } + } +} + +#Preview { + ComposeMessageIntentView( + composeMessageIntent: .new(originMailboxManager: PreviewHelper.sampleMailboxManager) + ) +} diff --git a/Mail/Views/New Message/ComposeMessageView.swift b/Mail/Views/New Message/ComposeMessageView.swift index 83419b527..3d4e4ecdc 100644 --- a/Mail/Views/New Message/ComposeMessageView.swift +++ b/Mail/Views/New Message/ComposeMessageView.swift @@ -88,7 +88,7 @@ struct ComposeMessageView: View { @StateObject private var alert = NewMessageAlert() @StateObject private var aiModel: AIModel - @StateRealmObject private var draft: Draft + @ObservedRealmObject private var draft: Draft @FocusState private var focusedField: ComposeViewFieldType? { willSet { @@ -111,112 +111,108 @@ struct ComposeMessageView: View { // MARK: - Init - init(editedDraft: EditedDraft, mailboxManager: MailboxManager, attachments: [Attachable] = []) { - messageReply = editedDraft.messageReply + init(draft: Draft, mailboxManager: MailboxManager, messageReply: MessageReply? = nil, attachments: [Attachable] = []) { + self.messageReply = messageReply - Self.writeDraftToRealm(mailboxManager.getRealm(), draft: editedDraft.detachedDraft) - _draft = StateRealmObject(wrappedValue: editedDraft.detachedDraft) + _draft = ObservedRealmObject(wrappedValue: draft) let currentDraftContentManager = DraftContentManager( - incompleteDraft: editedDraft.detachedDraft, - messageReply: editedDraft.messageReply, + incompleteDraft: draft, + messageReply: messageReply, mailboxManager: mailboxManager ) draftContentManager = currentDraftContentManager self.mailboxManager = mailboxManager - _attachmentsManager = StateObject(wrappedValue: AttachmentsManager(draftLocalUUID: editedDraft.detachedDraft.localUUID, + _attachmentsManager = StateObject(wrappedValue: AttachmentsManager(draftLocalUUID: draft.localUUID, mailboxManager: mailboxManager)) _initialAttachments = State(wrappedValue: attachments) _aiModel = StateObject(wrappedValue: AIModel( mailboxManager: mailboxManager, draftContentManager: currentDraftContentManager, - editedDraft: editedDraft + draft: draft, + isReplying: messageReply?.isReplying == true )) } // MARK: - View var body: some View { - NavigationView { - composeMessage - } - .navigationViewStyle(.stack) - .task { - do { - isLoadingContent = true - currentSignature = try await draftContentManager.prepareCompleteDraft() - await attachmentsManager.completeUploadedAttachments() - isLoadingContent = false - } catch { - // Unable to get signatures, "An error occurred" and close modal. - snackbarPresenter.show(message: MailError.unknownError.errorDescription ?? "") - dismissMessageView() + composeMessage + .task { + do { + isLoadingContent = true + currentSignature = try await draftContentManager.prepareCompleteDraft() + await attachmentsManager.completeUploadedAttachments() + isLoadingContent = false + } catch { + // Unable to get signatures, "An error occurred" and close modal. + snackbarPresenter.show(message: MailError.unknownError.errorDescription ?? "") + dismissMessageView() + } } - } - .onAppear { - attachmentsManager.importAttachments( - attachments: initialAttachments, - draft: draft, - disposition: AttachmentDisposition.defaultDisposition - ) - initialAttachments = [] - - if featureFlagsManager.isEnabled(.aiMailComposer) && UserDefaults.shared.shouldPresentAIFeature { - isShowingAIPopover = true - return + .onAppear { + attachmentsManager.importAttachments( + attachments: initialAttachments, + draft: draft, + disposition: AttachmentDisposition.defaultDisposition + ) + initialAttachments = [] + + if featureFlagsManager.isEnabled(.aiMailComposer) && UserDefaults.shared.shouldPresentAIFeature { + isShowingAIPopover = true + return + } + + switch messageReply?.replyMode { + case .reply, .replyAll: + focusedField = .editor + default: + focusedField = .to + } } + .onDisappear { + var shouldShowSnackbar = false + if !Bundle.main.isExtension && !mainViewState.isShowingSetAppAsDefaultDiscovery { + shouldShowSnackbar = !mainViewState.isShowingSetAppAsDefaultDiscovery + mainViewState.isShowingReviewAlert = reviewManager.shouldRequestReview() + } - switch messageReply?.replyMode { - case .reply, .replyAll: - focusedField = .editor - default: - focusedField = .to + draftManager.syncDraft(mailboxManager: mailboxManager, showSnackbar: shouldShowSnackbar) } - } - .onDisappear { - var shouldShowSnackbar = false - if !Bundle.main.isExtension && !mainViewState.isShowingSetAppAsDefaultDiscovery { - shouldShowSnackbar = !mainViewState.isShowingSetAppAsDefaultDiscovery - mainViewState.isShowingReviewAlert = reviewManager.shouldRequestReview() + .customAlert(isPresented: $alert.isShowing) { + switch alert.state { + case .link(let handler): + AddLinkView(actionHandler: handler) + case .emptySubject(let handler): + EmptySubjectView(actionHandler: handler) + case .externalRecipient(let state): + ExternalRecipientView(externalTagSate: state, isDraft: true) + case .none: + EmptyView() + } } - - draftManager.syncDraft(mailboxManager: mailboxManager, showSnackbar: shouldShowSnackbar) - } - .interactiveDismissDisabled() - .customAlert(isPresented: $alert.isShowing) { - switch alert.state { - case .link(let handler): - AddLinkView(actionHandler: handler) - case .emptySubject(let handler): - EmptySubjectView(actionHandler: handler) - case .externalRecipient(let state): - ExternalRecipientView(externalTagSate: state, isDraft: true) - case .none: - EmptyView() + .customAlert(isPresented: $isShowingCancelAttachmentsError) { + AttachmentsUploadInProgressErrorView { + dismissMessageView() + } } - } - .customAlert(isPresented: $isShowingCancelAttachmentsError) { - AttachmentsUploadInProgressErrorView { - dismissMessageView() + .discoveryPresenter(isPresented: $isShowingAIPopover) { + DiscoveryView(item: .aiDiscovery) { + UserDefaults.shared.shouldPresentAIFeature = false + } completionHandler: { willShowAIPrompt in + aiModel.isShowingPrompt = willShowAIPrompt + } } - } - .discoveryPresenter(isPresented: $isShowingAIPopover) { - DiscoveryView(item: .aiDiscovery) { - UserDefaults.shared.shouldPresentAIFeature = false - } completionHandler: { willShowAIPrompt in - aiModel.isShowingPrompt = willShowAIPrompt + .aiPromptPresenter(isPresented: $aiModel.isShowingPrompt) { + AIPromptView(aiModel: aiModel) } - } - .aiPromptPresenter(isPresented: $aiModel.isShowingPrompt) { - AIPromptView(aiModel: aiModel) - } - .sheet(isPresented: $aiModel.isShowingProposition) { - AIPropositionView(aiModel: aiModel) - } - .environmentObject(draftContentManager) - .matomoView(view: ["ComposeMessage"]) + .sheet(isPresented: $aiModel.isShowingProposition) { + AIPropositionView(aiModel: aiModel) + } + .environmentObject(draftContentManager) + .matomoView(view: ["ComposeMessage"]) } /// Compose message view @@ -376,20 +372,11 @@ struct ComposeMessageView: View { } dismissMessageView() } - - private static func writeDraftToRealm(_ realm: Realm, draft: Draft) { - try? realm.write { - draft.action = draft.action == nil && draft.remoteUUID.isEmpty ? .initialSave : .save - draft.delay = UserDefaults.shared.cancelSendDelay.rawValue - - realm.add(draft, update: .modified) - } - } } #Preview { ComposeMessageView( - editedDraft: EditedDraft.new(), + draft: Draft(), mailboxManager: PreviewHelper.sampleMailboxManager ) } diff --git a/Mail/Views/Search/SearchThreadsSectionView.swift b/Mail/Views/Search/SearchThreadsSectionView.swift index 34a6dd36d..d1f896ea9 100644 --- a/Mail/Views/Search/SearchThreadsSectionView.swift +++ b/Mail/Views/Search/SearchThreadsSectionView.swift @@ -69,7 +69,7 @@ struct SearchThreadsSectionView: View { DraftUtils.editDraft( from: thread, mailboxManager: viewModel.mailboxManager, - editedDraft: $mainViewState.editedDraft + composeMessageIntent: $mainViewState.composeMessageIntent ) } else { splitViewManager.adaptToProminentThreadView() diff --git a/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift index 216bb3644..f2582d01e 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -149,8 +149,8 @@ struct SplitView: View { .sheet(item: $mainViewState.settingsViewConfig) { config in SettingsNavigationView(baseNavigationPath: config.baseNavigationPath) } - .sheet(item: $mainViewState.editedDraft) { editedDraft in - ComposeMessageView(editedDraft: editedDraft, mailboxManager: mailboxManager) + .sheet(item: $mainViewState.composeMessageIntent) { intent in + ComposeMessageIntentView(composeMessageIntent: intent) } .onChange(of: scenePhase) { newScenePhase in guard newScenePhase == .active else { return } @@ -262,7 +262,7 @@ struct SplitView: View { guard let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return } if Constants.isMailTo(url) { - mainViewState.editedDraft = EditedDraft.mailTo(urlComponents: urlComponents) + mainViewState.composeMessageIntent = .mailTo(mailToURLComponents: urlComponents, originMailboxManager: mailboxManager) } } @@ -294,9 +294,10 @@ struct SplitView: View { } } else if notification.name == .onUserTappedReplyToNotification { if let tappedNotificationMessage { - mainViewState.editedDraft = EditedDraft.replying( - reply: MessageReply(message: tappedNotificationMessage, replyMode: .reply), - currentMailboxEmail: mailboxManager.mailbox.email + mainViewState.composeMessageIntent = .replyingTo( + message: tappedNotificationMessage, + replyMode: .reply, + originMailboxManager: mailboxManager ) } else { snackbarPresenter.show(message: MailError.localMessageNotFound.errorDescription ?? "") diff --git a/Mail/Views/Thread List/ThreadListCell.swift b/Mail/Views/Thread List/ThreadListCell.swift index 25eba86f0..4dba6000c 100644 --- a/Mail/Views/Thread List/ThreadListCell.swift +++ b/Mail/Views/Thread List/ThreadListCell.swift @@ -102,7 +102,7 @@ struct ThreadListCell: View { DraftUtils.editDraft( from: thread, mailboxManager: viewModel.mailboxManager, - editedDraft: $mainViewState.editedDraft + composeMessageIntent: $mainViewState.composeMessageIntent ) } else { splitViewManager.adaptToProminentThreadView() diff --git a/Mail/Views/Thread List/ThreadListView.swift b/Mail/Views/Thread List/ThreadListView.swift index 417060542..c44de0ee2 100644 --- a/Mail/Views/Thread List/ThreadListView.swift +++ b/Mail/Views/Thread List/ThreadListView.swift @@ -168,7 +168,7 @@ struct ThreadListView: View { title: MailResourcesStrings.Localizable.buttonNewMessage, isExtended: scrollObserver.scrollDirection != .bottom) { matomo.track(eventWithCategory: .newMessage, name: "openFromFab") - mainViewState.editedDraft = EditedDraft.new() + mainViewState.composeMessageIntent = .new(originMailboxManager: viewModel.mailboxManager) } .shortcutModifier(viewModel: viewModel, multipleSelectionViewModel: multipleSelectionViewModel) .onAppear { diff --git a/Mail/Views/Thread/Message/MessageHeader/MessageHeaderSummaryView.swift b/Mail/Views/Thread/Message/MessageHeader/MessageHeaderSummaryView.swift index 99d35d097..0809c9a5f 100644 --- a/Mail/Views/Thread/Message/MessageHeader/MessageHeaderSummaryView.swift +++ b/Mail/Views/Thread/Message/MessageHeader/MessageHeaderSummaryView.swift @@ -136,9 +136,10 @@ struct MessageHeaderSummaryView: View { if message.canReplyAll(currentMailboxEmail: mailboxManager.mailbox.email) { replyOrReplyAllMessage = message } else { - mainViewState.editedDraft = EditedDraft.replying( - reply: MessageReply(message: message, replyMode: .reply), - currentMailboxEmail: mailboxManager.mailbox.email + mainViewState.composeMessageIntent = .replyingTo( + message: message, + replyMode: .reply, + originMailboxManager: mailboxManager ) } } label: { diff --git a/Mail/Views/Thread/Message/MessageHeader/MessageHeaderView.swift b/Mail/Views/Thread/Message/MessageHeader/MessageHeaderView.swift index 054289bf0..5c89bd1e0 100644 --- a/Mail/Views/Thread/Message/MessageHeader/MessageHeaderView.swift +++ b/Mail/Views/Thread/Message/MessageHeader/MessageHeaderView.swift @@ -53,14 +53,7 @@ struct MessageHeaderView: View { DraftUtils.editDraft( from: message, mailboxManager: mailboxManager, - editedDraft: $mainViewState.editedDraft - ) - matomo.track(eventWithCategory: .newMessage, name: "openFromDraft") - matomo.track( - eventWithCategory: .newMessage, - action: .data, - name: "openLocalDraft", - value: !(mainViewState.editedDraft?.detachedDraft.isLoadedRemotely ?? false) + composeMessageIntent: $mainViewState.composeMessageIntent ) } else if message.originalThread?.messages.isEmpty == false { withAnimation { diff --git a/MailCore/Cache/Actions/ActionsManager.swift b/MailCore/Cache/Actions/ActionsManager.swift index 637cffb46..6510c38a0 100644 --- a/MailCore/Cache/Actions/ActionsManager.swift +++ b/MailCore/Cache/Actions/ActionsManager.swift @@ -235,9 +235,10 @@ public class ActionsManager: ObservableObject { } Task { @MainActor in - mainViewState?.editedDraft = EditedDraft.replying( - reply: MessageReply(message: replyingMessage, replyMode: mode), - currentMailboxEmail: mailboxManager.mailbox.email + mainViewState?.composeMessageIntent = .replyingTo( + message: replyingMessage, + replyMode: mode, + originMailboxManager: mailboxManager ) } } diff --git a/MailCore/Cache/DraftContentManager.swift b/MailCore/Cache/DraftContentManager.swift index 0fbd368b1..2dfe44071 100644 --- a/MailCore/Cache/DraftContentManager.swift +++ b/MailCore/Cache/DraftContentManager.swift @@ -63,11 +63,11 @@ extension DraftContentManager { if let messageReply { // New draft created either with reply or forward async let completeDraftReplyingBody = try await loadReplyingMessageAndFormat( - messageReply.message, + messageReply.frozenMessage, replyMode: messageReply.replyMode ) async let replyingAttachments = try await loadReplyingAttachments( - message: messageReply.message, + message: messageReply.frozenMessage, replyMode: messageReply.replyMode ) @@ -371,7 +371,7 @@ extension DraftContentManager { } private func guessMostFittingSignature(userSignatures: [Signature], defaultSignature: Signature) -> Signature { - guard let previousMessage = messageReply?.message else { return defaultSignature } + guard let previousMessage = messageReply?.frozenMessage else { return defaultSignature } let signaturesGroupedByEmail = Dictionary(grouping: userSignatures, by: \.senderEmail) let recipientsFieldsToCheck = [\Message.to, \Message.from, \Message.cc] @@ -445,7 +445,7 @@ extension DraftContentManager { extension DraftContentManager { public func getReplyingBody() async throws -> Body? { guard let messageReply else { return nil } - return try await loadReplyingMessage(messageReply.message, replyMode: messageReply.replyMode).body?.freezeIfNeeded() + return try await loadReplyingMessage(messageReply.frozenMessage, replyMode: messageReply.replyMode).body?.freezeIfNeeded() } private func getLiveDraft() throws -> Draft { diff --git a/MailCore/Cache/MailboxManager/MailboxManageable.swift b/MailCore/Cache/MailboxManager/MailboxManageable.swift index 96b6144fb..a06aa3246 100644 --- a/MailCore/Cache/MailboxManager/MailboxManageable.swift +++ b/MailCore/Cache/MailboxManager/MailboxManageable.swift @@ -24,10 +24,15 @@ public typealias MailboxManageable = MailboxManagerCalendareable & MailboxManagerContactable & MailboxManagerDraftable & MailboxManagerFolderable + & MailboxManagerMailboxable & MailboxManagerMessageable & MailboxManagerSearchable & RealmAccessible +public protocol MailboxManagerMailboxable { + var mailbox: Mailbox { get } +} + /// An abstract interface on the `MailboxManager` related to messages public protocol MailboxManagerMessageable { func messages(folder: Folder, isRetrying: Bool) async throws diff --git a/MailCore/Cache/MainViewState.swift b/MailCore/Cache/MainViewState.swift index 7e61ec27e..189e2b532 100644 --- a/MailCore/Cache/MainViewState.swift +++ b/MailCore/Cache/MainViewState.swift @@ -23,7 +23,8 @@ public protocol SelectedThreadOwnable { } public class MainViewState: ObservableObject, SelectedThreadOwnable { - @Published public var editedDraft: EditedDraft? + @Published public var composeMessageIntent: ComposeMessageIntent? + @Published public var settingsViewConfig: SettingsViewConfig? @Published public var isShowingSearch = false diff --git a/MailCore/Models/ComposeMessageIntent.swift b/MailCore/Models/ComposeMessageIntent.swift new file mode 100644 index 000000000..6deba6ac9 --- /dev/null +++ b/MailCore/Models/ComposeMessageIntent.swift @@ -0,0 +1,92 @@ +/* + 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 + +public struct ComposeMessageIntent: Codable, Identifiable, Hashable { + public enum IntentType: Codable, Hashable { + case new + case existing(draftLocalUUID: String) + case existingRemote(messageUid: String) + case mailTo(mailToURLComponents: URLComponents) + case writeTo(recipient: Recipient) + case reply(messageUid: String, replyMode: ReplyMode) + } + + public let id: UUID + public let userId: Int + public let mailboxId: Int + public let type: IntentType + + init(userId: Int, mailboxId: Int, type: IntentType) { + id = UUID() + self.userId = userId + self.mailboxId = mailboxId + self.type = type + } + + public static func new(originMailboxManager: MailboxManager) -> ComposeMessageIntent { + return ComposeMessageIntent( + userId: originMailboxManager.mailbox.userId, + mailboxId: originMailboxManager.mailbox.mailboxId, + type: .new + ) + } + + public static func existing(draft: Draft, originMailboxManager: MailboxManageable) -> ComposeMessageIntent { + return ComposeMessageIntent( + userId: originMailboxManager.mailbox.userId, + mailboxId: originMailboxManager.mailbox.mailboxId, + type: .existing(draftLocalUUID: draft.localUUID) + ) + } + + public static func existingRemote(messageUid: String, originMailboxManager: MailboxManageable) -> ComposeMessageIntent { + return ComposeMessageIntent( + userId: originMailboxManager.mailbox.userId, + mailboxId: originMailboxManager.mailbox.mailboxId, + type: .existingRemote(messageUid: messageUid) + ) + } + + public static func mailTo(mailToURLComponents: URLComponents, originMailboxManager: MailboxManager) -> ComposeMessageIntent { + return ComposeMessageIntent( + userId: originMailboxManager.mailbox.userId, + mailboxId: originMailboxManager.mailbox.mailboxId, + type: .mailTo(mailToURLComponents: mailToURLComponents) + ) + } + + public static func writeTo(recipient: Recipient, originMailboxManager: MailboxManager) -> ComposeMessageIntent { + return ComposeMessageIntent( + userId: originMailboxManager.mailbox.userId, + mailboxId: originMailboxManager.mailbox.mailboxId, + type: .writeTo(recipient: recipient) + ) + } + + public static func replyingTo(message: Message, + replyMode: ReplyMode, + originMailboxManager: MailboxManager) -> ComposeMessageIntent { + return ComposeMessageIntent( + userId: originMailboxManager.mailbox.userId, + mailboxId: originMailboxManager.mailbox.mailboxId, + type: .reply(messageUid: message.uid, replyMode: replyMode) + ) + } +} diff --git a/MailCore/Models/Draft.swift b/MailCore/Models/Draft.swift index 35fc3bd41..81863cb65 100644 --- a/MailCore/Models/Draft.swift +++ b/MailCore/Models/Draft.swift @@ -45,7 +45,7 @@ public enum SaveDraftOption: String, Codable, PersistableEnum { } } -public enum ReplyMode: Equatable { +public enum ReplyMode: Codable, Hashable, Equatable { case reply, replyAll case forward @@ -226,7 +226,7 @@ public final class Draft: Object, Codable, Identifiable { } public static func replying(reply: MessageReply, currentMailboxEmail: String) -> Draft { - let message = reply.message + let message = reply.frozenMessage let mode = reply.replyMode var subject = "\(message.formattedSubject)" switch mode { @@ -246,7 +246,7 @@ public final class Draft: Object, Codable, Identifiable { recipientHolder = message.recipientsForReplyTo(replyAll: mode == .replyAll, currentMailboxEmail: currentMailboxEmail) } - return Draft(localUUID: reply.localDraftUUID, + return Draft(localUUID: UUID().uuidString, inReplyToUid: mode.isReply ? message.uid : nil, forwardedUid: mode == .forward ? message.uid : nil, references: "\(message.references ?? "") \(message.messageId ?? "")", diff --git a/MailCore/Models/EditedDraft.swift b/MailCore/Models/EditedDraft.swift deleted file mode 100644 index 8ab224de9..000000000 --- a/MailCore/Models/EditedDraft.swift +++ /dev/null @@ -1,55 +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 Foundation - -/// Something to track an edited `Draft` -public struct EditedDraft: Identifiable { - public var id: ObjectIdentifier { - return detachedDraft.id - } - - public let detachedDraft: Draft - - public let messageReply: MessageReply? - - init(draft: Draft, messageReply: MessageReply?) { - detachedDraft = draft.detached() - self.messageReply = messageReply - } - - public static func new() -> EditedDraft { - return EditedDraft(draft: Draft(localUUID: UUID().uuidString), messageReply: nil) - } - - public static func existing(draft: Draft) -> EditedDraft { - return EditedDraft(draft: draft, messageReply: nil) - } - - public static func mailTo(urlComponents: URLComponents) -> EditedDraft { - return EditedDraft(draft: Draft.mailTo(urlComponents: urlComponents), messageReply: nil) - } - - public static func writing(to recipient: Recipient) -> EditedDraft { - return EditedDraft(draft: Draft.writing(to: recipient), messageReply: nil) - } - - public static func replying(reply: MessageReply, currentMailboxEmail: String) -> EditedDraft { - return EditedDraft(draft: Draft.replying(reply: reply, currentMailboxEmail: currentMailboxEmail), messageReply: reply) - } -} diff --git a/MailCore/Models/MessageReply.swift b/MailCore/Models/MessageReply.swift index bbfaa46b1..45d2adfa8 100644 --- a/MailCore/Models/MessageReply.swift +++ b/MailCore/Models/MessageReply.swift @@ -18,22 +18,16 @@ import Foundation -public struct MessageReply: Identifiable { - public var id: String { - return localDraftUUID - } - - public let localDraftUUID: String - public let message: Message +public struct MessageReply { + public let frozenMessage: Message public let replyMode: ReplyMode public var isReplying: Bool { replyMode == .reply || replyMode == .replyAll } - public init(message: Message, replyMode: ReplyMode) { - self.message = message + public init(frozenMessage: Message, replyMode: ReplyMode) { + self.frozenMessage = frozenMessage self.replyMode = replyMode - localDraftUUID = UUID().uuidString } } diff --git a/MailShareExtension/ComposeMessageWrapperView.swift b/MailShareExtension/ComposeMessageWrapperView.swift index 68d40f369..c4405337e 100644 --- a/MailShareExtension/ComposeMessageWrapperView.swift +++ b/MailShareExtension/ComposeMessageWrapperView.swift @@ -43,7 +43,7 @@ struct ComposeMessageWrapperView: View { var body: some View { if let mailboxManager = accountManager.currentMailboxManager { ComposeMessageView( - editedDraft: EditedDraft.existing(draft: draft), + draft: draft, mailboxManager: mailboxManager, attachments: itemProviders ) diff --git a/MailTests/Folders/ITFolderListViewModel.swift b/MailTests/Folders/ITFolderListViewModel.swift index c6e6ebf16..b1e9222db 100644 --- a/MailTests/Folders/ITFolderListViewModel.swift +++ b/MailTests/Folders/ITFolderListViewModel.swift @@ -43,6 +43,8 @@ struct MCKContactManageable_FolderListViewModel: ContactManageable { /// A MailboxManageable used to test the FolderListViewModel struct MCKMailboxManageable_FolderListViewModel: MailboxManageable { + let mailbox = Mailbox() + var contactManager: MailCore.ContactManageable { MCKContactManageable_FolderListViewModel(realmConfiguration: realmConfiguration) } diff --git a/MailTests/Search/ITSearchViewModel.swift b/MailTests/Search/ITSearchViewModel.swift index ff0435679..b20d0104a 100644 --- a/MailTests/Search/ITSearchViewModel.swift +++ b/MailTests/Search/ITSearchViewModel.swift @@ -45,6 +45,7 @@ struct MCKContactManageable_SearchViewModel: ContactManageable { /// A MailboxManageable used to test the SearchViewModel final class MCKMailboxManageable_SearchViewModel: MailboxManageable { + let mailbox = Mailbox() let targetFolder: Folder let realm: Realm let folderGenerator: FolderStructureGenerator