From 06cf460f847f9039b9219cdbd41da8c621c719ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 4 Jul 2023 15:19:36 +0200 Subject: [PATCH] feat(shareExtension): what is displayed with a snackbar in the app is now displayed with a notification while using the share extension feat: abstract application state, so it builds in share extension. feat: abstract the way to present a Snackbar within the app, so it can use a Notification within shareExtension thanks to MessagePresentable --- Mail/Helpers/AppAssembly.swift | 6 + .../Implementation/ApplicationState.swift | 26 ++++ .../New Message/ComposeMessageBodyView.swift | 8 +- MailCore/Cache/DraftManager.swift | 22 ++-- MailCore/Utils/ApplicationStatable.swift | 26 ++++ MailCore/Utils/MessagePresentable.swift | 116 ++++++++++++++++++ .../Proxy/ApplicationState.swift | 26 ++++ 7 files changed, 215 insertions(+), 15 deletions(-) create mode 100644 Mail/Proxy/Implementation/ApplicationState.swift create mode 100644 MailCore/Utils/ApplicationStatable.swift create mode 100644 MailCore/Utils/MessagePresentable.swift create mode 100644 MailShareExtension/Proxy/ApplicationState.swift diff --git a/Mail/Helpers/AppAssembly.swift b/Mail/Helpers/AppAssembly.swift index ae924be87..48970e6f4 100644 --- a/Mail/Helpers/AppAssembly.swift +++ b/Mail/Helpers/AppAssembly.swift @@ -76,6 +76,12 @@ enum ApplicationAssembly { }, Factory(type: SnackBarPresentable.self) { _, _ in SnackBarPresenter() + }, + Factory(type: MessagePresentable.self) { _, _ in + MessagePresenter() + }, + Factory(type: ApplicationStatable.self) { _, _ in + ApplicationState() } ] diff --git a/Mail/Proxy/Implementation/ApplicationState.swift b/Mail/Proxy/Implementation/ApplicationState.swift new file mode 100644 index 000000000..093238df3 --- /dev/null +++ b/Mail/Proxy/Implementation/ApplicationState.swift @@ -0,0 +1,26 @@ +/* + 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 UIKit +import MailCore + +public struct ApplicationState: ApplicationStatable { + public var applicationState: UIApplication.State? { + UIApplication.shared.applicationState + } +} diff --git a/Mail/Views/New Message/ComposeMessageBodyView.swift b/Mail/Views/New Message/ComposeMessageBodyView.swift index bdafe6273..b1de1aa74 100644 --- a/Mail/Views/New Message/ComposeMessageBodyView.swift +++ b/Mail/Views/New Message/ComposeMessageBodyView.swift @@ -23,7 +23,7 @@ import RealmSwift import SwiftUI struct ComposeMessageBodyView: View { - @LazyInjectService private var snackbarPresenter: SnackBarPresentable + @LazyInjectService private var messagePresentable: MessagePresentable @Environment(\.dismissModal) var dismissModal @@ -83,7 +83,7 @@ struct ComposeMessageBodyView: View { setSignature() case .error: // Unable to get signatures, "An error occurred" and close modal. - snackbarPresenter.show(message: MailError.unknownError.localizedDescription) + messagePresentable.show(message: MailError.unknownError.localizedDescription) dismissMessageView() case .progress: break @@ -120,7 +120,7 @@ struct ComposeMessageBodyView: View { isLoadingContent = false } catch { dismissMessageView() - snackbarPresenter.show(message: MailError.unknownError.localizedDescription) + messagePresentable.show(message: MailError.unknownError.localizedDescription) } } @@ -138,7 +138,7 @@ struct ComposeMessageBodyView: View { isLoadingContent = false } catch { dismissMessageView() - snackbarPresenter.show(message: MailError.unknownError.localizedDescription) + messagePresentable.show(message: MailError.unknownError.localizedDescription) } } diff --git a/MailCore/Cache/DraftManager.swift b/MailCore/Cache/DraftManager.swift index f0c78e2ef..7e6c876e4 100644 --- a/MailCore/Cache/DraftManager.swift +++ b/MailCore/Cache/DraftManager.swift @@ -73,7 +73,7 @@ public final class DraftManager { private static let saveExpirationSec = 3 @LazyInjectService private var matomo: MatomoUtils - @LazyInjectService private var snackbarPresenter: SnackBarPresentable + @LazyInjectService private var messagePresentable: MessagePresentable /// Used by DI only public init() { @@ -96,13 +96,13 @@ public final class DraftManager { try await mailboxManager.save(draft: draft) } catch { guard error.shouldDisplay else { return } - snackbarPresenter.show(message: error.localizedDescription) + messagePresentable.show(message: error.localizedDescription) } await draftQueue.endBackgroundTask(uuid: draft.localUUID) } public func send(draft: Draft, mailboxManager: MailboxManager) async -> Date? { - snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarEmailSending) + messagePresentable.show(message: MailResourcesStrings.Localizable.snackbarEmailSending) var sendDate: Date? await draftQueue.cleanQueueElement(uuid: draft.localUUID) @@ -110,10 +110,10 @@ public final class DraftManager { do { let cancelableResponse = try await mailboxManager.send(draft: draft) - snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarEmailSent) + messagePresentable.show(message: MailResourcesStrings.Localizable.snackbarEmailSent) sendDate = cancelableResponse.scheduledDate } catch { - snackbarPresenter.show(message: error.localizedDescription) + messagePresentable.show(message: error.localizedDescription) } await draftQueue.endBackgroundTask(uuid: draft.localUUID) return sendDate @@ -179,11 +179,11 @@ public final class DraftManager { } await saveDraftRemotely(draft: draft, mailboxManager: mailboxManager) - snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarDraftSaved, - action: .init(title: MailResourcesStrings.Localizable.actionDelete) { [weak self] in - self?.matomo.track(eventWithCategory: .snackbar, name: "deleteDraft") - self?.deleteDraftSnackBarAction(draft: draft, mailboxManager: mailboxManager) - }) + let messageAction: MessageAction = (MailResourcesStrings.Localizable.actionDelete, { [weak self] in + self?.matomo.track(eventWithCategory: .snackbar, name: "deleteDraft") + self?.deleteDraftSnackBarAction(draft: draft, mailboxManager: mailboxManager) + }) + messagePresentable.show(message: MailResourcesStrings.Localizable.snackbarDraftSaved, action: messageAction) } private func refreshDraftFolder(latestSendDate: Date?, mailboxManager: MailboxManager) async throws { @@ -209,7 +209,7 @@ public final class DraftManager { await tryOrDisplayError { if let liveDraft = draft.thaw() { try await mailboxManager.delete(draft: liveDraft.freeze()) - snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarDraftDeleted) + messagePresentable.show(message: MailResourcesStrings.Localizable.snackbarDraftDeleted) if let draftFolder = mailboxManager.getFolder(with: .draft)?.freeze() { await mailboxManager.refresh(folder: draftFolder) } diff --git a/MailCore/Utils/ApplicationStatable.swift b/MailCore/Utils/ApplicationStatable.swift new file mode 100644 index 000000000..52651173b --- /dev/null +++ b/MailCore/Utils/ApplicationStatable.swift @@ -0,0 +1,26 @@ +/* + 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 UIKit + +// TODO: Move to CoreUI + +/// Something that reads the application state if available +public protocol ApplicationStatable { + var applicationState: UIApplication.State? { get } +} diff --git a/MailCore/Utils/MessagePresentable.swift b/MailCore/Utils/MessagePresentable.swift new file mode 100644 index 000000000..83e5d4342 --- /dev/null +++ b/MailCore/Utils/MessagePresentable.swift @@ -0,0 +1,116 @@ +/* + 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 CocoaLumberjackSwift +import Foundation +import InfomaniakCore +import InfomaniakCoreUI +import InfomaniakDI +import UserNotifications + +// TODO: Move to CoreUI / use with kDrive + +/// Something that can present a message to the user, abstracted of execution context (App / NSExtension) +/// +/// Will present a snackbar while main app is opened, a local notification in Extension or Background. +public protocol MessagePresentable { + /// Will present a snackbar while main app is opened, a local notification in Extension or Background. + /// - Parameter message: The message to display + func show(message: String) + + /// Will present a snackbar while main app is opened, a local notification in Extension or Background. + /// - Parameters: + /// - message: The message to display + /// - action: Title and closure associated with the action + func show(message: String, action: MessageAction) +} + +public typealias MessageAction = (name: String, closure: () -> Void) + +public final class MessagePresenter: MessagePresentable { + @LazyInjectService private var snackbarPresenter: SnackBarPresentable + @LazyInjectService private var applicationState: ApplicationStatable + + /// Used by DI + public init() { + // META: keep sonarcloud happy + } + + // MARK: - MessagePresentable + + public func show(message: String) { + showInContext(message: message, action: nil) + } + + public func show(message: String, action: MessageAction) { + showInContext(message: message, action: action) + } + + // MARK: - private + + private func showInContext(message: String, action: MessageAction?) { + // check not in extension mode + guard !Bundle.main.isExtension else { + presentInLocalNotification(message: message, action: action) + return + } + + // if app not in foreground, we use the local notifications + guard applicationState.applicationState == .active else { + presentInLocalNotification(message: message, action: action) + return + } + + // Present the message as we are in foreground app context + presentInSnackbar(message: message, action: action) + } + + // MARK: Private + + private func presentInSnackbar(message: String, action: MessageAction?) { + guard let action = action else { + snackbarPresenter.show(message: message) + return + } + + let snackBarAction = IKSnackBar.Action(title: action.name, action: action.closure) + snackbarPresenter.show(message: message, action: snackBarAction) + } + + private func presentInLocalNotification(message: String, action: MessageAction?) { + if action != nil { + DDLogError("Action not implemented in notifications for now") + } + + let content = UNMutableNotificationContent() + content.body = message + + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.3, repeats: false) + let uuidString = UUID().uuidString + let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: trigger) + let notificationCenter = UNUserNotificationCenter.current() + notificationCenter.add(request) { error in + DDLogError("MessagePresenter local notification error:\(String(describing: error)) ") + } + + // Self destruct this notification, as used only for user feedback + DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) { + notificationCenter.removeDeliveredNotifications(withIdentifiers: [uuidString]) + } + } +} diff --git a/MailShareExtension/Proxy/ApplicationState.swift b/MailShareExtension/Proxy/ApplicationState.swift new file mode 100644 index 000000000..43c701d41 --- /dev/null +++ b/MailShareExtension/Proxy/ApplicationState.swift @@ -0,0 +1,26 @@ +/* + 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 UIKit +import MailCore + +public struct ApplicationState: ApplicationStatable { + public var applicationState: UIApplication.State? { + nil + } +}