Skip to content

Commit

Permalink
feat(shareExtension): what is displayed with a snackbar in the app is…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
adrien-coye committed Jul 4, 2023
1 parent 417b0f3 commit 06cf460
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 15 deletions.
6 changes: 6 additions & 0 deletions Mail/Helpers/AppAssembly.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ enum ApplicationAssembly {
},
Factory(type: SnackBarPresentable.self) { _, _ in
SnackBarPresenter()
},
Factory(type: MessagePresentable.self) { _, _ in
MessagePresenter()
},
Factory(type: ApplicationStatable.self) { _, _ in
ApplicationState()
}
]

Expand Down
26 changes: 26 additions & 0 deletions Mail/Proxy/Implementation/ApplicationState.swift
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

import UIKit
import MailCore

public struct ApplicationState: ApplicationStatable {
public var applicationState: UIApplication.State? {
UIApplication.shared.applicationState
}
}
8 changes: 4 additions & 4 deletions Mail/Views/New Message/ComposeMessageBodyView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -120,7 +120,7 @@ struct ComposeMessageBodyView: View {
isLoadingContent = false
} catch {
dismissMessageView()
snackbarPresenter.show(message: MailError.unknownError.localizedDescription)
messagePresentable.show(message: MailError.unknownError.localizedDescription)
}
}

Expand All @@ -138,7 +138,7 @@ struct ComposeMessageBodyView: View {
isLoadingContent = false
} catch {
dismissMessageView()
snackbarPresenter.show(message: MailError.unknownError.localizedDescription)
messagePresentable.show(message: MailError.unknownError.localizedDescription)
}
}

Expand Down
22 changes: 11 additions & 11 deletions MailCore/Cache/DraftManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -96,24 +96,24 @@ 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)
await draftQueue.beginBackgroundTask(withName: "Draft Sender", for: draft.localUUID)

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
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
Expand Down
26 changes: 26 additions & 0 deletions MailCore/Utils/ApplicationStatable.swift
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

import UIKit

// TODO: Move to CoreUI

/// Something that reads the application state if available
public protocol ApplicationStatable {
var applicationState: UIApplication.State? { get }
}
116 changes: 116 additions & 0 deletions MailCore/Utils/MessagePresentable.swift
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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])
}
}
}
26 changes: 26 additions & 0 deletions MailShareExtension/Proxy/ApplicationState.swift
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

import UIKit
import MailCore

public struct ApplicationState: ApplicationStatable {
public var applicationState: UIApplication.State? {
nil
}
}

0 comments on commit 06cf460

Please sign in to comment.