From 7438ffee380007a9ee18582997e50fc2a27a6e6d Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Tue, 30 Jan 2024 14:27:11 +0100 Subject: [PATCH 1/9] feat: ComposeMessageIntent handle basic cases --- Mail/Utils/DraftUtils.swift | 23 ++- Mail/Utils/ShortcutModifier.swift | 2 +- Mail/Views/AI Writer/AIModel.swift | 6 +- .../Views/AI Writer/Prompt/AIPromptView.swift | 2 +- .../Proposition/AIPropositionMenu.swift | 2 +- .../Proposition/AIPropositionView.swift | 2 +- .../ContactActionView.swift | 2 +- .../ComposeMessageIntentView.swift | 103 +++++++++++ .../New Message/ComposeMessageView.swift | 166 ++++++++---------- .../Search/SearchThreadsSectionView.swift | 2 +- Mail/Views/SplitView.swift | 5 +- Mail/Views/Thread List/ThreadListCell.swift | 2 +- Mail/Views/Thread List/ThreadListView.swift | 2 +- .../MessageHeader/MessageHeaderView.swift | 2 +- .../MailboxManager/MailboxManageable.swift | 5 + MailCore/Cache/MainViewState.swift | 2 + MailCore/Models/ComposeMessageIntent.swift | 71 ++++++++ MailCore/Models/EditedDraft.swift | 16 -- .../ComposeMessageWrapperView.swift | 2 +- 19 files changed, 293 insertions(+), 124 deletions(-) create mode 100644 Mail/Views/New Message/ComposeMessageIntentView.swift create mode 100644 MailCore/Models/ComposeMessageIntent.swift diff --git a/Mail/Utils/DraftUtils.swift b/Mail/Utils/DraftUtils.swift index a5b4c706d..3da28f281 100644 --- a/Mail/Utils/DraftUtils.swift +++ b/Mail/Utils/DraftUtils.swift @@ -22,23 +22,34 @@ 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) + 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) + 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)) + composeMessageIntent.wrappedValue = ComposeMessageIntent.existing( + draft: Draft(messageUid: message.uid), + originMailboxManager: mailboxManager + ) } } } 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..6606d2b72 100644 --- a/Mail/Views/AI Writer/AIModel.swift +++ b/Mail/Views/AI Writer/AIModel.swift @@ -76,11 +76,11 @@ final class AIModel: ObservableObject { messageReply?.isReplying == true } - init(mailboxManager: MailboxManager, draftContentManager: DraftContentManager, editedDraft: EditedDraft) { + init(mailboxManager: MailboxManager, draftContentManager: DraftContentManager, draft: Draft) { self.mailboxManager = mailboxManager self.draftContentManager = draftContentManager - draft = editedDraft.detachedDraft - messageReply = editedDraft.messageReply + self.draft = draft + messageReply = nil // editedDraft.messageReply } } diff --git a/Mail/Views/AI Writer/Prompt/AIPromptView.swift b/Mail/Views/AI Writer/Prompt/AIPromptView.swift index ad791a66f..f1d9e12bb 100644 --- a/Mail/Views/AI Writer/Prompt/AIPromptView.swift +++ b/Mail/Views/AI Writer/Prompt/AIPromptView.swift @@ -127,6 +127,6 @@ struct AIPromptView: View { AIPromptView(aiModel: AIModel( mailboxManager: PreviewHelper.sampleMailboxManager, draftContentManager: PreviewHelper.sampleDraftContentManager, - editedDraft: EditedDraft.new() + draft: Draft() )) } diff --git a/Mail/Views/AI Writer/Proposition/AIPropositionMenu.swift b/Mail/Views/AI Writer/Proposition/AIPropositionMenu.swift index 53de7c519..22166d698 100644 --- a/Mail/Views/AI Writer/Proposition/AIPropositionMenu.swift +++ b/Mail/Views/AI Writer/Proposition/AIPropositionMenu.swift @@ -88,6 +88,6 @@ struct FixedMenuOrderModifier: ViewModifier { AIPropositionMenu(aiModel: AIModel( mailboxManager: PreviewHelper.sampleMailboxManager, draftContentManager: PreviewHelper.sampleDraftContentManager, - editedDraft: .new() + draft: Draft() )) } diff --git a/Mail/Views/AI Writer/Proposition/AIPropositionView.swift b/Mail/Views/AI Writer/Proposition/AIPropositionView.swift index 9e21398ad..905e53948 100644 --- a/Mail/Views/AI Writer/Proposition/AIPropositionView.swift +++ b/Mail/Views/AI Writer/Proposition/AIPropositionView.swift @@ -153,6 +153,6 @@ struct AIPropositionView: View { AIPropositionView(aiModel: AIModel( mailboxManager: PreviewHelper.sampleMailboxManager, draftContentManager: PreviewHelper.sampleDraftContentManager, - editedDraft: .new() + draft: Draft() )) } 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/ComposeMessageIntentView.swift b/Mail/Views/New Message/ComposeMessageIntentView.swift new file mode 100644 index 000000000..c3d55d296 --- /dev/null +++ b/Mail/Views/New Message/ComposeMessageIntentView.swift @@ -0,0 +1,103 @@ +/* + 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 RealmSwift +import SwiftUI + +struct ComposeMessageIntentView: View { + @LazyInjectService private var accountManager: AccountManager + + let composeMessageIntent: ComposeMessageIntent + + @State private var draft: Draft? + @State private var mailboxManager: MailboxManager? + + var body: some View { + NavigationView { + if let draft, + let mailboxManager { + ComposeMessageView(draft: draft, mailboxManager: mailboxManager) + } else { + ProgressView() + .progressViewStyle(.circular) + } + } + .navigationViewStyle(.stack) + .interactiveDismissDisabled() + .task(id: composeMessageIntent) { + await initFromIntent() + } + } + + func initFromIntent() async { + guard let mailboxManager = accountManager.getMailboxManager( + for: composeMessageIntent.mailboxId, + userId: composeMessageIntent.userId + ) else { + return + } + + var draftLocalUUID: String? + var newDraft: Draft? + switch composeMessageIntent.type { + case .new: + newDraft = Draft(localUUID: UUID().uuidString) + case .existing(let existingDraftLocalUUID): + draftLocalUUID = existingDraftLocalUUID + case .mailTo(let mailToURLComponents): + newDraft = Draft.mailTo(urlComponents: mailToURLComponents) + case .writeTo(let recipient): + newDraft = Draft.writing(to: recipient) + case .reply: + break + case .replyAll: + break + case .forward: + break + } + + if let newDraft { + draftLocalUUID = newDraft.localUUID + writeDraftToRealm(mailboxManager.getRealm(), draft: newDraft) + } + + guard let draftLocalUUID else { return } + + Task { @MainActor in + draft = mailboxManager.draft(localUuid: draftLocalUUID) + self.mailboxManager = mailboxManager + } + } + + 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..eff2df5f9 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 { @@ -112,111 +112,110 @@ struct ComposeMessageView: View { // MARK: - Init init(editedDraft: EditedDraft, mailboxManager: MailboxManager, attachments: [Attachable] = []) { - messageReply = editedDraft.messageReply + fatalError() + } + + init(draft: Draft, mailboxManager: MailboxManager, attachments: [Attachable] = []) { + messageReply = nil // editedDraft.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: nil, 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 )) } // 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 +375,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..a1ccbe435 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -152,6 +152,9 @@ struct SplitView: View { .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 } Task { @@ -262,7 +265,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) } } 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/MessageHeaderView.swift b/Mail/Views/Thread/Message/MessageHeader/MessageHeaderView.swift index 054289bf0..0f33f94f0 100644 --- a/Mail/Views/Thread/Message/MessageHeader/MessageHeaderView.swift +++ b/Mail/Views/Thread/Message/MessageHeader/MessageHeaderView.swift @@ -53,7 +53,7 @@ struct MessageHeaderView: View { DraftUtils.editDraft( from: message, mailboxManager: mailboxManager, - editedDraft: $mainViewState.editedDraft + composeMessageIntent: $mainViewState.composeMessageIntent ) matomo.track(eventWithCategory: .newMessage, name: "openFromDraft") matomo.track( diff --git a/MailCore/Cache/MailboxManager/MailboxManageable.swift b/MailCore/Cache/MailboxManager/MailboxManageable.swift index 96b6144fb..f77bff987 100644 --- a/MailCore/Cache/MailboxManager/MailboxManageable.swift +++ b/MailCore/Cache/MailboxManager/MailboxManageable.swift @@ -26,8 +26,13 @@ public typealias MailboxManageable = MailboxManagerCalendareable & MailboxManagerFolderable & MailboxManagerMessageable & MailboxManagerSearchable + & MailboxManagerMailboxable & 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..b2a108125 100644 --- a/MailCore/Cache/MainViewState.swift +++ b/MailCore/Cache/MainViewState.swift @@ -24,6 +24,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..e1bf72a70 --- /dev/null +++ b/MailCore/Models/ComposeMessageIntent.swift @@ -0,0 +1,71 @@ +/* + 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 var id: Int { + return hashValue + } + + public enum IntentType: Codable, Hashable { + case new + case existing(draftLocalUUID: String) + case mailTo(mailToURLComponents: URLComponents) + case writeTo(recipient: Recipient) + case reply + case replyAll + case forward + } + + public let userId: Int + public let mailboxId: Int + public let type: IntentType + + 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 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) + ) + } +} diff --git a/MailCore/Models/EditedDraft.swift b/MailCore/Models/EditedDraft.swift index 8ab224de9..3eb1dd988 100644 --- a/MailCore/Models/EditedDraft.swift +++ b/MailCore/Models/EditedDraft.swift @@ -33,22 +33,6 @@ public struct EditedDraft: Identifiable { 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/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 ) From 1625fc5ad63fb0623ff9708ddf9df743f26ce9fe Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Wed, 31 Jan 2024 10:22:40 +0100 Subject: [PATCH 2/9] feat: Reply with intent --- Mail/Utils/DraftUtils.swift | 19 ++++++++- .../ComposeMessageIntentView.swift | 18 +++++---- .../New Message/ComposeMessageView.swift | 10 ++--- Mail/Views/SplitView.swift | 10 ++--- .../MessageHeaderSummaryView.swift | 7 ++-- .../MessageHeader/MessageHeaderView.swift | 7 ---- MailCore/Cache/Actions/ActionsManager.swift | 7 ++-- MailCore/Cache/MainViewState.swift | 1 - MailCore/Models/ComposeMessageIntent.swift | 14 +++++-- MailCore/Models/Draft.swift | 2 +- MailCore/Models/EditedDraft.swift | 39 ------------------- 11 files changed, 56 insertions(+), 78 deletions(-) delete mode 100644 MailCore/Models/EditedDraft.swift diff --git a/Mail/Utils/DraftUtils.swift b/Mail/Utils/DraftUtils.swift index 3da28f281..7bb82743b 100644 --- a/Mail/Utils/DraftUtils.swift +++ b/Mail/Utils/DraftUtils.swift @@ -17,6 +17,8 @@ */ import Foundation +import InfomaniakCoreUI +import InfomaniakDI import MailCore import SwiftUI @@ -30,6 +32,7 @@ enum DraftUtils { 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() { + matomoOpenDraft(draft: draft) composeMessageIntent.wrappedValue = ComposeMessageIntent.existing(draft: draft, originMailboxManager: mailboxManager) } else { DraftUtils.editDraft(from: message, mailboxManager: mailboxManager, composeMessageIntent: composeMessageIntent) @@ -43,13 +46,27 @@ enum DraftUtils { ) { // If we already have the draft locally, present it directly if let draft = mailboxManager.draft(messageUid: message.uid, using: nil)?.detached() { + matomoOpenDraft(draft: draft) composeMessageIntent.wrappedValue = ComposeMessageIntent.existing(draft: draft, originMailboxManager: mailboxManager) // Draft comes from API, we will update it after showing the ComposeMessageView } else { + let draft = Draft(messageUid: message.uid) + matomoOpenDraft(draft: draft) composeMessageIntent.wrappedValue = ComposeMessageIntent.existing( - draft: Draft(messageUid: message.uid), + draft: draft, originMailboxManager: mailboxManager ) } } + + private static func matomoOpenDraft(draft: Draft) { + @InjectService var matomo: MatomoUtils + matomo.track(eventWithCategory: .newMessage, name: "openFromDraft") + matomo.track( + eventWithCategory: .newMessage, + action: .data, + name: "openLocalDraft", + value: !draft.isLoadedRemotely + ) + } } diff --git a/Mail/Views/New Message/ComposeMessageIntentView.swift b/Mail/Views/New Message/ComposeMessageIntentView.swift index c3d55d296..b5df29644 100644 --- a/Mail/Views/New Message/ComposeMessageIntentView.swift +++ b/Mail/Views/New Message/ComposeMessageIntentView.swift @@ -28,12 +28,13 @@ struct ComposeMessageIntentView: View { @State private var draft: Draft? @State private var mailboxManager: MailboxManager? + @State private var messageReply: MessageReply? var body: some View { NavigationView { if let draft, let mailboxManager { - ComposeMessageView(draft: draft, mailboxManager: mailboxManager) + ComposeMessageView(draft: draft, mailboxManager: mailboxManager, messageReply: messageReply) } else { ProgressView() .progressViewStyle(.circular) @@ -65,12 +66,15 @@ struct ComposeMessageIntentView: View { newDraft = Draft.mailTo(urlComponents: mailToURLComponents) case .writeTo(let recipient): newDraft = Draft.writing(to: recipient) - case .reply: - break - case .replyAll: - break - case .forward: - break + 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 + newDraft = Draft.replying( + reply: messageReply, + currentMailboxEmail: mailboxManager.mailbox.email + ) + } } if let newDraft { diff --git a/Mail/Views/New Message/ComposeMessageView.swift b/Mail/Views/New Message/ComposeMessageView.swift index eff2df5f9..dc0ea4cf8 100644 --- a/Mail/Views/New Message/ComposeMessageView.swift +++ b/Mail/Views/New Message/ComposeMessageView.swift @@ -111,18 +111,14 @@ struct ComposeMessageView: View { // MARK: - Init - init(editedDraft: EditedDraft, mailboxManager: MailboxManager, attachments: [Attachable] = []) { - fatalError() - } - - init(draft: Draft, mailboxManager: MailboxManager, attachments: [Attachable] = []) { - messageReply = nil // editedDraft.messageReply + init(draft: Draft, mailboxManager: MailboxManager, messageReply: MessageReply? = nil, attachments: [Attachable] = []) { + self.messageReply = messageReply _draft = ObservedRealmObject(wrappedValue: draft) let currentDraftContentManager = DraftContentManager( incompleteDraft: draft, - messageReply: nil, + messageReply: messageReply, mailboxManager: mailboxManager ) draftContentManager = currentDraftContentManager diff --git a/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift index a1ccbe435..f2582d01e 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -149,9 +149,6 @@ 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) } @@ -297,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/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 0f33f94f0..5c89bd1e0 100644 --- a/Mail/Views/Thread/Message/MessageHeader/MessageHeaderView.swift +++ b/Mail/Views/Thread/Message/MessageHeader/MessageHeaderView.swift @@ -55,13 +55,6 @@ struct MessageHeaderView: View { mailboxManager: mailboxManager, composeMessageIntent: $mainViewState.composeMessageIntent ) - matomo.track(eventWithCategory: .newMessage, name: "openFromDraft") - matomo.track( - eventWithCategory: .newMessage, - action: .data, - name: "openLocalDraft", - value: !(mainViewState.editedDraft?.detachedDraft.isLoadedRemotely ?? false) - ) } else if message.originalThread?.messages.isEmpty == false { withAnimation { isHeaderExpanded = false 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/MainViewState.swift b/MailCore/Cache/MainViewState.swift index b2a108125..189e2b532 100644 --- a/MailCore/Cache/MainViewState.swift +++ b/MailCore/Cache/MainViewState.swift @@ -23,7 +23,6 @@ public protocol SelectedThreadOwnable { } public class MainViewState: ObservableObject, SelectedThreadOwnable { - @Published public var editedDraft: EditedDraft? @Published public var composeMessageIntent: ComposeMessageIntent? @Published public var settingsViewConfig: SettingsViewConfig? diff --git a/MailCore/Models/ComposeMessageIntent.swift b/MailCore/Models/ComposeMessageIntent.swift index e1bf72a70..909b91208 100644 --- a/MailCore/Models/ComposeMessageIntent.swift +++ b/MailCore/Models/ComposeMessageIntent.swift @@ -28,9 +28,7 @@ public struct ComposeMessageIntent: Codable, Identifiable, Hashable { case existing(draftLocalUUID: String) case mailTo(mailToURLComponents: URLComponents) case writeTo(recipient: Recipient) - case reply - case replyAll - case forward + case reply(messageUid: String, replyMode: ReplyMode) } public let userId: Int @@ -68,4 +66,14 @@ public struct ComposeMessageIntent: Codable, Identifiable, Hashable { 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..5f7e427b2 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 diff --git a/MailCore/Models/EditedDraft.swift b/MailCore/Models/EditedDraft.swift deleted file mode 100644 index 3eb1dd988..000000000 --- a/MailCore/Models/EditedDraft.swift +++ /dev/null @@ -1,39 +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 replying(reply: MessageReply, currentMailboxEmail: String) -> EditedDraft { - return EditedDraft(draft: Draft.replying(reply: reply, currentMailboxEmail: currentMailboxEmail), messageReply: reply) - } -} From 87703ce7f0117d6aba050624d09f3a7dda19d971 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Wed, 31 Jan 2024 10:23:01 +0100 Subject: [PATCH 3/9] refactor: Explicit frozen --- .../Views/New Message/ComposeMessageBodyView.swift | 2 +- MailCore/Cache/DraftContentManager.swift | 8 ++++---- MailCore/Models/Draft.swift | 4 ++-- MailCore/Models/MessageReply.swift | 14 ++++---------- 4 files changed, 11 insertions(+), 17 deletions(-) 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/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/Models/Draft.swift b/MailCore/Models/Draft.swift index 5f7e427b2..81863cb65 100644 --- a/MailCore/Models/Draft.swift +++ b/MailCore/Models/Draft.swift @@ -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/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 } } From 3d1159d04f54f4a9c24ba388eb8e73f30b5fc3dd Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Wed, 31 Jan 2024 10:38:07 +0100 Subject: [PATCH 4/9] test: Make mock conform to updated protocol --- MailTests/Folders/ITFolderListViewModel.swift | 2 ++ MailTests/Search/ITSearchViewModel.swift | 1 + 2 files changed, 3 insertions(+) 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 From b7d40f8c3a821455ebb692cc6369f51470b2a299 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Wed, 31 Jan 2024 13:50:37 +0100 Subject: [PATCH 5/9] fix: Use NBNavigationStack instead of NavigationView --- Mail/Views/New Message/ComposeMessageIntentView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mail/Views/New Message/ComposeMessageIntentView.swift b/Mail/Views/New Message/ComposeMessageIntentView.swift index b5df29644..f381f9f70 100644 --- a/Mail/Views/New Message/ComposeMessageIntentView.swift +++ b/Mail/Views/New Message/ComposeMessageIntentView.swift @@ -18,6 +18,7 @@ import InfomaniakDI import MailCore +import NavigationBackport import RealmSwift import SwiftUI @@ -31,7 +32,7 @@ struct ComposeMessageIntentView: View { @State private var messageReply: MessageReply? var body: some View { - NavigationView { + NBNavigationStack { if let draft, let mailboxManager { ComposeMessageView(draft: draft, mailboxManager: mailboxManager, messageReply: messageReply) @@ -40,7 +41,6 @@ struct ComposeMessageIntentView: View { .progressViewStyle(.circular) } } - .navigationViewStyle(.stack) .interactiveDismissDisabled() .task(id: composeMessageIntent) { await initFromIntent() From 3e4871c7cf06bbf27a588f755eaac492154a16ec Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Wed, 31 Jan 2024 13:59:00 +0100 Subject: [PATCH 6/9] fix: Use random ID instead of hash to allow multiple empty drafts --- .../Cache/MailboxManager/MailboxManageable.swift | 2 +- MailCore/Models/ComposeMessageIntent.swift | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/MailCore/Cache/MailboxManager/MailboxManageable.swift b/MailCore/Cache/MailboxManager/MailboxManageable.swift index f77bff987..a06aa3246 100644 --- a/MailCore/Cache/MailboxManager/MailboxManageable.swift +++ b/MailCore/Cache/MailboxManager/MailboxManageable.swift @@ -24,9 +24,9 @@ public typealias MailboxManageable = MailboxManagerCalendareable & MailboxManagerContactable & MailboxManagerDraftable & MailboxManagerFolderable + & MailboxManagerMailboxable & MailboxManagerMessageable & MailboxManagerSearchable - & MailboxManagerMailboxable & RealmAccessible public protocol MailboxManagerMailboxable { diff --git a/MailCore/Models/ComposeMessageIntent.swift b/MailCore/Models/ComposeMessageIntent.swift index 909b91208..857d165a7 100644 --- a/MailCore/Models/ComposeMessageIntent.swift +++ b/MailCore/Models/ComposeMessageIntent.swift @@ -19,10 +19,6 @@ import Foundation public struct ComposeMessageIntent: Codable, Identifiable, Hashable { - public var id: Int { - return hashValue - } - public enum IntentType: Codable, Hashable { case new case existing(draftLocalUUID: String) @@ -31,10 +27,18 @@ public struct ComposeMessageIntent: Codable, Identifiable, Hashable { 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, From da1c7295553dab74fed48fcd7c3a9e0799e1b676 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Wed, 31 Jan 2024 14:10:17 +0100 Subject: [PATCH 7/9] refactor: PR feedback --- Mail/Views/New Message/ComposeMessageIntentView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mail/Views/New Message/ComposeMessageIntentView.swift b/Mail/Views/New Message/ComposeMessageIntentView.swift index f381f9f70..b08d5b334 100644 --- a/Mail/Views/New Message/ComposeMessageIntentView.swift +++ b/Mail/Views/New Message/ComposeMessageIntentView.swift @@ -25,12 +25,12 @@ import SwiftUI struct ComposeMessageIntentView: View { @LazyInjectService private var accountManager: AccountManager - let composeMessageIntent: ComposeMessageIntent - @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, From 4631aebdce0b7fcab393d162e802b56df0921432 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Wed, 31 Jan 2024 14:10:43 +0100 Subject: [PATCH 8/9] fix: Add back missing isReplying --- Mail/Views/AI Writer/AIModel.swift | 11 ++++------- Mail/Views/AI Writer/Prompt/AIPromptView.swift | 3 ++- .../AI Writer/Proposition/AIPropositionMenu.swift | 3 ++- .../AI Writer/Proposition/AIPropositionView.swift | 3 ++- Mail/Views/New Message/ComposeMessageView.swift | 3 ++- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Mail/Views/AI Writer/AIModel.swift b/Mail/Views/AI Writer/AIModel.swift index 6606d2b72..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, draft: Draft) { + init(mailboxManager: MailboxManager, draftContentManager: DraftContentManager, draft: Draft, isReplying: Bool) { self.mailboxManager = mailboxManager self.draftContentManager = draftContentManager self.draft = draft - messageReply = nil // editedDraft.messageReply + 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 f1d9e12bb..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, - draft: Draft() + draft: Draft(), + isReplying: false )) } diff --git a/Mail/Views/AI Writer/Proposition/AIPropositionMenu.swift b/Mail/Views/AI Writer/Proposition/AIPropositionMenu.swift index 22166d698..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, - draft: Draft() + draft: Draft(), + isReplying: false )) } diff --git a/Mail/Views/AI Writer/Proposition/AIPropositionView.swift b/Mail/Views/AI Writer/Proposition/AIPropositionView.swift index 905e53948..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, - draft: Draft() + draft: Draft(), + isReplying: false )) } diff --git a/Mail/Views/New Message/ComposeMessageView.swift b/Mail/Views/New Message/ComposeMessageView.swift index dc0ea4cf8..3d4e4ecdc 100644 --- a/Mail/Views/New Message/ComposeMessageView.swift +++ b/Mail/Views/New Message/ComposeMessageView.swift @@ -131,7 +131,8 @@ struct ComposeMessageView: View { _aiModel = StateObject(wrappedValue: AIModel( mailboxManager: mailboxManager, draftContentManager: currentDraftContentManager, - draft: draft + draft: draft, + isReplying: messageReply?.isReplying == true )) } From c141657a79d7ef846a051c9a346af3ec6b83d034 Mon Sep 17 00:00:00 2001 From: Philippe Weidmann Date: Wed, 31 Jan 2024 15:09:07 +0100 Subject: [PATCH 9/9] fix: Handle remote Draft not loaded + Error handling --- Mail/Utils/DraftUtils.swift | 15 ++++--- .../ComposeMessageIntentView.swift | 39 +++++++++++-------- MailCore/Models/ComposeMessageIntent.swift | 9 +++++ 3 files changed, 39 insertions(+), 24 deletions(-) diff --git a/Mail/Utils/DraftUtils.swift b/Mail/Utils/DraftUtils.swift index 7bb82743b..8eff13de6 100644 --- a/Mail/Utils/DraftUtils.swift +++ b/Mail/Utils/DraftUtils.swift @@ -32,7 +32,7 @@ enum DraftUtils { 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() { - matomoOpenDraft(draft: draft) + matomoOpenDraft(isLoadedRemotely: false) composeMessageIntent.wrappedValue = ComposeMessageIntent.existing(draft: draft, originMailboxManager: mailboxManager) } else { DraftUtils.editDraft(from: message, mailboxManager: mailboxManager, composeMessageIntent: composeMessageIntent) @@ -46,27 +46,26 @@ enum DraftUtils { ) { // If we already have the draft locally, present it directly if let draft = mailboxManager.draft(messageUid: message.uid, using: nil)?.detached() { - matomoOpenDraft(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 { - let draft = Draft(messageUid: message.uid) - matomoOpenDraft(draft: draft) - composeMessageIntent.wrappedValue = ComposeMessageIntent.existing( - draft: draft, + matomoOpenDraft(isLoadedRemotely: true) + composeMessageIntent.wrappedValue = ComposeMessageIntent.existingRemote( + messageUid: message.uid, originMailboxManager: mailboxManager ) } } - private static func matomoOpenDraft(draft: Draft) { + 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: !draft.isLoadedRemotely + value: !isLoadedRemotely ) } } diff --git a/Mail/Views/New Message/ComposeMessageIntentView.swift b/Mail/Views/New Message/ComposeMessageIntentView.swift index b08d5b334..b8e861305 100644 --- a/Mail/Views/New Message/ComposeMessageIntentView.swift +++ b/Mail/Views/New Message/ComposeMessageIntentView.swift @@ -24,6 +24,9 @@ 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? @@ -52,41 +55,45 @@ struct ComposeMessageIntentView: View { for: composeMessageIntent.mailboxId, userId: composeMessageIntent.userId ) else { + dismiss() + snackbarPresenter.show(message: MailError.unknownError.errorDescription ?? "") return } - var draftLocalUUID: String? - var newDraft: Draft? + var draftToWrite: Draft? switch composeMessageIntent.type { case .new: - newDraft = Draft(localUUID: UUID().uuidString) + draftToWrite = Draft(localUUID: UUID().uuidString) case .existing(let existingDraftLocalUUID): - draftLocalUUID = existingDraftLocalUUID + draftToWrite = mailboxManager.draft(localUuid: existingDraftLocalUUID) + case .existingRemote(let messageUid): + draftToWrite = Draft(messageUid: messageUid) case .mailTo(let mailToURLComponents): - newDraft = Draft.mailTo(urlComponents: mailToURLComponents) + draftToWrite = Draft.mailTo(urlComponents: mailToURLComponents) case .writeTo(let recipient): - newDraft = Draft.writing(to: 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 - newDraft = Draft.replying( + draftToWrite = Draft.replying( reply: messageReply, currentMailboxEmail: mailboxManager.mailbox.email ) } } - if let newDraft { - draftLocalUUID = newDraft.localUUID - writeDraftToRealm(mailboxManager.getRealm(), draft: newDraft) - } - - guard let draftLocalUUID else { return } + if let draftToWrite { + let draftLocalUUID = draftToWrite.localUUID + writeDraftToRealm(mailboxManager.getRealm(), draft: draftToWrite) - Task { @MainActor in - draft = mailboxManager.draft(localUuid: draftLocalUUID) - self.mailboxManager = mailboxManager + Task { @MainActor in + draft = mailboxManager.draft(localUuid: draftLocalUUID) + self.mailboxManager = mailboxManager + } + } else { + dismiss() + snackbarPresenter.show(message: MailError.localMessageNotFound.errorDescription ?? "") } } diff --git a/MailCore/Models/ComposeMessageIntent.swift b/MailCore/Models/ComposeMessageIntent.swift index 857d165a7..6deba6ac9 100644 --- a/MailCore/Models/ComposeMessageIntent.swift +++ b/MailCore/Models/ComposeMessageIntent.swift @@ -22,6 +22,7 @@ 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) @@ -55,6 +56,14 @@ public struct ComposeMessageIntent: Codable, Identifiable, Hashable { ) } + 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,