diff --git a/.package.resolved b/.package.resolved
index aaa57d82f..9c8e383f2 100644
--- a/.package.resolved
+++ b/.package.resolved
@@ -41,7 +41,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/Infomaniak/ios-core",
"state" : {
- "revision" : "4eaefd644f75d833d6b1009dd94a9d6d674ccb53"
+ "revision" : "d779a9f6619615a4b4a91fa9d3fbb48415a27470"
}
},
{
diff --git a/Mail/AppDelegate.swift b/Mail/AppDelegate.swift
index eb19f6b36..8473f70f9 100644
--- a/Mail/AppDelegate.swift
+++ b/Mail/AppDelegate.swift
@@ -17,19 +17,27 @@
*/
import CocoaLumberjackSwift
+import InfomaniakCore
import InfomaniakDI
import InfomaniakNotifications
import MailCore
import UIKit
-class AppDelegate: UIResponder, UIApplicationDelegate {
+@available(iOSApplicationExtension, unavailable)
+final class AppDelegate: UIResponder, UIApplicationDelegate {
private let notificationCenterDelegate = NotificationCenterDelegate()
- static var orientationLock = UIInterfaceOrientationMask.all
- func application(
- _ application: UIApplication,
- willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
- ) -> Bool {
+ @LazyInjectService private var orientationManager: OrientationManageable
+ @LazyInjectService private var accountManager: AccountManager
+ @LazyInjectService private var applicationState: ApplicationStatable
+
+ /// Making sure the DI is registered at a very early stage of the app launch.
+ private let dependencyInjectionHook = EarlyDIHook()
+
+ func application(_ application: UIApplication,
+ willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
+ DDLogInfo("Application starting in foreground ? \(applicationState.applicationState != .background)")
+
UNUserNotificationCenter.current().delegate = notificationCenterDelegate
Task {
// Ask permission app launch
@@ -44,14 +52,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
@InjectService var notificationService: InfomaniakNotifications
@InjectService var tokenStore: TokenStore
- for account in AccountManager.instance.accounts {
+ for account in accountManager.accounts {
Task {
/* Because of a backend issue we can't register the notification token directly after the creation or refresh of
an API token. We wait at least 15 seconds before trying to register. */
try? await Task.sleep(nanoseconds: 15_000_000_000)
guard let token = tokenStore.tokenFor(userId: account.userId) else { return }
- let userApiFetcher = AccountManager.instance.getApiFetcher(for: token.userId, token: token)
+ let userApiFetcher = accountManager.getApiFetcher(for: token.userId, token: token)
await notificationService.updateRemoteNotificationsToken(tokenData: deviceToken,
userApiFetcher: userApiFetcher,
updatePolicy: .always)
@@ -65,6 +73,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
- return AppDelegate.orientationLock
+ return orientationManager.orientationLock
}
}
diff --git a/Mail/Components/UnavailableMailboxListView.swift b/Mail/Components/UnavailableMailboxListView.swift
index 0108a2ab1..38fb3d1b7 100644
--- a/Mail/Components/UnavailableMailboxListView.swift
+++ b/Mail/Components/UnavailableMailboxListView.swift
@@ -16,6 +16,7 @@
along with this program. If not, see .
*/
+import InfomaniakDI
import MailCore
import MailResources
import RealmSwift
@@ -27,14 +28,20 @@ struct UnavailableMailboxListView: View {
@ObservedResults(
Mailbox.self,
configuration: MailboxInfosManager.instance.realmConfiguration,
- where: { $0.userId == AccountManager.instance.currentUserId && $0.isPasswordValid == false },
+ where: { mailbox in
+ @InjectService var accountManager: AccountManager
+ return mailbox.userId == accountManager.currentUserId && mailbox.isPasswordValid == false
+ },
sortDescriptor: SortDescriptor(keyPath: \Mailbox.mailboxId)
) private var passwordBlockedMailboxes
@ObservedResults(
Mailbox.self,
configuration: MailboxInfosManager.instance.realmConfiguration,
- where: { $0.userId == AccountManager.instance.currentUserId && $0.isLocked == true },
+ where: { mailbox in
+ @InjectService var accountManager: AccountManager
+ return mailbox.userId == accountManager.currentUserId && mailbox.isLocked == true
+ },
sortDescriptor: SortDescriptor(keyPath: \Mailbox.mailboxId)
) private var lockedMailboxes
diff --git a/Mail/Helpers/AppAssembly.swift b/Mail/Helpers/AppAssembly.swift
new file mode 100644
index 000000000..c98f437af
--- /dev/null
+++ b/Mail/Helpers/AppAssembly.swift
@@ -0,0 +1,141 @@
+/*
+ Infomaniak Mail - iOS App
+ Copyright (C) 2022 Infomaniak Network SA
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+import Foundation
+import InfomaniakBugTracker
+import InfomaniakCore
+import InfomaniakCoreUI
+import InfomaniakDI
+import InfomaniakLogin
+import InfomaniakNotifications
+import MailCore
+
+private let realmRootPath = "mailboxes"
+private let appGroupIdentifier = "group.com.infomaniak.mail"
+
+extension Array where Element == Factory {
+ func registerFactoriesInDI() {
+ forEach { SimpleResolver.sharedResolver.store(factory: $0) }
+ }
+}
+
+/// Something that prepares the application Dependency Injection
+enum ApplicationAssembly {
+ static func setupDI() {
+ // Setup main servicies
+ setupMainServices()
+
+ // Setup proxy types necessary for the App code to work in Extension mode
+ setupProxyTypes()
+ }
+
+ private static func setupMainServices() {
+ let factories = [
+ Factory(type: InfomaniakNetworkLoginable.self) { _, _ in
+ InfomaniakNetworkLogin(clientId: MailApiFetcher.clientId)
+ },
+ Factory(type: InfomaniakLoginable.self) { _, _ in
+ InfomaniakLogin(clientId: MailApiFetcher.clientId)
+ },
+ Factory(type: KeychainHelper.self) { _, _ in
+ KeychainHelper(accessGroup: AccountManager.accessGroup)
+ },
+ Factory(type: InfomaniakNotifications.self) { _, _ in
+ InfomaniakNotifications(appGroup: AccountManager.appGroup)
+ },
+ Factory(type: AppLockHelper.self) { _, _ in
+ AppLockHelper()
+ },
+ Factory(type: BugTracker.self) { _, _ in
+ BugTracker(info: BugTrackerInfo(project: "app-mobile-mail", gitHubRepoName: "ios-mail", appReleaseType: .beta))
+ },
+ Factory(type: MatomoUtils.self) { _, _ in
+ MatomoUtils(siteId: Constants.matomoId, baseURL: URLConstants.matomo.url)
+ },
+ Factory(type: IKSnackBarAvoider.self) { _, _ in
+ IKSnackBarAvoider()
+ },
+ Factory(type: DraftManager.self) { _, _ in
+ DraftManager()
+ },
+ Factory(type: AccountManager.self) { _, _ in
+ AccountManager()
+ },
+ Factory(type: SnackBarPresentable.self) { _, _ in
+ SnackBarPresenter()
+ },
+ Factory(type: UserAlertDisplayable.self) { _, _ in
+ UserAlertDisplayer()
+ },
+ Factory(type: ApplicationStatable.self) { _, _ in
+ ApplicationState()
+ },
+ Factory(type: UserActivityController.self) { _, _ in
+ UserActivityController()
+ },
+ Factory(type: PlatformDetectable.self) { _, _ in
+ PlatformDetector()
+ },
+ Factory(type: AppGroupPathProvidable.self) { _, _ in
+ guard let provider = AppGroupPathProvider(
+ realmRootPath: realmRootPath,
+ appGroupIdentifier: appGroupIdentifier
+ ) else {
+ fatalError("could not safely init AppGroupPathProvider")
+ }
+
+ return provider
+ },
+ Factory(type: TokenStore.self) { _, _ in
+ TokenStore()
+ }
+ ]
+
+ factories.registerFactoriesInDI()
+ }
+
+ private static func setupProxyTypes() {
+ let factories = [
+ Factory(type: CacheManageable.self) { _, _ in
+ CacheManager()
+ },
+ Factory(type: OrientationManageable.self) { _, _ in
+ OrientationManager()
+ },
+ Factory(type: RemoteNotificationRegistrable.self) { _, _ in
+ RemoteNotificationRegistrer()
+ }
+ ]
+
+ factories.registerFactoriesInDI()
+ }
+}
+
+/// Something that loads the DI on init
+public struct EarlyDIHook {
+ public init() {
+ // Setup date encoding
+ ApiFetcher.decoder.dateDecodingStrategy = .iso8601
+
+ // setup DI ASAP
+ ApplicationAssembly.setupDI()
+
+ // Setup debug stack early, requires DI to be setup to work
+ Logging.initLogging()
+ }
+}
diff --git a/Mail/NotificationCenterDelegate.swift b/Mail/Helpers/NotificationCenterDelegate.swift
similarity index 74%
rename from Mail/NotificationCenterDelegate.swift
rename to Mail/Helpers/NotificationCenterDelegate.swift
index 865409eef..db09644ef 100644
--- a/Mail/NotificationCenterDelegate.swift
+++ b/Mail/Helpers/NotificationCenterDelegate.swift
@@ -17,6 +17,7 @@
*/
import Foundation
+import InfomaniakDI
import MailCore
import UIKit
import UserNotifications
@@ -26,24 +27,26 @@ public struct NotificationTappedPayload {
}
@MainActor
-class NotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate {
+final class NotificationCenterDelegate: NSObject, UNUserNotificationCenterDelegate {
+ @LazyInjectService private var accountManager: AccountManager
+
private func handleClickOnNotification(scene: UIScene?, content: UNNotificationContent) {
guard let mailboxId = content.userInfo[NotificationsHelper.UserInfoKeys.mailboxId] as? Int,
let userId = content.userInfo[NotificationsHelper.UserInfoKeys.userId] as? Int,
let mailbox = MailboxInfosManager.instance.getMailbox(id: mailboxId, userId: userId),
- let mailboxManager = AccountManager.instance.getMailboxManager(for: mailbox) else {
+ let mailboxManager = accountManager.getMailboxManager(for: mailbox) else {
return
}
- if AccountManager.instance.currentMailboxManager?.mailbox != mailboxManager.mailbox {
- if AccountManager.instance.getCurrentAccount()?.userId != mailboxManager.mailbox.userId {
- if let switchedAccount = AccountManager.instance.accounts.values
+ if accountManager.currentMailboxManager?.mailbox != mailboxManager.mailbox {
+ if accountManager.getCurrentAccount()?.userId != mailboxManager.mailbox.userId {
+ if let switchedAccount = accountManager.accounts.values
.first(where: { $0.userId == mailboxManager.mailbox.userId }) {
- AccountManager.instance.switchAccount(newAccount: switchedAccount)
- AccountManager.instance.switchMailbox(newMailbox: mailbox)
+ accountManager.switchAccount(newAccount: switchedAccount)
+ accountManager.switchMailbox(newMailbox: mailbox)
}
} else {
- AccountManager.instance.switchMailbox(newMailbox: mailbox)
+ accountManager.switchMailbox(newMailbox: mailbox)
}
}
diff --git a/Mail/Helpers/WorkInProgress.swift b/Mail/Helpers/WorkInProgress.swift
index 711d7a063..246261271 100644
--- a/Mail/Helpers/WorkInProgress.swift
+++ b/Mail/Helpers/WorkInProgress.swift
@@ -18,11 +18,13 @@
import Foundation
import InfomaniakCoreUI
+import InfomaniakDI
import MailCore
import MailResources
// To delete: alert to facilitate tests for beta version
@MainActor
func showWorkInProgressSnackBar() {
- IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.workInProgressTitle)
+ @InjectService var snackbarPresenter: SnackBarPresentable
+ snackbarPresenter.show(message: MailResourcesStrings.Localizable.workInProgressTitle)
}
diff --git a/Mail/MailApp.swift b/Mail/MailApp.swift
index ca32cc5d8..efd0a763a 100644
--- a/Mail/MailApp.swift
+++ b/Mail/MailApp.swift
@@ -28,67 +28,13 @@ import Sentry
import SwiftUI
import UIKit
-public struct EarlyDIHook {
- public init() {
- // setup DI and logging ASAP
- Logging.initLogging()
- setupDI()
- }
-
- func setupDI() {
- let networkLoginService = Factory(type: InfomaniakNetworkLoginable.self) { _, _ in
- InfomaniakNetworkLogin(clientId: MailApiFetcher.clientId)
- }
- let loginService = Factory(type: InfomaniakLoginable.self) { _, _ in
- InfomaniakLogin(clientId: MailApiFetcher.clientId)
- }
- let keychainHelper = Factory(type: KeychainHelper.self) { _, _ in
- KeychainHelper(accessGroup: AccountManager.accessGroup)
- }
- let tokenStore = Factory(type: TokenStore.self) { _, _ in
- TokenStore()
- }
- let notificationService = Factory(type: InfomaniakNotifications.self) { _, _ in
- InfomaniakNotifications(appGroup: AccountManager.appGroup)
- }
- let appLockHelper = Factory(type: AppLockHelper.self) { _, _ in
- AppLockHelper()
- }
- let bugTracker = Factory(type: BugTracker.self) { _, _ in
- BugTracker(info: BugTrackerInfo(project: "app-mobile-mail", gitHubRepoName: "ios-mail", appReleaseType: .beta))
- }
- let matomoUtils = Factory(type: MatomoUtils.self) { _, _ in
- MatomoUtils(siteId: Constants.matomoId, baseURL: URLConstants.matomo.url)
- }
- let avoider = Factory(type: SnackBarAvoider.self) { _, _ in
- SnackBarAvoider()
- }
- let draftManager = Factory(type: DraftManager.self) { _, _ in
- DraftManager()
- }
- let userActivityController = Factory(type: UserActivityController.self) { _, _ in
- UserActivityController()
- }
-
- SimpleResolver.sharedResolver.store(factory: networkLoginService)
- SimpleResolver.sharedResolver.store(factory: loginService)
- SimpleResolver.sharedResolver.store(factory: notificationService)
- SimpleResolver.sharedResolver.store(factory: keychainHelper)
- SimpleResolver.sharedResolver.store(factory: tokenStore)
- SimpleResolver.sharedResolver.store(factory: appLockHelper)
- SimpleResolver.sharedResolver.store(factory: bugTracker)
- SimpleResolver.sharedResolver.store(factory: matomoUtils)
- SimpleResolver.sharedResolver.store(factory: avoider)
- SimpleResolver.sharedResolver.store(factory: draftManager)
- SimpleResolver.sharedResolver.store(factory: userActivityController)
- }
-}
-
@main
struct MailApp: App {
/// Making sure the DI is registered at a very early stage of the app launch.
private let dependencyInjectionHook = EarlyDIHook()
- @LazyInjectService var appLockHelper: AppLockHelper
+
+ @LazyInjectService private var appLockHelper: AppLockHelper
+ @LazyInjectService private var accountManager: AccountManager
@UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
@@ -99,11 +45,8 @@ struct MailApp: App {
@StateObject private var navigationState = NavigationState()
- private let accountManager = AccountManager.instance
-
init() {
DDLogInfo("Application starting in foreground ? \(UIApplication.shared.applicationState != .background)")
- ApiFetcher.decoder.dateDecodingStrategy = .iso8601
}
var body: some Scene {
@@ -161,7 +104,7 @@ struct MailApp: App {
try await accountManager.updateUser(for: account)
accountManager.enableBugTrackerIfAvailable()
- try await accountManager.currentMailboxManager?.contactManager.fetchContactsAndAddressBooks()
+ try await accountManager.currentContactManager?.fetchContactsAndAddressBooks()
} catch {
DDLogError("Error while updating user account: \(error)")
}
diff --git a/Mail/Proxy/Implementation/ApplicationState.swift b/Mail/Proxy/Implementation/ApplicationState.swift
new file mode 100644
index 000000000..b15fd87f4
--- /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 MailCore
+import UIKit
+
+public struct ApplicationState: ApplicationStatable {
+ public var applicationState: UIApplication.State? {
+ UIApplication.shared.applicationState
+ }
+}
diff --git a/Mail/Proxy/Implementation/CacheManager.swift b/Mail/Proxy/Implementation/CacheManager.swift
new file mode 100644
index 000000000..3010b7d1f
--- /dev/null
+++ b/Mail/Proxy/Implementation/CacheManager.swift
@@ -0,0 +1,45 @@
+/*
+ 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 InfomaniakDI
+import MailCore
+import UIKit
+
+@available(iOSApplicationExtension, unavailable)
+public final class CacheManager: CacheManageable {
+ @LazyInjectService private var accountManager: AccountManager
+
+ public func refreshCacheData() {
+ guard let currentAccount = accountManager.getCurrentAccount() else {
+ return
+ }
+
+ Task {
+ do {
+ try await accountManager.updateUser(for: currentAccount)
+ accountManager.enableBugTrackerIfAvailable()
+
+ try await accountManager.currentContactManager?.fetchContactsAndAddressBooks()
+ } catch {
+ DDLogError("Error while updating user account: \(error)")
+ }
+ }
+ }
+}
diff --git a/Mail/Proxy/Implementation/OrientationLock.swift b/Mail/Proxy/Implementation/OrientationLock.swift
new file mode 100644
index 000000000..61c8445da
--- /dev/null
+++ b/Mail/Proxy/Implementation/OrientationLock.swift
@@ -0,0 +1,35 @@
+/*
+ Infomaniak Mail - iOS App
+ Copyright (C) 2022 Infomaniak Network SA
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+import Foundation
+import InfomaniakCoreUI
+import UIKit
+
+@available(iOSApplicationExtension, unavailable)
+public final class OrientationManager: OrientationManageable {
+ /// Default to .all
+ public var orientationLock = UIInterfaceOrientationMask.all
+
+ public func setOrientationLock(_ orientation: UIInterfaceOrientationMask) {
+ orientationLock = orientation
+ }
+
+ public var interfaceOrientation: UIInterfaceOrientation? {
+ UIApplication.shared.mainSceneKeyWindow?.windowScene?.interfaceOrientation
+ }
+}
diff --git a/Mail/Proxy/Implementation/RemoteNotificationRegistrer.swift b/Mail/Proxy/Implementation/RemoteNotificationRegistrer.swift
new file mode 100644
index 000000000..35a754849
--- /dev/null
+++ b/Mail/Proxy/Implementation/RemoteNotificationRegistrer.swift
@@ -0,0 +1,27 @@
+/*
+ Infomaniak Mail - iOS App
+ Copyright (C) 2022 Infomaniak Network SA
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+import Foundation
+import UIKit
+
+@available(iOSApplicationExtension, unavailable)
+public final class RemoteNotificationRegistrer: RemoteNotificationRegistrable {
+ public func register() {
+ UIApplication.shared.registerForRemoteNotifications()
+ }
+}
diff --git a/Mail/Proxy/Protocols/CacheManageable.swift b/Mail/Proxy/Protocols/CacheManageable.swift
new file mode 100644
index 000000000..0941b482f
--- /dev/null
+++ b/Mail/Proxy/Protocols/CacheManageable.swift
@@ -0,0 +1,24 @@
+/*
+ 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 that can manipulate cached data
+public protocol CacheManageable {
+ func refreshCacheData()
+}
diff --git a/Mail/Proxy/Protocols/OrientationManageable.swift b/Mail/Proxy/Protocols/OrientationManageable.swift
new file mode 100644
index 000000000..760162cf1
--- /dev/null
+++ b/Mail/Proxy/Protocols/OrientationManageable.swift
@@ -0,0 +1,32 @@
+/*
+ Infomaniak Mail - iOS App
+ Copyright (C) 2022 Infomaniak Network SA
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+import Foundation
+import UIKit
+
+/// Something that can manage rotation state
+public protocol OrientationManageable {
+ /// Access the orientation lock mask
+ var orientationLock: UIInterfaceOrientationMask { get }
+
+ /// Set the orientation lock mask
+ func setOrientationLock(_ orientation: UIInterfaceOrientationMask)
+
+ /// Read the interface orientation
+ var interfaceOrientation: UIInterfaceOrientation? { get }
+}
diff --git a/Mail/Proxy/Protocols/RemoteNotificationRegistrable.swift b/Mail/Proxy/Protocols/RemoteNotificationRegistrable.swift
new file mode 100644
index 000000000..d84c57682
--- /dev/null
+++ b/Mail/Proxy/Protocols/RemoteNotificationRegistrable.swift
@@ -0,0 +1,24 @@
+/*
+ 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 that can register remote notifications
+public protocol RemoteNotificationRegistrable {
+ func register()
+}
diff --git a/Mail/Proxy/Protocols/RootViewManageable.swift b/Mail/Proxy/Protocols/RootViewManageable.swift
new file mode 100644
index 000000000..c99444dcc
--- /dev/null
+++ b/Mail/Proxy/Protocols/RootViewManageable.swift
@@ -0,0 +1,29 @@
+/*
+ Infomaniak Mail - iOS App
+ Copyright (C) 2022 Infomaniak Network SA
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+import Foundation
+import UIKit
+
+/// Something that can fetch the Root View Controller
+public protocol RootViewManageable {
+ /// The current rootViewController
+ var rootViewController: UIViewController? { get }
+
+ /// The current mainSceneKeyWindow
+ var mainSceneKeyWindow: UIWindow? { get }
+}
diff --git a/Mail/Utils/NavigationState.swift b/Mail/Utils/NavigationState.swift
index db580e4c0..66838a435 100644
--- a/Mail/Utils/NavigationState.swift
+++ b/Mail/Utils/NavigationState.swift
@@ -81,7 +81,6 @@ enum RootViewDestination {
class NavigationState: ObservableObject {
@LazyInjectService private var appLockHelper: AppLockHelper
- private let accountManager = AccountManager.instance
private var accountManagerObservation: AnyCancellable?
@Published private(set) var rootViewState: RootViewState
@@ -96,17 +95,20 @@ class NavigationState: ObservableObject {
private(set) var account: Account?
init() {
- account = AccountManager.instance.getCurrentAccount()
+ @InjectService var accountManager: AccountManager
+
+ account = accountManager.getCurrentAccount()
rootViewState = NavigationState.getMainViewStateIfPossible()
accountManagerObservation = accountManager.objectWillChange.receive(on: RunLoop.main).sink { [weak self] in
- self?.account = AccountManager.instance.getCurrentAccount()
+ self?.account = accountManager.getCurrentAccount()
self?.rootViewState = NavigationState.getMainViewStateIfPossible()
}
}
static func getMainViewStateIfPossible() -> RootViewState {
- let accountManager = AccountManager.instance
+ @InjectService var accountManager: AccountManager
+
if let currentAccount = accountManager.getCurrentAccount() {
if let currentMailboxManager = accountManager.currentMailboxManager {
return .mainView(currentMailboxManager)
diff --git a/Mail/Utils/SnackBarAwareModifier.swift b/Mail/Utils/SnackBarAwareModifier.swift
index 188d1092a..8b5cf42e8 100644
--- a/Mail/Utils/SnackBarAwareModifier.swift
+++ b/Mail/Utils/SnackBarAwareModifier.swift
@@ -16,12 +16,13 @@
along with this program. If not, see .
*/
+import InfomaniakCoreUI
import InfomaniakDI
import MailCore
import SwiftUI
struct SnackBarAwareModifier: ViewModifier {
- @InjectService var avoider: SnackBarAvoider
+ @InjectService var avoider: IKSnackBarAvoider
var inset: CGFloat {
didSet {
avoider.addAvoider(inset: inset)
diff --git a/Mail/Utils/View+Extension.swift b/Mail/Utils/View+Extension.swift
index 868e36127..8021b147b 100644
--- a/Mail/Utils/View+Extension.swift
+++ b/Mail/Utils/View+Extension.swift
@@ -17,26 +17,41 @@
*/
import InfomaniakCoreUI
+import InfomaniakDI
import MailCore
import MailResources
import SwiftUI
struct DeviceRotationViewModifier: ViewModifier {
let action: (UIInterfaceOrientation?) -> Void
- @State private var lastOrientation = UIApplication.shared.mainSceneKeyWindow?.windowScene?.interfaceOrientation
+
+ private var orientationManager: OrientationManageable
+
+ @State private var lastOrientation: UIInterfaceOrientation?
+
+ init(action: @escaping (UIInterfaceOrientation?) -> Void) {
+ let orientationSource = InjectService().wrappedValue
+ let orientation = orientationSource.interfaceOrientation
+ self.action = action
+ orientationManager = orientationSource
+ lastOrientation = orientation
+ }
func body(content: Content) -> some View {
content
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
- if lastOrientation != UIApplication.shared.mainSceneKeyWindow?.windowScene?.interfaceOrientation {
- lastOrientation = UIApplication.shared.mainSceneKeyWindow?.windowScene?.interfaceOrientation
- action(lastOrientation)
+ guard let currentOrientation = orientationManager.interfaceOrientation else {
+ return
+ }
+ if lastOrientation != currentOrientation {
+ lastOrientation = currentOrientation
+ action(currentOrientation)
}
}
}
}
-// A View wrapper to make the modifier easier to use
+/// A View wrapper to make the modifier easier to use
extension View {
func onRotate(perform action: @escaping (UIInterfaceOrientation?) -> Void) -> some View {
modifier(DeviceRotationViewModifier(action: action))
diff --git a/Mail/Views/Alerts/AddLinkView.swift b/Mail/Views/Alerts/AddLinkView.swift
index 321601606..a00df99d4 100644
--- a/Mail/Views/Alerts/AddLinkView.swift
+++ b/Mail/Views/Alerts/AddLinkView.swift
@@ -27,6 +27,7 @@ struct AddLinkView: View {
@State private var url = ""
@FocusState private var isFocused: Bool
+ @LazyInjectService private var snackbarPresenter: SnackBarPresentable
@LazyInjectService private var matomo: MatomoUtils
var actionHandler: ((String) -> Void)?
@@ -50,14 +51,14 @@ struct AddLinkView: View {
matomo.track(eventWithCategory: .editorActions, name: "addLinkConfirm")
guard var urlComponents = URLComponents(string: url) else {
- IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.snackbarInvalidUrl)
+ snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarInvalidUrl)
return
}
if urlComponents.scheme == nil {
urlComponents.scheme = URLConstants.schemeUrl
}
guard let url = urlComponents.url?.absoluteString else {
- IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.snackbarInvalidUrl)
+ snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarInvalidUrl)
return
}
actionHandler?(url)
diff --git a/Mail/Views/Alerts/DetachMailboxConfirmationView.swift b/Mail/Views/Alerts/DetachMailboxConfirmationView.swift
index a96053f14..87ac9373c 100644
--- a/Mail/Views/Alerts/DetachMailboxConfirmationView.swift
+++ b/Mail/Views/Alerts/DetachMailboxConfirmationView.swift
@@ -23,6 +23,8 @@ import MailResources
import SwiftUI
struct DetachMailboxConfirmationView: View {
+ @LazyInjectService private var accountManager: AccountManager
+
@EnvironmentObject private var navigationState: NavigationState
let mailbox: Mailbox
@@ -58,7 +60,7 @@ struct DetachMailboxConfirmationView: View {
Task {
await tryOrDisplayError {
- try await AccountManager.instance.detachMailbox(mailbox: mailbox)
+ try await accountManager.detachMailbox(mailbox: mailbox)
navigationState.transitionToRootViewDestination(.mainView)
}
}
diff --git a/Mail/Views/Alerts/LogoutConfirmationView.swift b/Mail/Views/Alerts/LogoutConfirmationView.swift
index fabceb5f1..b7b235ad5 100644
--- a/Mail/Views/Alerts/LogoutConfirmationView.swift
+++ b/Mail/Views/Alerts/LogoutConfirmationView.swift
@@ -25,6 +25,8 @@ import MailResources
import SwiftUI
struct LogoutConfirmationView: View {
+ @LazyInjectService private var accountManager: AccountManager
+
let account: Account
var body: some View {
@@ -44,11 +46,11 @@ struct LogoutConfirmationView: View {
@InjectService var notificationService: InfomaniakNotifications
await notificationService.removeStoredTokenFor(userId: account.userId)
- AccountManager.instance.removeTokenAndAccount(account: account)
- if let nextAccount = AccountManager.instance.accounts.first {
- AccountManager.instance.switchAccount(newAccount: nextAccount)
+ accountManager.removeTokenAndAccount(account: account)
+ if let nextAccount = accountManager.accounts.first {
+ accountManager.switchAccount(newAccount: nextAccount)
}
- AccountManager.instance.saveAccounts()
+ accountManager.saveAccounts()
}
}
}
diff --git a/Mail/Views/Alerts/ReportPhishingView.swift b/Mail/Views/Alerts/ReportPhishingView.swift
index bee83b6ab..c7e9748f3 100644
--- a/Mail/Views/Alerts/ReportPhishingView.swift
+++ b/Mail/Views/Alerts/ReportPhishingView.swift
@@ -17,11 +17,13 @@
*/
import InfomaniakCoreUI
+import InfomaniakDI
import MailCore
import MailResources
import SwiftUI
struct ReportPhishingView: View {
+ @LazyInjectService private var snackbarPresenter: SnackBarPresentable
@EnvironmentObject private var mailboxManager: MailboxManager
let message: Message
@@ -45,7 +47,7 @@ struct ReportPhishingView: View {
var messages = [message.freezeIfNeeded()]
messages.append(contentsOf: message.duplicates)
_ = try await mailboxManager.move(messages: messages, to: .spam)
- await IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.snackbarReportPhishingConfirmation)
+ await snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarReportPhishingConfirmation)
}
}
}
diff --git a/Mail/Views/Attachment/AttachmentPreview.swift b/Mail/Views/Attachment/AttachmentPreview.swift
index 69119cb8b..43d32344a 100644
--- a/Mail/Views/Attachment/AttachmentPreview.swift
+++ b/Mail/Views/Attachment/AttachmentPreview.swift
@@ -30,6 +30,8 @@ struct AttachmentPreview: View {
@Environment(\.verticalSizeClass) var sizeClass
+ @LazyInjectService var rootViewControllerFetcher: RootViewManageable
+
var body: some View {
NavigationView {
Group {
@@ -72,7 +74,7 @@ struct AttachmentPreview: View {
@InjectService var matomo: MatomoUtils
matomo.track(eventWithCategory: .message, name: "download")
guard let url = attachment.localUrl,
- var source = UIApplication.shared.mainSceneKeyWindow?.rootViewController else {
+ var source = rootViewControllerFetcher.rootViewController else {
return
}
if let presentedViewController = source.presentedViewController {
diff --git a/Mail/Views/Bottom sheets/Actions/ActionsViewModel.swift b/Mail/Views/Bottom sheets/Actions/ActionsViewModel.swift
index 11a00945f..6d05b9887 100644
--- a/Mail/Views/Bottom sheets/Actions/ActionsViewModel.swift
+++ b/Mail/Views/Bottom sheets/Actions/ActionsViewModel.swift
@@ -239,6 +239,7 @@ enum ActionsTarget: Equatable, Identifiable {
@Published var listActions: [Action] = []
@LazyInjectService private var matomo: MatomoUtils
+ @LazyInjectService private var snackbarPresenter: SnackBarPresentable
init(mailboxManager: MailboxManager,
target: ActionsTarget,
@@ -457,7 +458,7 @@ enum ActionsTarget: Equatable, Identifiable {
guard case .message(let message) = target else { return }
let response = try await mailboxManager.apiFetcher.blockSender(message: message)
if response {
- IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.snackbarSenderBlacklisted(1))
+ snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarSenderBlacklisted(1))
}
}
diff --git a/Mail/Views/Bottom sheets/Actions/ContactActionsView.swift b/Mail/Views/Bottom sheets/Actions/ContactActionsView.swift
index a320074dd..e3e9c10d0 100644
--- a/Mail/Views/Bottom sheets/Actions/ContactActionsView.swift
+++ b/Mail/Views/Bottom sheets/Actions/ContactActionsView.swift
@@ -29,6 +29,7 @@ struct ContactActionsView: View {
@EnvironmentObject private var mailboxManager: MailboxManager
@LazyInjectService private var matomo: MatomoUtils
+ @LazyInjectService private var snackbarPresenter: SnackBarPresentable
@State private var writtenToRecipient: Recipient?
@@ -98,14 +99,14 @@ struct ContactActionsView: View {
Task {
await tryOrDisplayError {
try await mailboxManager.contactManager.addContact(recipient: recipient)
- IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.snackbarContactSaved)
+ snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarContactSaved)
}
}
}
private func copyEmail() {
UIPasteboard.general.string = recipient.email
- IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.snackbarEmailCopiedToClipboard)
+ snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarEmailCopiedToClipboard)
}
}
diff --git a/Mail/Views/Bottom sheets/ReportDisplayProblemView.swift b/Mail/Views/Bottom sheets/ReportDisplayProblemView.swift
index 4a1239ae5..e67e209c7 100644
--- a/Mail/Views/Bottom sheets/ReportDisplayProblemView.swift
+++ b/Mail/Views/Bottom sheets/ReportDisplayProblemView.swift
@@ -18,12 +18,14 @@
import InfomaniakCore
import InfomaniakCoreUI
+import InfomaniakDI
import MailCore
import MailResources
import Sentry
import SwiftUI
struct ReportDisplayProblemView: View {
+ @LazyInjectService private var snackbarPresenter: SnackBarPresentable
@EnvironmentObject private var mailboxManager: MailboxManager
let message: Message
@@ -55,7 +57,7 @@ struct ReportDisplayProblemView: View {
_ = SentrySDK.capture(message: "Message display problem reported") { scope in
scope.add(fileAttachment)
}
- await IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.snackbarDisplayProblemReported)
+ snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarDisplayProblemReported)
}
}
}
diff --git a/Mail/Views/Bottom sheets/RestoreEmailsView.swift b/Mail/Views/Bottom sheets/RestoreEmailsView.swift
index 71764957c..833d8822a 100644
--- a/Mail/Views/Bottom sheets/RestoreEmailsView.swift
+++ b/Mail/Views/Bottom sheets/RestoreEmailsView.swift
@@ -28,10 +28,10 @@ struct RestoreEmailsView: View {
@State private var selectedDate = ""
@State private var availableDates = [String]()
-
@State private var pickerNoSelectionText = MailResourcesStrings.Localizable.loadingText
@LazyInjectService private var matomo: MatomoUtils
+ @LazyInjectService private var snackbarPresenter: SnackBarPresentable
var body: some View {
VStack(alignment: .leading) {
@@ -76,7 +76,7 @@ struct RestoreEmailsView: View {
Task {
await tryOrDisplayError {
try await mailboxManager.apiFetcher.restoreBackup(mailbox: mailboxManager.mailbox, date: selectedDate)
- IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.snackbarRestorationLaunched)
+ snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarRestorationLaunched)
}
}
}
diff --git a/Mail/LockedAppView.swift b/Mail/Views/LockedAppView.swift
similarity index 100%
rename from Mail/LockedAppView.swift
rename to Mail/Views/LockedAppView.swift
diff --git a/Mail/Views/Menu Drawer/Items/MenuDrawerItemsListView.swift b/Mail/Views/Menu Drawer/Items/MenuDrawerItemsListView.swift
index 4a1804914..2eed3c6d2 100644
--- a/Mail/Views/Menu Drawer/Items/MenuDrawerItemsListView.swift
+++ b/Mail/Views/Menu Drawer/Items/MenuDrawerItemsListView.swift
@@ -27,6 +27,8 @@ import SwiftUI
struct MenuDrawerItemsAdvancedListView: View {
@State private var isShowingRestoreMails = false
+ @Environment(\.openURL) private var openURL
+
let mailboxCanRestoreEmails: Bool
var body: some View {
@@ -35,7 +37,7 @@ struct MenuDrawerItemsAdvancedListView: View {
MenuDrawerItemCell(icon: MailResourcesAsset.drawerDownload,
label: MailResourcesStrings.Localizable.buttonImportEmails,
matomoName: "importEmails") {
- UIApplication.shared.open(URLConstants.importMails.url)
+ openURL(URLConstants.importMails.url)
}
if mailboxCanRestoreEmails {
MenuDrawerItemCell(
@@ -56,6 +58,8 @@ struct MenuDrawerItemsAdvancedListView: View {
struct MenuDrawerItemsHelpListView: View {
@EnvironmentObject private var mailboxManager: MailboxManager
+ @Environment(\.openURL) private var openURL
+
@State private var isShowingHelp = false
@State private var isShowingBugTracker = false
@@ -85,7 +89,7 @@ struct MenuDrawerItemsHelpListView: View {
if mailboxManager.account.user?.isStaff == true {
isShowingBugTracker.toggle()
} else if let userReportURL = URL(string: MailResourcesStrings.Localizable.urlUserReportiOS) {
- UIApplication.shared.open(userReportURL)
+ openURL(userReportURL)
}
}
}
diff --git a/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift b/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift
index fa5f7b526..555dd84ce 100644
--- a/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift
+++ b/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift
@@ -41,6 +41,8 @@ extension View {
}
struct MailboxCell: View {
+ @LazyInjectService private var accountManager: AccountManager
+
@Environment(\.mailboxCellStyle) private var style: Style
@EnvironmentObject private var navigationDrawerState: NavigationDrawerState
@@ -77,7 +79,7 @@ struct MailboxCell: View {
case .account:
matomo.track(eventWithCategory: .account, name: "switchMailbox")
}
- AccountManager.instance.switchMailbox(newMailbox: mailbox)
+ accountManager.switchMailbox(newMailbox: mailbox)
navigationDrawerState.close()
}
.floatingPanel(isPresented: $isShowingLockedView) {
diff --git a/Mail/Views/Menu Drawer/MailboxManagement/MailboxesManagementView.swift b/Mail/Views/Menu Drawer/MailboxManagement/MailboxesManagementView.swift
index 5a8a25e6f..b40a30d82 100644
--- a/Mail/Views/Menu Drawer/MailboxManagement/MailboxesManagementView.swift
+++ b/Mail/Views/Menu Drawer/MailboxManagement/MailboxesManagementView.swift
@@ -28,6 +28,8 @@ struct MailboxesManagementView: View {
@EnvironmentObject var mailboxManager: MailboxManager
@EnvironmentObject var navigationDrawerState: NavigationDrawerState
+ @LazyInjectService private var accountManager: AccountManager
+
@ObservedResults(
Mailbox.self,
configuration: MailboxInfosManager.instance.realmConfiguration,
@@ -94,8 +96,8 @@ struct MailboxesManagementView: View {
}
private func updateAccount() async throws {
- guard let account = AccountManager.instance.account(for: mailboxManager.mailbox.userId) else { return }
- try await AccountManager.instance.updateUser(for: account)
+ guard let account = accountManager.account(for: mailboxManager.mailbox.userId) else { return }
+ try await accountManager.updateUser(for: account)
}
}
diff --git a/Mail/Views/New Message/Attachments/Attachable.swift b/Mail/Views/New Message/Attachments/Attachable.swift
index 09420d564..91a3c510e 100644
--- a/Mail/Views/New Message/Attachments/Attachable.swift
+++ b/Mail/Views/New Message/Attachments/Attachable.swift
@@ -16,7 +16,10 @@
along with this program. If not, see .
*/
+import Combine
import Foundation
+import InfomaniakCore
+import InfomaniakCoreUI
import MailCore
import PhotosUI
import UniformTypeIdentifiers
@@ -28,6 +31,10 @@ protocol Attachable {
}
extension NSItemProvider: Attachable {
+ enum ErrorDomain: Error {
+ case UTINotFound
+ }
+
private var preferredIdentifier: String {
return registeredTypeIdentifiers
.first { UTType($0)?.conforms(to: .image) == true || UTType($0)?.conforms(to: .movie) == true } ?? ""
@@ -38,27 +45,29 @@ extension NSItemProvider: Attachable {
}
func writeToTemporaryURL() async throws -> URL {
- return try await loadFileRepresentation(typeIdentifier: preferredIdentifier)
- }
+ switch underlyingType {
+ case .isURL:
+ let getPlist = try ItemProviderURLRepresentation(from: self)
+ return try await getPlist.result.get()
+
+ case .isText:
+ let getText = try ItemProviderTextRepresentation(from: self)
+ return try await getText.result.get()
+
+ case .isUIImage:
+ let getUIImage = try ItemProviderUIImageRepresentation(from: self)
+ return try await getUIImage.result.get()
+
+ case .isImageData, .isCompressedData, .isMiscellaneous:
+ let getFile = try ItemProviderFileRepresentation(from: self)
+ return try await getFile.result.get()
+
+ case .isDirectory:
+ let getFile = try ItemProviderZipRepresentation(from: self)
+ return try await getFile.result.get()
- private func loadFileRepresentation(typeIdentifier: String) async throws -> URL {
- try await withCheckedThrowingContinuation { continuation in
- loadFileRepresentation(forTypeIdentifier: typeIdentifier) { fileProviderURL, error in
- guard let fileProviderURL else {
- continuation.resume(throwing: error ?? MailError.unknownError)
- return
- }
-
- do {
- let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
- let temporaryFileURL = temporaryURL.appendingPathComponent(fileProviderURL.lastPathComponent)
- try FileManager.default.createDirectory(at: temporaryURL, withIntermediateDirectories: true)
- try FileManager.default.copyItem(atPath: fileProviderURL.path, toPath: temporaryFileURL.path)
- continuation.resume(returning: temporaryFileURL)
- } catch {
- continuation.resume(throwing: error)
- }
- }
+ case .none:
+ throw ErrorDomain.UTINotFound
}
}
}
diff --git a/Mail/Views/New Message/Attachments/AttachmentsManager.swift b/Mail/Views/New Message/Attachments/AttachmentsManager.swift
index 6207deab9..fd8547de9 100644
--- a/Mail/Views/New Message/Attachments/AttachmentsManager.swift
+++ b/Mail/Views/New Message/Attachments/AttachmentsManager.swift
@@ -17,12 +17,14 @@
*/
import CocoaLumberjackSwift
+import Combine
import Foundation
+import InfomaniakCore
import MailCore
import PhotosUI
import SwiftUI
-class AttachmentUploadTask: ObservableObject {
+final class AttachmentUploadTask: ObservableObject {
@Published var progress: Double = 0
var task: Task?
@Published var error: MailError?
@@ -32,14 +34,22 @@ class AttachmentUploadTask: ObservableObject {
}
@MainActor
-class AttachmentsManager: ObservableObject {
+final class AttachmentsManager: ObservableObject {
private let draft: Draft
private let mailboxManager: MailboxManager
+ private let parallelTaskMapper = ParallelTaskMapper()
+ private let backgroundRealm: BackgroundRealm
+
+ /// Something to debounce content will change updates
+ private let contentWillChangeSubject = PassthroughSubject()
+ private var contentWillChangeObserver: AnyCancellable?
+
var attachments: [Attachment] {
return draft.attachments.filter { $0.contentId == nil }.toArray()
}
- private(set) var attachmentUploadTasks = [String: AttachmentUploadTask]()
+ private var attachmentUploadTasks = SendableDictionary()
+
var allAttachmentsUploaded: Bool {
return attachmentUploadTasks.values.allSatisfy(\.uploadDone)
}
@@ -55,6 +65,14 @@ class AttachmentsManager: ObservableObject {
init(draft: Draft, mailboxManager: MailboxManager) {
self.draft = draft
self.mailboxManager = mailboxManager
+
+ // Debouncing objectWillChange helps a lot scaling with numerous attachments
+ backgroundRealm = BackgroundRealm(configuration: mailboxManager.realmConfiguration)
+ contentWillChangeObserver = contentWillChangeSubject
+ .debounce(for: .milliseconds(150), scheduler: DispatchQueue.main)
+ .sink { _ in
+ self.objectWillChange.send()
+ }
}
func completeUploadedAttachments() {
@@ -62,26 +80,39 @@ class AttachmentsManager: ObservableObject {
let uploadTask = attachmentUploadTaskOrCreate(for: attachment.uuid)
uploadTask.progress = 1
}
- objectWillChange.send()
+ contentWillChangeSubject.send()
}
- private func updateAttachment(oldAttachment: Attachment, newAttachment: Attachment) {
- guard let realm = draft.realm,
- let oldAttachment = draft.attachments.first(where: { $0.uuid == oldAttachment.uuid }) else {
+ private func updateAttachment(oldAttachment: Attachment, newAttachment: Attachment) async {
+ guard let oldAttachment = draft.attachments.first(where: { $0.uuid == oldAttachment.uuid }) else {
return
}
- if oldAttachment.uuid != newAttachment.uuid {
- attachmentUploadTasks[newAttachment.uuid] = attachmentUploadTasks[oldAttachment.uuid]
- attachmentUploadTasks.removeValue(forKey: oldAttachment.uuid)
+ let oldAttachmentUUID = oldAttachment.uuid
+ let newAttachmentUUID = newAttachment.uuid
+ let primaryKey = draft.localUUID
+
+ if oldAttachmentUUID != newAttachmentUUID {
+ attachmentUploadTasks[newAttachmentUUID] = attachmentUploadTasks[oldAttachmentUUID]
+ attachmentUploadTasks.removeValue(forKey: oldAttachmentUUID)
}
- try? realm.write {
- // We need to update every field of the local attachment because embedded objects don't have a primary key
- oldAttachment.update(with: newAttachment)
+ await backgroundRealm.execute { realm in
+ try? realm.write {
+ guard let draftInContext = realm.object(ofType: Draft.self, forPrimaryKey: primaryKey) else {
+ return
+ }
+
+ guard let liveOldAttachment = draftInContext.attachments.first(where: { $0.uuid == oldAttachmentUUID }) else {
+ return
+ }
+
+ // We need to update every field of the local attachment because embedded objects don't have a primary key
+ liveOldAttachment.update(with: newAttachment)
+ }
}
- objectWillChange.send()
+ contentWillChangeSubject.send()
}
/// Lookup and return. New object created and returned instead
@@ -117,26 +148,50 @@ class AttachmentsManager: ObservableObject {
}
func removeAttachment(_ attachment: Attachment) {
- guard let realm = draft.realm,
- let liveAttachment = draft.attachments.first(where: { $0.uuid == attachment.uuid }) else { return }
+ let attachmentUUID = attachment.uuid
+ let primaryKey = draft.localUUID
- let attachmentUUID = liveAttachment.uuid
- try? realm.write {
- realm.delete(liveAttachment)
- }
- attachmentUploadTasks[attachmentUUID]?.task?.cancel()
- attachmentUploadTasks.removeValue(forKey: attachmentUUID)
+ Task {
+ await backgroundRealm.execute { realm in
+ try? realm.write {
+ guard let draftInContext = realm.object(ofType: Draft.self, forPrimaryKey: primaryKey) else {
+ return
+ }
+
+ guard let liveAttachment = draftInContext.attachments.first(where: { $0.uuid == attachmentUUID }) else {
+ return
+ }
+
+ realm.delete(liveAttachment)
+ }
+ }
- objectWillChange.send()
+ attachmentUploadTasks[attachmentUUID]?.task?.cancel()
+ attachmentUploadTasks.removeValue(forKey: attachmentUUID)
+
+ contentWillChangeSubject.send()
+ }
}
- private func addLocalAttachment(attachment: Attachment) -> Attachment {
+ private func addLocalAttachment(attachment: Attachment) async -> Attachment? {
attachmentUploadTasks[attachment.uuid] = AttachmentUploadTask()
- try? draft.realm?.write {
- draft.attachments.append(attachment)
+ let primaryKey = draft.localUUID
+
+ var detached: Attachment?
+ await backgroundRealm.execute { realm in
+ try? realm.write {
+ guard let draftInContext = realm.object(ofType: Draft.self, forPrimaryKey: primaryKey) else {
+ return
+ }
+
+ draftInContext.attachments.append(attachment)
+ }
+
+ detached = attachment.detached()
}
- objectWillChange.send()
- return attachment.freeze()
+
+ contentWillChangeSubject.send()
+ return detached
}
private func updateAttachmentUploadError(attachment: Attachment, error: Error?) {
@@ -147,10 +202,9 @@ class AttachmentsManager: ObservableObject {
}
}
- @MainActor
private func createLocalAttachment(name: String,
type: UTType?,
- disposition: AttachmentDisposition) -> Attachment {
+ disposition: AttachmentDisposition) async -> Attachment? {
let name = nameWithExtension(name: name,
correspondingTo: type)
let attachment = Attachment(uuid: UUID().uuidString,
@@ -159,14 +213,15 @@ class AttachmentsManager: ObservableObject {
size: 0,
name: name,
disposition: disposition)
- let savedAttachment = addLocalAttachment(attachment: attachment)
+ let savedAttachment = await addLocalAttachment(attachment: attachment)
return savedAttachment
}
private func updateLocalAttachment(url: URL, attachment: Attachment) async -> Attachment {
let urlResources = try? url.resourceValues(forKeys: [.typeIdentifierKey, .fileSizeKey])
let uti = UTType(urlResources?.typeIdentifier ?? "")
- let updatedName = nameWithExtension(name: attachment.name,
+ let name = url.lastPathComponent
+ let updatedName = nameWithExtension(name: name,
correspondingTo: uti)
let mimeType = uti?.preferredMIMEType ?? attachment.mimeType
let size = Int64(urlResources?.fileSize ?? 0)
@@ -178,23 +233,33 @@ class AttachmentsManager: ObservableObject {
name: updatedName,
disposition: attachment.disposition)
- updateAttachment(oldAttachment: attachment, newAttachment: newAttachment)
+ await updateAttachment(oldAttachment: attachment, newAttachment: newAttachment)
return newAttachment
}
- func importAttachments(attachments: [Attachable], disposition: AttachmentDisposition = .attachment) {
- for attachment in attachments {
- Task {
- let cid = await importAttachment(attachment: attachment, disposition: disposition)
+ func importAttachments(attachments: [Attachable], draft: Draft, disposition: AttachmentDisposition = .attachment) {
+ guard !attachments.isEmpty else {
+ return
+ }
+
+ // Cap max number of attachments, API errors out at 100
+ let attachmentsSlice = attachments[safe: 0 ..< draft.availableAttachmentsSlots]
+
+ Task {
+ try? await self.parallelTaskMapper.map(collection: attachmentsSlice) { attachment in
+ _ = await self.importAttachment(attachment: attachment, disposition: disposition)
// TODO: - Manage inline attachment
}
}
}
private func importAttachment(attachment: Attachable, disposition: AttachmentDisposition) async -> String? {
- let localAttachment = createLocalAttachment(name: attachment.suggestedName ?? getDefaultFileName(),
- type: attachment.type,
- disposition: disposition)
+ guard let localAttachment = await createLocalAttachment(name: attachment.suggestedName ?? getDefaultFileName(),
+ type: attachment.type,
+ disposition: disposition) else {
+ return nil
+ }
+
let importTask = Task { () -> String? in
do {
let url = try await attachment.writeToTemporaryURL()
@@ -252,7 +317,7 @@ class AttachmentsManager: ObservableObject {
self?.attachmentUploadTasks[localAttachment.uuid]?.progress = progress
}
}
- updateAttachment(oldAttachment: localAttachment, newAttachment: remoteAttachment)
+ await updateAttachment(oldAttachment: localAttachment, newAttachment: remoteAttachment)
return remoteAttachment
}
}
diff --git a/Mail/Views/New Message/ComposeMessageBodyView.swift b/Mail/Views/New Message/ComposeMessageBodyView.swift
index f7d909a8d..ece4799c2 100644
--- a/Mail/Views/New Message/ComposeMessageBodyView.swift
+++ b/Mail/Views/New Message/ComposeMessageBodyView.swift
@@ -63,19 +63,19 @@ struct ComposeMessageBodyView: View {
}
.fullScreenCover(isPresented: $isShowingCamera) {
CameraPicker { data in
- attachmentsManager.importAttachments(attachments: [data])
+ attachmentsManager.importAttachments(attachments: [data], draft: draft)
}
.ignoresSafeArea()
}
.sheet(isPresented: $isShowingFileSelection) {
DocumentPicker(pickerType: .selectContent([.item]) { urls in
- attachmentsManager.importAttachments(attachments: urls)
+ attachmentsManager.importAttachments(attachments: urls, draft: draft)
})
.ignoresSafeArea()
}
.sheet(isPresented: $isShowingPhotoLibrary) {
ImagePicker { results in
- attachmentsManager.importAttachments(attachments: results)
+ attachmentsManager.importAttachments(attachments: results, draft: draft)
}
.ignoresSafeArea()
}
diff --git a/Mail/Views/New Message/ComposeMessageView+Init.swift b/Mail/Views/New Message/ComposeMessageView+Init.swift
index 71ccba60f..e3a53cd65 100644
--- a/Mail/Views/New Message/ComposeMessageView+Init.swift
+++ b/Mail/Views/New Message/ComposeMessageView+Init.swift
@@ -23,8 +23,9 @@ import MailCore
import RealmSwift
extension ComposeMessageView {
- static func newMessage(_ draft: Draft, mailboxManager: MailboxManager) -> ComposeMessageView {
- return ComposeMessageView(draft: draft, mailboxManager: mailboxManager)
+ static func newMessage(_ draft: Draft, mailboxManager: MailboxManager,
+ itemProviders: [NSItemProvider] = []) -> ComposeMessageView {
+ return ComposeMessageView(draft: draft, mailboxManager: mailboxManager, attachments: itemProviders)
}
static func replyOrForwardMessage(messageReply: MessageReply, mailboxManager: MailboxManager) -> ComposeMessageView {
diff --git a/Mail/Views/New Message/ComposeMessageView.swift b/Mail/Views/New Message/ComposeMessageView.swift
index d7e7ee6ed..da5d65852 100644
--- a/Mail/Views/New Message/ComposeMessageView.swift
+++ b/Mail/Views/New Message/ComposeMessageView.swift
@@ -16,6 +16,7 @@
along with this program. If not, see .
*/
+import InfomaniakCore
import InfomaniakCoreUI
import InfomaniakDI
import Introspect
@@ -59,15 +60,18 @@ final class NewMessageAlert: SheetState {
struct ComposeMessageView: View {
@Environment(\.dismiss) private var dismiss
+ @Environment(\.dismissModal) var dismissModal
@LazyInjectService private var matomo: MatomoUtils
@LazyInjectService private var draftManager: DraftManager
+ @LazyInjectService private var snackbarPresenter: SnackBarPresentable
@State private var isLoadingContent = true
@State private var isShowingCancelAttachmentsError = false
@State private var autocompletionType: ComposeViewFieldType?
@State private var editorFocus = false
@State private var currentSignature: Signature?
+ @State private var initialAttachments = [Attachable]()
@State private var editorModel = RichTextEditorModel()
@State private var scrollView: UIScrollView?
@@ -98,7 +102,7 @@ struct ComposeMessageView: View {
// MARK: - Init
- init(draft: Draft, mailboxManager: MailboxManager, messageReply: MessageReply? = nil) {
+ init(draft: Draft, mailboxManager: MailboxManager, messageReply: MessageReply? = nil, attachments: [Attachable] = []) {
self.messageReply = messageReply
Self.saveNewDraftInRealm(mailboxManager.getRealm(), draft: draft)
@@ -112,6 +116,7 @@ struct ComposeMessageView: View {
self.mailboxManager = mailboxManager
_attachmentsManager = StateObject(wrappedValue: AttachmentsManager(draft: draft, mailboxManager: mailboxManager))
+ _initialAttachments = State(wrappedValue: attachments)
}
// MARK: - View
@@ -120,6 +125,7 @@ struct ComposeMessageView: View {
NavigationView {
composeMessage
}
+ .navigationViewStyle(.stack)
.task {
do {
isLoadingContent = true
@@ -128,11 +134,14 @@ struct ComposeMessageView: View {
isLoadingContent = false
} catch {
// Unable to get signatures, "An error occurred" and close modal.
- IKSnackBar.showSnackBar(message: MailError.unknownError.localizedDescription)
+ snackbarPresenter.show(message: MailError.unknownError.localizedDescription)
dismiss()
}
}
.onAppear {
+ attachmentsManager.importAttachments(attachments: initialAttachments, draft: draft)
+ initialAttachments = []
+
switch messageReply?.replyMode {
case .reply, .replyAll:
focusedField = .editor
@@ -156,7 +165,7 @@ struct ComposeMessageView: View {
}
.customAlert(isPresented: $isShowingCancelAttachmentsError) {
AttachmentsUploadInProgressErrorView {
- dismiss()
+ dismissMessageView()
}
}
.matomoView(view: ["ComposeMessage"])
@@ -242,12 +251,18 @@ struct ComposeMessageView: View {
// MARK: - Func
+ /// Something to dismiss the view regardless of presentation context
+ private func dismissMessageView() {
+ dismissModal()
+ dismiss()
+ }
+
private func didTouchDismiss() {
guard attachmentsManager.allAttachmentsUploaded else {
isShowingCancelAttachmentsError = true
return
}
- dismiss()
+ dismissMessageView()
}
private func didTouchSend() {
@@ -266,7 +281,7 @@ struct ComposeMessageView: View {
liveDraft.action = .send
}
}
- dismiss()
+ dismissMessageView()
}
private static func saveNewDraftInRealm(_ realm: Realm, draft: Draft) {
diff --git a/Mail/Views/New Message/Header Cells/ComposeMessageCellRecipients.swift b/Mail/Views/New Message/Header Cells/ComposeMessageCellRecipients.swift
index 9d684b9f2..41d1b1129 100644
--- a/Mail/Views/New Message/Header Cells/ComposeMessageCellRecipients.swift
+++ b/Mail/Views/New Message/Header Cells/ComposeMessageCellRecipients.swift
@@ -48,6 +48,9 @@ struct ComposeMessageCellRecipients: View {
@FocusState var focusedField: ComposeViewFieldType?
+ @LazyInjectService private var snackbarPresenter: SnackBarPresentable
+ @LazyInjectService private var matomo: MatomoUtils
+
let type: ComposeViewFieldType
var areCCAndBCCEmpty = false
@@ -111,16 +114,15 @@ struct ComposeMessageCellRecipients: View {
}
@MainActor private func addNewRecipient(_ recipient: Recipient) {
- @InjectService var matomo: MatomoUtils
matomo.track(eventWithCategory: .newMessage, name: "addNewRecipient")
guard Constants.isEmailAddress(recipient.email) else {
- IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.addUnknownRecipientInvalidEmail)
+ snackbarPresenter.show(message: MailResourcesStrings.Localizable.addUnknownRecipientInvalidEmail)
return
}
guard !recipients.contains(where: { $0.isSameRecipient(as: recipient) }) else {
- IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.addUnknownRecipientAlreadyUsed)
+ snackbarPresenter.show(message: MailResourcesStrings.Localizable.addUnknownRecipientAlreadyUsed)
return
}
diff --git a/Mail/Views/New Message/Recipients/FullRecipientsList.swift b/Mail/Views/New Message/Recipients/FullRecipientsList.swift
index 2b597caa5..5ece0bed4 100644
--- a/Mail/Views/New Message/Recipients/FullRecipientsList.swift
+++ b/Mail/Views/New Message/Recipients/FullRecipientsList.swift
@@ -16,12 +16,12 @@
along with this program. If not, see .
*/
+import InfomaniakCoreUI
+import InfomaniakDI
import MailCore
import RealmSwift
import SwiftUI
import WrappingHStack
-import InfomaniakDI
-import InfomaniakCoreUI
struct FullRecipientsList: View {
@EnvironmentObject private var mailboxManager: MailboxManager
diff --git a/Mail/Views/New Message/Recipients/RecipientChip.swift b/Mail/Views/New Message/Recipients/RecipientChip.swift
index abbfbd5cf..8fec6017e 100644
--- a/Mail/Views/New Message/Recipients/RecipientChip.swift
+++ b/Mail/Views/New Message/Recipients/RecipientChip.swift
@@ -17,6 +17,7 @@
*/
import InfomaniakCoreUI
+import InfomaniakDI
import MailCore
import MailResources
import Popovers
@@ -26,6 +27,7 @@ struct RecipientChip: View {
@EnvironmentObject private var mailboxManager: MailboxManager
@AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor
+ @LazyInjectService private var snackbarPresenter: SnackBarPresentable
let recipient: Recipient
let fieldType: ComposeViewFieldType
@@ -48,7 +50,7 @@ struct RecipientChip: View {
Templates.MenuButton(text: Text(MailResourcesStrings.Localizable.contactActionCopyEmailAddress),
image: MailResourcesAsset.duplicate.swiftUIImage) {
UIPasteboard.general.string = recipient.email
- IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.snackbarEmailCopiedToClipboard)
+ snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarEmailCopiedToClipboard)
}
Templates.MenuButton(text: Text(MailResourcesStrings.Localizable.actionDelete),
diff --git a/Mail/Views/Onboarding/OnboardingView.swift b/Mail/Views/Onboarding/OnboardingView.swift
index 3aa1aa7bf..5c6bff498 100644
--- a/Mail/Views/Onboarding/OnboardingView.swift
+++ b/Mail/Views/Onboarding/OnboardingView.swift
@@ -68,9 +68,12 @@ struct Slide: Identifiable {
}
@MainActor
-class LoginHandler: InfomaniakLoginDelegate, ObservableObject {
- @LazyInjectService var loginService: InfomaniakLoginable
- @LazyInjectService var matomo: MatomoUtils
+final class LoginHandler: InfomaniakLoginDelegate, ObservableObject {
+ @LazyInjectService private var loginService: InfomaniakLoginable
+ @LazyInjectService private var matomo: MatomoUtils
+ @LazyInjectService private var remoteNotificationRegistrer: RemoteNotificationRegistrable
+ @LazyInjectService private var accountManager: AccountManager
+ @LazyInjectService private var snackbarPresenter: SnackBarPresentable
@Published var isLoading = false
@Published var isPresentingErrorAlert = false
@@ -123,18 +126,18 @@ class LoginHandler: InfomaniakLoginDelegate, ObservableObject {
private func loginSuccessful(code: String, codeVerifier verifier: String) {
matomo.track(eventWithCategory: .account, name: "loggedIn")
- let previousAccount = AccountManager.instance.getCurrentAccount()
+ let previousAccount = accountManager.getCurrentAccount()
Task {
do {
- _ = try await AccountManager.instance.createAndSetCurrentAccount(code: code, codeVerifier: verifier)
- UIApplication.shared.registerForRemoteNotifications()
+ _ = try await accountManager.createAndSetCurrentAccount(code: code, codeVerifier: verifier)
+ remoteNotificationRegistrer.register()
} catch let error as MailError where error == MailError.noMailbox {
shouldShowEmptyMailboxesView = true
} catch {
if let previousAccount {
- AccountManager.instance.switchAccount(newAccount: previousAccount)
+ accountManager.switchAccount(newAccount: previousAccount)
}
- IKSnackBar.showSnackBar(message: error.localizedDescription)
+ snackbarPresenter.show(message: error.localizedDescription)
}
isLoading = false
}
@@ -150,6 +153,8 @@ class LoginHandler: InfomaniakLoginDelegate, ObservableObject {
struct OnboardingView: View {
@Environment(\.dismiss) private var dismiss
+ @LazyInjectService var orientationManager: OrientationManageable
+
@EnvironmentObject private var navigationState: NavigationState
@AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor
@@ -245,7 +250,7 @@ struct OnboardingView: View {
if UIDevice.current.userInterfaceIdiom == .phone {
UIDevice.current
.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
- AppDelegate.orientationLock = .portrait
+ orientationManager.setOrientationLock(.portrait)
UIViewController.attemptRotationToDeviceOrientation()
}
}
diff --git a/Mail/Views/Settings/General/SettingsNotificationsInstructionsView.swift b/Mail/Views/Settings/General/SettingsNotificationsInstructionsView.swift
index 22d00fe0d..dc4046dcb 100644
--- a/Mail/Views/Settings/General/SettingsNotificationsInstructionsView.swift
+++ b/Mail/Views/Settings/General/SettingsNotificationsInstructionsView.swift
@@ -16,11 +16,13 @@
along with this program. If not, see .
*/
+import InfomaniakDI
import MailResources
import SwiftUI
struct SettingsNotificationsInstructionsView: View {
@Environment(\.dismiss) private var dismiss
+ @Environment(\.openURL) var openURL
var body: some View {
VStack(alignment: .leading, spacing: 24) {
@@ -40,9 +42,7 @@ struct SettingsNotificationsInstructionsView: View {
return
}
- if UIApplication.shared.canOpenURL(settingsUrl) {
- UIApplication.shared.open(settingsUrl)
- }
+ openURL(settingsUrl)
}
}
diff --git a/Mail/Views/Settings/General/SettingsNotificationsView.swift b/Mail/Views/Settings/General/SettingsNotificationsView.swift
index 36c3b90e5..0d6e939b6 100644
--- a/Mail/Views/Settings/General/SettingsNotificationsView.swift
+++ b/Mail/Views/Settings/General/SettingsNotificationsView.swift
@@ -27,9 +27,12 @@ import SwiftUI
struct SettingsNotificationsView: View {
@LazyInjectService private var notificationService: InfomaniakNotifications
@LazyInjectService private var matomo: MatomoUtils
+ @LazyInjectService private var accountManager: AccountManager
@EnvironmentObject private var mailboxManager: MailboxManager
+ @Environment(\.openURL) private var openURL
+
@AppStorage(UserDefaults.shared.key(.notificationsEnabled)) private var notificationsEnabled = DefaultPreferences
.notificationsEnabled
@State var subscribedTopics: [String]?
@@ -50,9 +53,7 @@ struct SettingsNotificationsView: View {
return
}
- if UIApplication.shared.canOpenURL(settingsUrl) {
- UIApplication.shared.open(settingsUrl)
- }
+ openURL(settingsUrl)
}
.mailButtonStyle(.link)
}
diff --git a/Mail/Views/Settings/General/SettingsView.swift b/Mail/Views/Settings/General/SettingsView.swift
index ddafc07bd..96816125b 100644
--- a/Mail/Views/Settings/General/SettingsView.swift
+++ b/Mail/Views/Settings/General/SettingsView.swift
@@ -24,6 +24,8 @@ import MailResources
import SwiftUI
struct SettingsView: View {
+ @InjectService private var accountManager: AccountManager
+
@EnvironmentObject private var mailboxManager: MailboxManager
@LazyInjectService private var appLockHelper: AppLockHelper
@@ -40,7 +42,7 @@ struct SettingsView: View {
.textStyle(.bodySmallSecondary)
ForEachMailboxView(userId: mailboxManager.account.userId) { mailbox in
- if let mailboxManager = AccountManager.instance.getMailboxManager(for: mailbox) {
+ if let mailboxManager = accountManager.getMailboxManager(for: mailbox) {
SettingsSubMenuCell(title: mailbox.email) {
MailboxSettingsView(mailboxManager: mailboxManager)
}
diff --git a/Mail/Views/Settings/SettingsOptionView.swift b/Mail/Views/Settings/SettingsOptionView.swift
index 77ec230ca..56922d9a3 100644
--- a/Mail/Views/Settings/SettingsOptionView.swift
+++ b/Mail/Views/Settings/SettingsOptionView.swift
@@ -34,6 +34,8 @@ struct SettingsOptionView: View where OptionEnum: CaseIterable, Opti
private let matomoValue: Float?
private let matomoName: KeyPath?
+ @LazyInjectService private var matomo: MatomoUtils
+
@State private var values: [OptionEnum]
@State private var selectedValue: OptionEnum {
didSet {
@@ -51,8 +53,6 @@ struct SettingsOptionView: View where OptionEnum: CaseIterable, Opti
}
}
- @LazyInjectService private var matomo: MatomoUtils
-
init(title: String,
subtitle: String? = nil,
values: [OptionEnum] = Array(OptionEnum.allCases),
diff --git a/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift
index 693cf344b..32060a556 100644
--- a/Mail/Views/SplitView.swift
+++ b/Mail/Views/SplitView.swift
@@ -19,6 +19,7 @@
import InfomaniakBugTracker
import InfomaniakCore
import InfomaniakCoreUI
+import InfomaniakDI
import Introspect
import MailCore
import MailResources
@@ -27,11 +28,17 @@ import RealmSwift
import SwiftUI
public class SplitViewManager: ObservableObject {
+ @LazyInjectService private var platformDetector: PlatformDetectable
+
@Published var showSearch = false
@Published var selectedFolder: Folder?
var splitViewController: UISplitViewController?
func adaptToProminentThreadView() {
+ guard !platformDetector.isMacCatalyst, !platformDetector.isiOSAppOnMac else {
+ return
+ }
+
splitViewController?.hide(.primary)
if splitViewController?.splitBehavior == .overlay {
splitViewController?.hide(.supplementary)
@@ -51,6 +58,10 @@ struct SplitView: View {
@StateObject private var navigationDrawerController = NavigationDrawerState()
@StateObject private var splitViewManager = SplitViewManager()
+ @LazyInjectService private var orientationManager: OrientationManageable
+ @LazyInjectService private var snackbarPresenter: SnackBarPresentable
+ @LazyInjectService private var platformDetector: PlatformDetectable
+
let mailboxManager: MailboxManager
var body: some View {
@@ -114,14 +125,14 @@ struct SplitView: View {
if let tappedNotificationThread = tappedNotificationMessage?.originalThread {
navigationState.threadPath = [tappedNotificationThread]
} else {
- IKSnackBar.showSnackBar(message: MailError.localMessageNotFound.errorDescription)
+ snackbarPresenter.show(message: MailError.localMessageNotFound.errorDescription)
}
}
.onReceive(NotificationCenter.default.publisher(for: .onOpenedMailTo)) { identifiableURLComponents in
mailToURLComponents = identifiableURLComponents.object as? IdentifiableURLComponents
}
.onAppear {
- AppDelegate.orientationLock = .all
+ orientationManager.setOrientationLock(.all)
}
.task(id: mailboxManager.mailbox.objectId) {
await fetchSignatures()
@@ -148,7 +159,10 @@ struct SplitView: View {
}
private func setupBehaviour(orientation: UIInterfaceOrientation) {
- if orientation.isLandscape {
+ if platformDetector.isMacCatalyst || platformDetector.isiOSAppOnMac {
+ splitViewController?.preferredSplitBehavior = .tile
+ splitViewController?.preferredDisplayMode = .twoBesideSecondary
+ } else if orientation.isLandscape {
splitViewController?.preferredSplitBehavior = .displace
splitViewController?.preferredDisplayMode = splitViewManager.selectedFolder == nil
? .twoDisplaceSecondary
diff --git a/Mail/Views/Switch User/AccountCellView.swift b/Mail/Views/Switch User/AccountCellView.swift
index 64482eb3f..a07d12074 100644
--- a/Mail/Views/Switch User/AccountCellView.swift
+++ b/Mail/Views/Switch User/AccountCellView.swift
@@ -26,6 +26,8 @@ import RealmSwift
import SwiftUI
struct AccountCellView: View {
+ @LazyInjectService private var accountManager: AccountManager
+
@Environment(\.dismissModal) var dismissModal
let account: Account
@@ -50,7 +52,7 @@ struct AccountCellView: View {
@InjectService var matomo: MatomoUtils
matomo.track(eventWithCategory: .account, name: "switch")
dismissModal()
- AccountManager.instance.switchAccount(newAccount: account)
+ accountManager.switchAccount(newAccount: account)
} label: {
AccountHeaderCell(account: account, isSelected: Binding(get: {
isSelected
diff --git a/Mail/Views/Switch User/AccountListView.swift b/Mail/Views/Switch User/AccountListView.swift
index 8e309e603..d6dcd8b53 100644
--- a/Mail/Views/Switch User/AccountListView.swift
+++ b/Mail/Views/Switch User/AccountListView.swift
@@ -24,8 +24,13 @@ import MailResources
import RealmSwift
import SwiftUI
-class AccountListViewModel: ObservableObject {
- @Published var selectedUserId: Int? = AccountManager.instance.currentUserId
+final class AccountListViewModel: ObservableObject {
+ @LazyInjectService private var accountManager: AccountManager
+
+ @Published var selectedUserId: Int? = {
+ @InjectService var accountManager: AccountManager
+ return accountManager.currentUserId
+ }()
@Published var accounts = [Account: [Mailbox]]()
@@ -50,7 +55,7 @@ class AccountListViewModel: ObservableObject {
}
private func handleMailboxChanged(_ mailboxes: [Mailbox]) {
- for account in AccountManager.instance.accounts {
+ for account in accountManager.accounts {
accounts[account] = mailboxes.filter { $0.userId == account.userId }
}
}
@@ -61,6 +66,8 @@ struct AccountListView: View {
@State var isShowingNewAccountView = false
@LazyInjectService private var matomo: MatomoUtils
+ @LazyInjectService private var orientationManager: OrientationManageable
+ @LazyInjectService private var accountManager: AccountManager
var body: some View {
ScrollView {
@@ -79,7 +86,7 @@ struct AccountListView: View {
isShowingNewAccountView = true
}
.fullScreenCover(isPresented: $isShowingNewAccountView, onDismiss: {
- AppDelegate.orientationLock = .all
+ orientationManager.setOrientationLock(.all)
}, content: {
OnboardingView(page: 4, isScrollEnabled: false)
})
@@ -91,9 +98,9 @@ struct AccountListView: View {
private func updateUsers() async throws {
await withThrowingTaskGroup(of: Void.self) { group in
- for account in AccountManager.instance.accounts {
+ for account in accountManager.accounts {
group.addTask {
- _ = try await AccountManager.instance.updateUser(for: account)
+ _ = try await accountManager.updateUser(for: account)
}
}
}
diff --git a/Mail/Views/Switch User/AccountView.swift b/Mail/Views/Switch User/AccountView.swift
index 78a554ba0..325c219b8 100644
--- a/Mail/Views/Switch User/AccountView.swift
+++ b/Mail/Views/Switch User/AccountView.swift
@@ -25,29 +25,32 @@ import MailResources
import Sentry
import SwiftUI
-class AccountViewDelegate: DeleteAccountDelegate {
+final class AccountViewDelegate: DeleteAccountDelegate {
+ @LazyInjectService private var accountManager: AccountManager
+ @LazyInjectService private var snackbarPresenter: SnackBarPresentable
+
@MainActor func didCompleteDeleteAccount() {
Task {
- guard let account = AccountManager.instance.getCurrentAccount() else { return }
- AccountManager.instance.removeTokenAndAccount(account: account)
- if let nextAccount = AccountManager.instance.accounts.first {
- AccountManager.instance.switchAccount(newAccount: nextAccount)
- IKSnackBar.showSnackBar(message: "Account deleted")
+ guard let account = accountManager.getCurrentAccount() else { return }
+ accountManager.removeTokenAndAccount(account: account)
+ if let nextAccount = accountManager.accounts.first {
+ accountManager.switchAccount(newAccount: nextAccount)
+ snackbarPresenter.show(message: "Account deleted")
}
- AccountManager.instance.saveAccounts()
+ accountManager.saveAccounts()
}
}
@MainActor func didFailDeleteAccount(error: InfomaniakLoginError) {
SentrySDK.capture(error: error)
- IKSnackBar.showSnackBar(message: "Failed to delete account")
+ snackbarPresenter.show(message: "Failed to delete account")
}
}
struct AccountView: View {
@LazyInjectService private var matomo: MatomoUtils
@LazyInjectService private var tokenStore: TokenStore
-
+
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var mailboxManager: MailboxManager
diff --git a/Mail/Views/Switch User/AddMailboxView.swift b/Mail/Views/Switch User/AddMailboxView.swift
index 98dd85a93..b09e5a34a 100644
--- a/Mail/Views/Switch User/AddMailboxView.swift
+++ b/Mail/Views/Switch User/AddMailboxView.swift
@@ -25,6 +25,9 @@ import SwiftUI
struct AddMailboxView: View {
@Environment(\.dismiss) var dismiss
+ @LazyInjectService private var accountManager: AccountManager
+ @LazyInjectService private var snackbarPresenter: SnackBarPresentable
+
@State private var newAddress = ""
@State private var password = ""
@State private var showError = false
@@ -123,7 +126,7 @@ struct AddMailboxView: View {
Task {
do {
isButtonLoading = true
- try await AccountManager.instance.addMailbox(mail: newAddress, password: password)
+ try await accountManager.addMailbox(mail: newAddress, password: password)
isButtonLoading = false
} catch let error as MailApiError where error == .apiInvalidCredential {
withAnimation {
@@ -136,7 +139,7 @@ struct AddMailboxView: View {
password = ""
isButtonLoading = false
}
- await IKSnackBar.showSnackBar(message: error.localizedDescription)
+ snackbarPresenter.show(message: error.localizedDescription)
}
}
}
diff --git a/Mail/Views/Thread/ThreadView.swift b/Mail/Views/Thread/ThreadView.swift
index 4cec2d5f0..2ffa9d1b5 100644
--- a/Mail/Views/Thread/ThreadView.swift
+++ b/Mail/Views/Thread/ThreadView.swift
@@ -153,14 +153,16 @@ struct ThreadView: View {
}
switch action {
case .reply:
- guard let message = thread.lastMessageToExecuteAction(currentMailboxEmail: mailboxManager.mailbox.email) else { return }
+ guard let message = thread.lastMessageToExecuteAction(currentMailboxEmail: mailboxManager.mailbox.email)
+ else { return }
if message.canReplyAll(currentMailboxEmail: mailboxManager.mailbox.email) {
replyOrReplyAllMessage = message
} else {
navigationState.messageReply = MessageReply(message: message, replyMode: .reply)
}
case .forward:
- guard let message = thread.lastMessageToExecuteAction(currentMailboxEmail: mailboxManager.mailbox.email) else { return }
+ guard let message = thread.lastMessageToExecuteAction(currentMailboxEmail: mailboxManager.mailbox.email)
+ else { return }
navigationState.messageReply = MessageReply(message: message, replyMode: .forward)
case .archive:
Task {
diff --git a/Mail/Views/Thread/WebView.swift b/Mail/Views/Thread/WebView.swift
index 05265a846..59195b736 100644
--- a/Mail/Views/Thread/WebView.swift
+++ b/Mail/Views/Thread/WebView.swift
@@ -17,6 +17,7 @@
*/
import Combine
+import InfomaniakDI
import MailCore
import SwiftUI
import WebKit
@@ -132,7 +133,7 @@ extension WebViewController: WKNavigationDelegate {
if navigationAction.navigationType == .linkActivated {
if let url = navigationAction.request.url {
decisionHandler(.cancel)
- UIApplication.shared.open(url)
+ openURL?(url)
}
} else {
decisionHandler(.allow)
diff --git a/Mail/Views/Unavailable Mailbox/UnavailableMailboxesView.swift b/Mail/Views/Unavailable Mailbox/UnavailableMailboxesView.swift
index 46f4d8c74..10252b0b3 100644
--- a/Mail/Views/Unavailable Mailbox/UnavailableMailboxesView.swift
+++ b/Mail/Views/Unavailable Mailbox/UnavailableMailboxesView.swift
@@ -23,6 +23,7 @@ import MailResources
import SwiftUI
struct UnavailableMailboxesView: View {
+ @LazyInjectService private var orientationManager: OrientationManageable
@LazyInjectService private var matomo: MatomoUtils
@State private var isShowingNewAccountView = false
@@ -86,7 +87,7 @@ struct UnavailableMailboxesView: View {
}
.navigationViewStyle(.stack)
.fullScreenCover(isPresented: $isShowingNewAccountView) {
- AppDelegate.orientationLock = .all
+ orientationManager.setOrientationLock(.all)
} content: {
OnboardingView(page: 4, isScrollEnabled: false)
}
diff --git a/Mail/Views/Unavailable Mailbox/UpdateMailboxPasswordView.swift b/Mail/Views/Unavailable Mailbox/UpdateMailboxPasswordView.swift
index 281bccc7d..2f4840e37 100644
--- a/Mail/Views/Unavailable Mailbox/UpdateMailboxPasswordView.swift
+++ b/Mail/Views/Unavailable Mailbox/UpdateMailboxPasswordView.swift
@@ -23,10 +23,11 @@ import MailResources
import SwiftUI
struct UpdateMailboxPasswordView: View {
- @EnvironmentObject private var navigationState: NavigationState
-
+ @LazyInjectService private var accountManager: AccountManager
@LazyInjectService private var matomo: MatomoUtils
+ @EnvironmentObject private var navigationState: NavigationState
+
@State private var updatedMailboxPassword = ""
@State private var isShowingError = false
@State private var isLoading = false
@@ -115,7 +116,7 @@ struct UpdateMailboxPasswordView: View {
Task {
isLoading = true
do {
- try await AccountManager.instance.updateMailboxPassword(mailbox: mailbox, password: updatedMailboxPassword)
+ try await accountManager.updateMailboxPassword(mailbox: mailbox, password: updatedMailboxPassword)
navigationState.transitionToRootViewDestination(.mainView)
} catch {
isShowingError = true
diff --git a/MailCore/API/MailApiFetcher.swift b/MailCore/API/MailApiFetcher.swift
index 604c96041..6bac549f0 100644
--- a/MailCore/API/MailApiFetcher.swift
+++ b/MailCore/API/MailApiFetcher.swift
@@ -374,7 +374,7 @@ public class MailApiFetcher: ApiFetcher {
}
}
-class SyncedAuthenticator: OAuthAuthenticator {
+final class SyncedAuthenticator: OAuthAuthenticator {
func handleFailedRefreshingToken(oldToken: ApiToken, error: Error?) -> Result {
guard let error = error as NSError?,
error.domain == "invalid_grant" else {
diff --git a/MailCore/Cache/AccountManager.swift b/MailCore/Cache/AccountManager.swift
index 35fbe5cc1..639d38fce 100644
--- a/MailCore/Cache/AccountManager.swift
+++ b/MailCore/Cache/AccountManager.swift
@@ -59,7 +59,7 @@ public extension InfomaniakNetworkLoginable {
}
}
-public class AccountManager: RefreshTokenDelegate, ObservableObject {
+public final class AccountManager: RefreshTokenDelegate, ObservableObject {
@LazyInjectService var networkLoginService: InfomaniakNetworkLoginable
@LazyInjectService var tokenStore: TokenStore
@LazyInjectService var bugTracker: BugTracker
@@ -68,12 +68,18 @@ public class AccountManager: RefreshTokenDelegate, ObservableObject {
private static let appIdentifierPrefix = Bundle.main.infoDictionary!["AppIdentifierPrefix"] as! String
private static let group = "com.infomaniak.mail"
+
+ private let tag = "ch.infomaniak.token".data(using: .utf8)!
+ private var currentAccount: Account?
+
public static let appGroup = "group." + group
public static let accessGroup: String = AccountManager.appIdentifierPrefix + AccountManager.group
- public static var instance = AccountManager()
- private var currentAccount: Account?
public var accounts = SendableArray()
+ public var tokens = [ApiToken]()
+ public let refreshTokenLockedQueue = DispatchQueue(label: "com.infomaniak.mail.refreshtoken")
+ public static var instance = AccountManager()
+
public weak var delegate: AccountManagerDelegate?
public var currentUserId: Int {
didSet {
@@ -102,6 +108,11 @@ public class AccountManager: RefreshTokenDelegate, ObservableObject {
}
}
+ /// Shorthand for `currentMailboxManager?.contactManager`
+ public var currentContactManager: ContactManager? {
+ currentMailboxManager?.contactManager
+ }
+
public var currentApiFetcher: MailApiFetcher? {
return apiFetchers[currentUserId]
}
@@ -110,7 +121,7 @@ public class AccountManager: RefreshTokenDelegate, ObservableObject {
private let contactManagers = SendableDictionary()
private let apiFetchers = SendableDictionary()
- private init() {
+ public init() {
currentMailboxId = UserDefaults.shared.currentMailboxId
currentUserId = UserDefaults.shared.currentMailUserId
@@ -207,7 +218,7 @@ public class AccountManager: RefreshTokenDelegate, ObservableObject {
}
tokenStore.removeTokenFor(userId: token.userId)
if let account = account(for: token.userId),
- account.userId == currentUserId {
+ account.userId == currentUserId {
delegate?.currentAccountNeedsAuthentication()
NotificationsHelper.sendDisconnectedNotification()
}
diff --git a/MailCore/Cache/BackgroundRealm.swift b/MailCore/Cache/BackgroundRealm.swift
index 533c908f1..eec14f5b2 100644
--- a/MailCore/Cache/BackgroundRealm.swift
+++ b/MailCore/Cache/BackgroundRealm.swift
@@ -20,11 +20,11 @@ import Foundation
import RealmSwift
import Sentry
-public class BackgroundRealm {
+public final class BackgroundRealm {
private let configuration: Realm.Configuration
private let queue: DispatchQueue
- init(configuration: Realm.Configuration) {
+ public init(configuration: Realm.Configuration) {
guard let fileURL = configuration.fileURL else {
fatalError("Realm configurations without file URL not supported")
}
diff --git a/MailCore/Cache/DraftManager.swift b/MailCore/Cache/DraftManager.swift
index cd6bb57b1..17e2c1fdf 100644
--- a/MailCore/Cache/DraftManager.swift
+++ b/MailCore/Cache/DraftManager.swift
@@ -70,6 +70,7 @@ public final class DraftManager {
private static let saveExpirationSec = 3
@LazyInjectService private var matomo: MatomoUtils
+ @LazyInjectService private var alertDisplayable: UserAlertDisplayable
/// Used by DI only
public init() {
@@ -91,15 +92,14 @@ public final class DraftManager {
do {
try await mailboxManager.save(draft: draft)
} catch {
- if error.shouldDisplay {
- await IKSnackBar.showSnackBar(message: error.localizedDescription)
- }
+ guard error.shouldDisplay else { return }
+ alertDisplayable.show(message: error.localizedDescription)
}
await draftQueue.endBackgroundTask(uuid: draft.localUUID)
}
public func send(draft: Draft, mailboxManager: MailboxManager) async -> Date? {
- await IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.snackbarEmailSending)
+ alertDisplayable.show(message: MailResourcesStrings.Localizable.snackbarEmailSending)
var sendDate: Date?
await draftQueue.cleanQueueElement(uuid: draft.localUUID)
@@ -107,10 +107,10 @@ public final class DraftManager {
do {
let cancelableResponse = try await mailboxManager.send(draft: draft)
- await IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.snackbarEmailSent)
+ alertDisplayable.show(message: MailResourcesStrings.Localizable.snackbarEmailSent)
sendDate = cancelableResponse.scheduledDate
} catch {
- await IKSnackBar.showSnackBar(message: error.localizedDescription)
+ alertDisplayable.show(message: error.localizedDescription)
}
await draftQueue.endBackgroundTask(uuid: draft.localUUID)
return sendDate
@@ -125,7 +125,7 @@ public final class DraftManager {
var sendDate: Date?
switch draft.action {
case .initialSave:
- await self.initialSave(draft: draft, mailboxManager: mailboxManager)
+ await self.initialSaveRemotely(draft: draft, mailboxManager: mailboxManager)
case .save:
await self.saveDraftRemotely(draft: draft, mailboxManager: mailboxManager)
case .send:
@@ -148,8 +148,48 @@ public final class DraftManager {
}
}
+ /// First save of a draft with the remote, if non empty.
+ ///
+ /// Present a message with a `delete draft` action
+ @discardableResult
+ public func initialSaveRemotely(draft: Draft, mailboxManager: MailboxManager) async -> Bool {
+ guard !isDraftEmpty(draft: draft) else {
+ deleteEmptyDraft(draft: draft, for: mailboxManager)
+ return false
+ }
+
+ await saveDraftRemotely(draft: draft, mailboxManager: mailboxManager)
+
+ let messageAction: UserAlertAction = (MailResourcesStrings.Localizable.actionDelete, { [weak self] in
+ self?.matomo.track(eventWithCategory: .snackbar, name: "deleteDraft")
+ self?.deleteDraftSnackBarAction(draft: draft, mailboxManager: mailboxManager)
+ })
+ alertDisplayable.show(message: MailResourcesStrings.Localizable.snackbarDraftSaved, action: messageAction)
+
+ return true
+ }
+
+ /// Check multiple conditions to infer if a draft is empty or not
+ private func isDraftEmpty(draft: Draft) -> Bool {
+ guard isDraftBodyEmptyOfAttachments(draft: draft) else {
+ return false
+ }
+
+ guard (try? isDraftBodyEmptyOfChanges(draft.body)) ?? true else {
+ return false
+ }
+
+ return true
+ }
+
+ /// Check that the draft has some Attachments of not
+ private func isDraftBodyEmptyOfAttachments(draft: Draft) -> Bool {
+ // This excludes the signature attachments that are present in Draft.attachments
+ return draft.attachments.filter { $0.contentId == nil }.isEmpty
+ }
+
/// Check if once the Signature node is removed, we still have content
- func isDraftBodyEmptyOfChanges(_ body: String) throws -> Bool {
+ internal func isDraftBodyEmptyOfChanges(_ body: String) throws -> Bool {
guard !body.isEmpty else {
return true
}
@@ -167,22 +207,6 @@ public final class DraftManager {
return !document.hasText()
}
- private func initialSave(draft: Draft, mailboxManager: MailboxManager) async {
- // We consider the body to be not-empty on HTML parsing failure to keep user content.
- let isDraftEmpty = (try? isDraftBodyEmptyOfChanges(draft.body)) ?? false
- guard !isDraftEmpty else {
- deleteEmptyDraft(draft: draft, for: mailboxManager)
- return
- }
-
- await saveDraftRemotely(draft: draft, mailboxManager: mailboxManager)
- await IKSnackBar.showSnackBar(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)
- })
- }
-
private func refreshDraftFolder(latestSendDate: Date?, mailboxManager: MailboxManager) async throws {
if let draftFolder = mailboxManager.getFolder(with: .draft)?.freeze() {
await mailboxManager.refresh(folder: draftFolder)
@@ -206,7 +230,7 @@ public final class DraftManager {
await tryOrDisplayError {
if let liveDraft = draft.thaw() {
try await mailboxManager.delete(draft: liveDraft.freeze())
- await IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.snackbarDraftDeleted)
+ alertDisplayable.show(message: MailResourcesStrings.Localizable.snackbarDraftDeleted)
if let draftFolder = mailboxManager.getFolder(with: .draft)?.freeze() {
await mailboxManager.refresh(folder: draftFolder)
}
diff --git a/MailCore/Cache/MailboxManager.swift b/MailCore/Cache/MailboxManager.swift
index cf663a616..343316c13 100644
--- a/MailCore/Cache/MailboxManager.swift
+++ b/MailCore/Cache/MailboxManager.swift
@@ -20,38 +20,28 @@ import CocoaLumberjackSwift
import Foundation
import InfomaniakCore
import InfomaniakCoreUI
+import InfomaniakDI
import MailResources
import RealmSwift
import Sentry
import SwiftRegex
-public class MailboxManager: ObservableObject {
- public class MailboxManagerConstants {
+public final class MailboxManager: ObservableObject {
+ @LazyInjectService private var snackbarPresenter: SnackBarPresentable
+
+ public final class MailboxManagerConstants {
private let fileManager = FileManager.default
public let rootDocumentsURL: URL
public let groupDirectoryURL: URL
public let cacheDirectoryURL: URL
init() {
- groupDirectoryURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: AccountManager.appGroup)!
- rootDocumentsURL = groupDirectoryURL.appendingPathComponent("mailboxes", isDirectory: true)
- cacheDirectoryURL = groupDirectoryURL.appendingPathComponent("Library/Caches", isDirectory: true)
- print(groupDirectoryURL)
- try? fileManager.setAttributes(
- [FileAttributeKey.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication],
- ofItemAtPath: groupDirectoryURL.path
- )
- try? FileManager.default.createDirectory(
- atPath: rootDocumentsURL.path,
- withIntermediateDirectories: true,
- attributes: nil
- )
- try? FileManager.default.createDirectory(
- atPath: cacheDirectoryURL.path,
- withIntermediateDirectories: true,
- attributes: nil
- )
+ @InjectService var appGroupPathProvider: AppGroupPathProvidable
+ groupDirectoryURL = appGroupPathProvider.groupDirectoryURL
+ rootDocumentsURL = appGroupPathProvider.realmRootURL
+ cacheDirectoryURL = appGroupPathProvider.cacheDirectoryURL
+ DDLogInfo("groupDirectoryURL: \(groupDirectoryURL)")
DDLogInfo(
"App working path is: \(fileManager.urls(for: .documentDirectory, in: .userDomainMask).first?.absoluteString ?? "")"
)
@@ -984,7 +974,7 @@ public class MailboxManager: ObservableObject {
|| firstMessageFolderRole == .spam
|| firstMessageFolderRole == .draft {
try await delete(messages: messagesToMoveOrDelete)
- async let _ = IKSnackBar.showSnackBar(message: deletionSnackbarMessage(for: messages, permanentlyDelete: true))
+ async let _ = snackbarPresenter.show(message: deletionSnackbarMessage(for: messages, permanentlyDelete: true))
} else {
let undoRedoAction = try await move(messages: messagesToMoveOrDelete, to: .trash)
async let _ = IKSnackBar.showCancelableSnackBar(
diff --git a/MailCore/Models/Draft.swift b/MailCore/Models/Draft.swift
index e76ddd6ee..28d031754 100644
--- a/MailCore/Models/Draft.swift
+++ b/MailCore/Models/Draft.swift
@@ -288,3 +288,13 @@ public final class Draft: Object, Codable, Identifiable {
try container.encode(delay, forKey: .delay)
}
}
+
+public extension Draft {
+ /// Returns the available attachments slots
+ var availableAttachmentsSlots: Int {
+ let maxBound = 96
+ let offset = min(attachments.count, maxBound)
+ let available = max(maxBound - offset, 0)
+ return available
+ }
+}
diff --git a/MailCore/Models/MergedContact.swift b/MailCore/Models/MergedContact.swift
index 7a251a3cf..4d06a0321 100644
--- a/MailCore/Models/MergedContact.swift
+++ b/MailCore/Models/MergedContact.swift
@@ -35,7 +35,7 @@ extension CNContact {
}
}
-public class MergedContact {
+public final class MergedContact {
private static let contactFormatter = CNContactFormatter()
public var email: String
diff --git a/MailCore/Models/Recipient.swift b/MailCore/Models/Recipient.swift
index b6abc01f1..3b5c5cd9d 100644
--- a/MailCore/Models/Recipient.swift
+++ b/MailCore/Models/Recipient.swift
@@ -37,7 +37,7 @@ public struct RecipientHolder {
var bcc = [Recipient]()
}
-public class Recipient: EmbeddedObject, Codable {
+public final class Recipient: EmbeddedObject, Codable {
@Persisted public var email: String
@Persisted public var name: String
diff --git a/MailCore/Models/Thread.swift b/MailCore/Models/Thread.swift
index 76cc5ff77..94bf51b31 100644
--- a/MailCore/Models/Thread.swift
+++ b/MailCore/Models/Thread.swift
@@ -157,7 +157,8 @@ public class Thread: Object, Decodable, Identifiable {
}
public func lastMessageToExecuteAction(currentMailboxEmail: String) -> Message? {
- if let message = messages.last(where: { $0.isDraft == false && $0.fromMe(currentMailboxEmail: currentMailboxEmail) == false }) {
+ if let message = messages
+ .last(where: { $0.isDraft == false && $0.fromMe(currentMailboxEmail: currentMailboxEmail) == false }) {
return message
} else if let message = messages.last(where: { $0.isDraft == false }) {
return message
diff --git a/MailCore/Utils/ApplicationStatable.swift b/MailCore/Utils/ApplicationStatable.swift
new file mode 100644
index 000000000..191042f88
--- /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 {
+ @MainActor var applicationState: UIApplication.State? { get }
+}
diff --git a/MailCore/Utils/Error+Extension.swift b/MailCore/Utils/Error+Extension.swift
index 7d147d170..fa1782784 100644
--- a/MailCore/Utils/Error+Extension.swift
+++ b/MailCore/Utils/Error+Extension.swift
@@ -19,6 +19,7 @@
import CocoaLumberjackSwift
import Foundation
import InfomaniakCoreUI
+import InfomaniakDI
import Sentry
public func tryOrDisplayError(_ body: () throws -> Void) {
@@ -38,11 +39,10 @@ public func tryOrDisplayError(_ body: () async throws -> Void) async {
}
private func displayErrorIfNeeded(error: Error) {
+ @InjectService var snackbarPresenter: SnackBarPresentable
if let error = error as? MailError {
- if error.shouldDisplay && !Bundle.main.isExtension {
- Task.detached {
- await IKSnackBar.showSnackBar(message: error.errorDescription)
- }
+ if error.shouldDisplay {
+ snackbarPresenter.show(message: error.errorDescription)
} else {
SentrySDK.capture(message: "Encountered error that we didn't display to the user") { scope in
scope.setContext(
@@ -52,23 +52,25 @@ private func displayErrorIfNeeded(error: Error) {
}
}
DDLogError("MailError: \(error)")
- } else if error.shouldDisplay && !Bundle.main.isExtension {
- Task.detached {
- await IKSnackBar.showSnackBar(message: error.localizedDescription)
- }
+ } else if error.shouldDisplay {
+ snackbarPresenter.show(message: error.localizedDescription)
DDLogError("Error: \(error)")
}
}
public extension Error {
var shouldDisplay: Bool {
+ guard !Bundle.main.isExtension else {
+ return false
+ }
+
switch asAFError {
case .explicitlyCancelled:
return false
case .sessionTaskFailed(let error):
return (error as NSError).code != NSURLErrorNotConnectedToInternet
default:
- return true
+ return false
}
}
}
diff --git a/MailCore/Utils/NotificationsHelper.swift b/MailCore/Utils/NotificationsHelper.swift
index 88d71e536..4f4562be0 100644
--- a/MailCore/Utils/NotificationsHelper.swift
+++ b/MailCore/Utils/NotificationsHelper.swift
@@ -60,13 +60,14 @@ public enum NotificationsHelper {
public static func getUnreadCount() async -> Int {
var totalUnreadCount = 0
@InjectService var notificationService: InfomaniakNotifications
+ @InjectService var accountManager: AccountManager
- for account in AccountManager.instance.accounts {
+ for account in accountManager.accounts {
let currentSubscription = await notificationService.subscriptionForUser(id: account.userId)
for mailbox in MailboxInfosManager.instance.getMailboxes(for: account.userId)
where currentSubscription?.topics.contains(mailbox.notificationTopicName) == true {
- if let mailboxManager = AccountManager.instance.getMailboxManager(for: mailbox) {
+ if let mailboxManager = accountManager.getMailboxManager(for: mailbox) {
totalUnreadCount += mailboxManager.getFolder(with: .inbox)?.unreadCount ?? 0
}
}
@@ -106,7 +107,8 @@ public enum NotificationsHelper {
private static func sendImmediately(notification: UNMutableNotificationContent, id: String,
action: IKSnackBar.Action? = nil) {
DispatchQueue.main.async {
- let isInBackground = Bundle.main.isExtension || UIApplication.shared.applicationState != .active
+ @LazyInjectService var applicationState: ApplicationStatable
+ let isInBackground = Bundle.main.isExtension || applicationState.applicationState != .active
if isInBackground {
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false)
diff --git a/MailCore/Utils/PlatformDetectable.swift b/MailCore/Utils/PlatformDetectable.swift
new file mode 100644
index 000000000..390150568
--- /dev/null
+++ b/MailCore/Utils/PlatformDetectable.swift
@@ -0,0 +1,59 @@
+/*
+ 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
+
+// TODO: Move to core
+
+/// Something to help with current running context
+public protocol PlatformDetectable {
+ /// We are running in Mac Catalyst mode
+ var isMacCatalyst: Bool { get }
+
+ /// We are running an iOS App on Mac
+ var isiOSAppOnMac: Bool { get }
+
+ /// We are running in extension mode
+ var isInExtension: Bool { get }
+}
+
+public struct PlatformDetector: PlatformDetectable {
+ public init() {
+ // META: Keep SonarCloud happy
+ }
+
+ public var isMacCatalyst: Bool = {
+ #if targetEnvironment(macCatalyst)
+ true
+ #else
+ false
+ #endif
+ }()
+
+ public var isiOSAppOnMac: Bool = {
+ ProcessInfo().isiOSAppOnMac
+ }()
+
+ public var isInExtension: Bool = {
+ guard Bundle.main.bundlePath.hasSuffix(".appex") else {
+ return false
+ }
+
+ return true
+ }()
+}
diff --git a/MailCore/Utils/IKSnackBar+Extension.swift b/MailCore/Utils/SnackBar/IKSnackBar+Extension.swift
similarity index 64%
rename from MailCore/Utils/IKSnackBar+Extension.swift
rename to MailCore/Utils/SnackBar/IKSnackBar+Extension.swift
index c430663df..98b915f55 100644
--- a/MailCore/Utils/IKSnackBar+Extension.swift
+++ b/MailCore/Utils/SnackBar/IKSnackBar+Extension.swift
@@ -22,6 +22,7 @@ import InfomaniakCoreUI
import InfomaniakDI
import MailResources
import SnackBar
+import UIKit
public extension SnackBarStyle {
static func mailStyle(withAnchor anchor: CGFloat) -> SnackBarStyle {
@@ -40,42 +41,51 @@ public extension SnackBarStyle {
}
}
-public class SnackBarAvoider {
- public var snackBarInset: CGFloat = 0
-
- public init() { /* Needed to init */ }
-
- public func addAvoider(inset: CGFloat) {
- if inset != snackBarInset {
- snackBarInset = inset
- }
- }
-
- public func removeAvoider() {
- snackBarInset = 0
- }
-}
-
public extension IKSnackBar {
@discardableResult
@MainActor
- static func showSnackBar(
+ /// Call this method to display a `SnackBar`
+ /// - Parameters:
+ /// - message: The message to display
+ /// - duration: The time the message should be displayed
+ /// - action: The action to perform if any
+ /// - anchor: The anchor to use for presenting
+ /// - contextView: Set a context view, when displaying in extension mode for eg.
+ /// - Returns: An IKSnackBar if any
+ static func showMailSnackBar(
message: String,
duration: SnackBar.Duration = .lengthLong,
action: IKSnackBar.Action? = nil,
- anchor: CGFloat = 0
+ anchor: CGFloat = 0,
+ contextView: UIView? = nil
) -> IKSnackBar? {
- @LazyInjectService var avoider: SnackBarAvoider
- let snackbar = IKSnackBar.make(
- message: message,
- duration: duration,
- style: .mailStyle(withAnchor: avoider.snackBarInset),
- elevation: 0
- )
+ @LazyInjectService var avoider: IKSnackBarAvoider
+
+ let snackbar: IKSnackBar?
+ if let contextView = contextView {
+ snackbar = IKSnackBar.make(
+ in: contextView,
+ message: message,
+ duration: duration,
+ style: .mailStyle(withAnchor: avoider.snackBarInset)
+ )
+ } else {
+ snackbar = IKSnackBar.make(
+ message: message,
+ duration: duration,
+ style: .mailStyle(withAnchor: avoider.snackBarInset),
+ elevation: 0
+ )
+ }
+
+ guard let snackbar = snackbar else {
+ return nil
+ }
+
if let action {
- snackbar?.setAction(action).show()
+ snackbar.setAction(action).show()
} else {
- snackbar?.show()
+ snackbar.show()
}
return snackbar
}
@@ -89,10 +99,11 @@ public extension IKSnackBar {
undoRedoAction: UndoRedoAction,
mailboxManager: MailboxManager
) -> IKSnackBar? {
- return IKSnackBar.showSnackBar(
+ return IKSnackBar.showMailSnackBar(
message: message,
duration: duration,
action: .init(title: MailResourcesStrings.Localizable.buttonCancel) {
+ @InjectService var snackbarPresenter: SnackBarPresentable
Task {
do {
@InjectService var matomo: MatomoUtils
@@ -101,11 +112,11 @@ public extension IKSnackBar {
let cancelled = try await mailboxManager.apiFetcher.undoAction(resource: undoRedoAction.undo.resource)
if cancelled {
- IKSnackBar.showSnackBar(message: cancelSuccessMessage)
+ snackbarPresenter.show(message: cancelSuccessMessage)
try await undoRedoAction.redo?()
}
} catch {
- IKSnackBar.showSnackBar(message: error.localizedDescription)
+ snackbarPresenter.show(message: error.localizedDescription)
}
}
}
diff --git a/MailCore/Utils/SnackBar/SnackBarPresentable.swift b/MailCore/Utils/SnackBar/SnackBarPresentable.swift
new file mode 100644
index 000000000..77010e85b
--- /dev/null
+++ b/MailCore/Utils/SnackBar/SnackBarPresentable.swift
@@ -0,0 +1,70 @@
+/*
+ Infomaniak Mail - iOS App
+ Copyright (C) 2022 Infomaniak Network SA
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+import Foundation
+import InfomaniakCoreUI
+import SnackBar
+import UIKit
+
+// todo use the version in CoreUI
+public protocol SnackBarPresentable {
+ func show(message: String)
+ func show(message: String, action: IKSnackBar.Action?)
+ func show(
+ message: String,
+ duration: SnackBar.Duration,
+ action: IKSnackBar.Action?,
+ anchor: CGFloat,
+ contextView: UIView?
+ )
+}
+
+public final class SnackBarPresenter: SnackBarPresentable {
+ /// Set to display the snack bar is a specific context, like
+ private var contextView: UIView?
+
+ public init(contextView: UIView? = nil) {
+ self.contextView = contextView
+ }
+
+ public func show(message: String) {
+ show(message: message, contextView: contextView)
+ }
+
+ public func show(message: String, action: IKSnackBar.Action?) {
+ show(message: message, action: action, contextView: nil)
+ }
+
+ public func show(
+ message: String,
+ duration: SnackBar.Duration = .lengthLong,
+ action: IKSnackBar.Action? = nil,
+ anchor: CGFloat = 0,
+ contextView: UIView? = nil
+ ) {
+ Task { @MainActor in
+ IKSnackBar.showMailSnackBar(
+ message: message,
+ duration: duration,
+ action: action,
+ anchor: anchor,
+ contextView: contextView
+ )
+ }
+ }
+}
diff --git a/MailCore/Utils/URLSchemeHandler.swift b/MailCore/Utils/URLSchemeHandler.swift
index 5ffebd9b2..2b7b7336c 100644
--- a/MailCore/Utils/URLSchemeHandler.swift
+++ b/MailCore/Utils/URLSchemeHandler.swift
@@ -17,22 +17,25 @@
*/
import Foundation
+import InfomaniakDI
import WebKit
-public class URLSchemeHandler: NSObject, WKURLSchemeHandler {
+public final class URLSchemeHandler: NSObject, WKURLSchemeHandler {
public static let scheme = "mail-infomaniak"
public static let domain = "://mail.infomaniak.com"
private var dataTasksInProgress = [Int: URLSessionDataTask]()
private let syncQueue = DispatchQueue(label: "com.infomaniak.mail.URLSchemeHandler")
+ @LazyInjectService private var accountManager: AccountManager
+
public func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
guard let url = urlSchemeTask.request.url else {
urlSchemeTask.didFailWithError(MailError.resourceError)
return
}
-
- guard let currentAccessToken = AccountManager.instance.getCurrentAccount()?.token?.accessToken else {
+
+ guard let currentAccessToken = accountManager.getCurrentAccount()?.token?.accessToken else {
urlSchemeTask.didFailWithError(MailError.unknownError)
return
}
diff --git a/MailCore/Utils/UserAlertDisplayable.swift b/MailCore/Utils/UserAlertDisplayable.swift
new file mode 100644
index 000000000..4db668ba5
--- /dev/null
+++ b/MailCore/Utils/UserAlertDisplayable.swift
@@ -0,0 +1,118 @@
+/*
+ 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 UserAlertDisplayable {
+ /// 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: UserAlertAction)
+}
+
+public typealias UserAlertAction = (name: String, closure: () -> Void)
+
+public final class UserAlertDisplayer: UserAlertDisplayable {
+ @LazyInjectService private var snackbarPresenter: SnackBarPresentable
+ @LazyInjectService private var applicationState: ApplicationStatable
+
+ /// Used by DI
+ public init() {
+ // META: keep sonarcloud happy
+ }
+
+ // MARK: - UserAlertDisplayable
+
+ public func show(message: String) {
+ showInContext(message: message, action: nil)
+ }
+
+ public func show(message: String, action: UserAlertAction) {
+ showInContext(message: message, action: action)
+ }
+
+ // MARK: - private
+
+ private func showInContext(message: String, action: UserAlertAction?) {
+ Task { @MainActor in
+ // 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: UserAlertAction?) {
+ 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: UserAlertAction?) {
+ 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("UserAlertDisplayer 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/MailNotificationServiceExtension/NotificationService.swift b/MailNotificationServiceExtension/NotificationService.swift
index 2bd3ba774..71e30e971 100644
--- a/MailNotificationServiceExtension/NotificationService.swift
+++ b/MailNotificationServiceExtension/NotificationService.swift
@@ -25,34 +25,18 @@ import MailResources
import RealmSwift
import UserNotifications
-class NotificationService: UNNotificationServiceExtension {
+final class NotificationService: UNNotificationServiceExtension {
+ /// Making sure the DI is registered at a very early stage of the app launch.
+ private let dependencyInjectionHook = EarlyDIHook()
+
+ @LazyInjectService private var accountManager: AccountManager
+
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override init() {
super.init()
Logging.initLogging()
- let networkLoginService = Factory(type: InfomaniakNetworkLoginable.self) { _, _ in
- InfomaniakNetworkLogin(clientId: MailApiFetcher.clientId)
- }
- let loginService = Factory(type: InfomaniakLoginable.self) { _, _ in
- InfomaniakLogin(clientId: MailApiFetcher.clientId)
- }
- let keychainHelper = Factory(type: KeychainHelper.self) { _, _ in
- KeychainHelper(accessGroup: AccountManager.accessGroup)
- }
- let tokenStore = Factory(type: TokenStore.self) { _, _ in
- TokenStore()
- }
- let notificationService = Factory(type: InfomaniakNotifications.self) { _, _ in
- InfomaniakNotifications(appGroup: AccountManager.appGroup)
- }
-
- SimpleResolver.sharedResolver.store(factory: networkLoginService)
- SimpleResolver.sharedResolver.store(factory: loginService)
- SimpleResolver.sharedResolver.store(factory: keychainHelper)
- SimpleResolver.sharedResolver.store(factory: tokenStore)
- SimpleResolver.sharedResolver.store(factory: notificationService)
}
func fetchMessage(uid: String, in mailboxManager: MailboxManager) async throws -> Message? {
@@ -95,7 +79,7 @@ class NotificationService: UNNotificationServiceExtension {
guard let mailboxId = userInfos[NotificationsHelper.UserInfoKeys.mailboxId] as? Int,
let userId = userInfos[NotificationsHelper.UserInfoKeys.userId] as? Int,
let mailbox = MailboxInfosManager.instance.getMailbox(id: mailboxId, userId: userId),
- let mailboxManager = AccountManager.instance.getMailboxManager(for: mailbox) else {
+ let mailboxManager = accountManager.getMailboxManager(for: mailbox) else {
// This should never happen, we received a notification for an unknown mailbox
logNotificationFailed(userInfo: userInfos, type: .mailboxNotFound)
return contentHandler(bestAttemptContent)
diff --git a/MailNotificationServiceExtension/NotificationServiceAssembly.swift b/MailNotificationServiceExtension/NotificationServiceAssembly.swift
new file mode 100644
index 000000000..13ca3688e
--- /dev/null
+++ b/MailNotificationServiceExtension/NotificationServiceAssembly.swift
@@ -0,0 +1,101 @@
+/*
+ Infomaniak Mail - iOS App
+ Copyright (C) 2022 Infomaniak Network SA
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+import Foundation
+import InfomaniakCore
+import InfomaniakDI
+import InfomaniakLogin
+import InfomaniakNotifications
+import MailCore
+
+private let realmRootPath = "mailboxes"
+private let appGroupIdentifier = "group.com.infomaniak.mail"
+
+extension Array where Element == Factory {
+ func registerFactoriesInDI() {
+ forEach { SimpleResolver.sharedResolver.store(factory: $0) }
+ }
+}
+
+/// Something that prepares the extension Dependency Injection
+enum NotificationServiceAssembly {
+ static func setupDI() {
+ // Setup main servicies
+ setupMainServices()
+ }
+
+ private static func setupMainServices() {
+ let factories = [
+ Factory(type: InfomaniakNetworkLoginable.self) { _, _ in
+ InfomaniakNetworkLogin(clientId: MailApiFetcher.clientId)
+ },
+ Factory(type: InfomaniakLoginable.self) { _, _ in
+ InfomaniakLogin(clientId: MailApiFetcher.clientId)
+ },
+ Factory(type: KeychainHelper.self) { _, _ in
+ KeychainHelper(accessGroup: AccountManager.accessGroup)
+ },
+ Factory(type: InfomaniakNotifications.self) { _, _ in
+ InfomaniakNotifications(appGroup: AccountManager.appGroup)
+ },
+ Factory(type: DraftManager.self) { _, _ in
+ DraftManager()
+ },
+ Factory(type: AccountManager.self) { _, _ in
+ AccountManager()
+ },
+ Factory(type: SnackBarPresentable.self) { _, _ in
+ SnackBarPresenter()
+ },
+ Factory(type: UserAlertDisplayable.self) { _, _ in
+ UserAlertDisplayer()
+ },
+ Factory(type: UserActivityController.self) { _, _ in
+ UserActivityController()
+ },
+ Factory(type: PlatformDetectable.self) { _, _ in
+ PlatformDetector()
+ },
+ Factory(type: AppGroupPathProvidable.self) { _, _ in
+ guard let provider = AppGroupPathProvider(
+ realmRootPath: realmRootPath,
+ appGroupIdentifier: appGroupIdentifier
+ ) else {
+ fatalError("could not safely init AppGroupPathProvider")
+ }
+
+ return provider
+ },
+ Factory(type: TokenStore.self) { _, _ in
+ TokenStore()
+ },
+ ]
+
+ factories.registerFactoriesInDI()
+ }
+}
+
+/// Something that loads the DI on init
+public struct EarlyDIHook {
+ public init() {
+ ApiFetcher.decoder.dateDecodingStrategy = .iso8601
+
+ // setup DI ASAP
+ NotificationServiceAssembly.setupDI()
+ }
+}
diff --git a/MailResources/Localizable/de.lproj/Localizable.strings b/MailResources/Localizable/de.lproj/Localizable.strings
index 0f7ff86b2..553c68b81 100644
--- a/MailResources/Localizable/de.lproj/Localizable.strings
+++ b/MailResources/Localizable/de.lproj/Localizable.strings
@@ -3,9 +3,8 @@
* Project: kMail
* Release: Working copy
* Locale: de, German
- * Tagged: ios
- * Exported by: Ambroise Decouttere
- * Exported at: Thu, 27 Jul 2023 16:53:47 +0200
+ * Exported by: Adrien Coye
+ * Exported at: Wed, 02 Aug 2023 10:42:34 +0200
*/
/* loco:6256793050618f7416758a32 */
@@ -584,6 +583,12 @@
/* loco:62bc67601f560d734f2fbe58 */
"snackbarContactSaved" = "Neuer Kontakt erfolgreich registriert";
+/* loco:62bd4fa9c71d395cd431e6a2 */
+"NSFaceIDUsageDescription" = "Face ID wird zum Entsperren der Anwendung verwendet.";
+
+/* loco:62bd538485526660827f95d2 */
+"NSContactsUsageDescription" = "Lokale Kontakte werden verwendet, um Empfängerinformationen anzuzeigen. Wir werden sie NICHT an den Server senden.";
+
/* loco:62bd85a64880c362bd5ece72 */
"snackbarEmailCopiedToClipboard" = "E-Mail Adresse in die Zwischenablage kopiert";
@@ -710,6 +715,9 @@
/* loco:62cd644f5141ee164b494272 */
"snackbarDisplayProblemReported" = "Anzeigeproblem gemeldet";
+/* loco:62ce828ff799954b2054aac2 */
+"NSCameraUsageDescription" = "Die Kamera wird zum Anhängen von Fotos an Ihre E-Mail verwendet";
+
/* loco:62cecdf0413c882ede7451c2 */
"reportPhishingTitle" = "Einen Phishing-Versuch melden";
@@ -719,6 +727,9 @@
/* loco:62cfb94ffe73905627441c62 */
"settingsOptionSystemTheme" = "System";
+/* loco:62cfdf508559983b710b7bd2 */
+"threadListHeaderLastUpdateNow" = "jetzt";
+
/* loco:62d5561a4fa44b4a25143fc2 */
"searchFilterRead" = "Lesen";
@@ -788,6 +799,12 @@
/* loco:634ea0a369dac5469e1ce6d2 */
"noConversationSelected" = "Kein Gespräch in %@ ausgewählt";
+/* loco:6357867da9aca53d914670a2 */
+"settingsSwipeLeft" = "Links wischen";
+
+/* loco:63578695fa87a96c446068f2 */
+"settingsSwipeRight" = "Rechts wischen";
+
/* loco:635fab674606bd5f944f7784 */
"workInProgressTitle" = "Bald verfügbar";
@@ -803,6 +820,15 @@
/* loco:637ce6f461e6ca5762134f53 */
"noFolderTitle" = "Sie haben noch keinen Ordner…";
+/* loco:6384d9c01d10203fe203b7e2 */
+"notificationSyncDraftChannelName" = "Speichern von Entwürfen";
+
+/* loco:6384dc46d8b6482c3f1962a4 */
+"notificationSyncMessagesChannelName" = "Abrufen neuer E-Mails";
+
+/* loco:6384e1c927eaf70c585c8732 */
+"notificationNewMessagesChannelName" = "Neue Emails";
+
/* loco:638f5a17e9a49034ae006a92 */
"snackbarThreadDeletedPermanently" = "Konversation gelöscht";
@@ -839,6 +865,15 @@
/* loco:63a474bd648cd86d56444012 */
"confirmLogoutDescription" = "Sind Sie sicher, dass Sie sich von %@ abmelden möchten?";
+/* loco:63bc30711f2f796a1c262c92 */
+"newMessageNotificationSummary" = "%d neue Nachricht";
+
+/* loco:63bc31091f2f796a1c262c95 */
+"newMessageNotificationSummary-plural" = "%d neue Nachrichten";
+
+/* loco:63bc31577985706f5870b192 */
+"newMessageNotificationSummary-plural-many" = "";
+
/* loco:63c64f5d53fe7b61a46310c3 */
"newFolderDialogHint" = "Name des Ordners";
@@ -851,9 +886,15 @@
/* loco:63d14902e3c4eb4832777c42 */
"errorCancelAttachmentsUploadInProgress" = "Einige Anhänge werden noch importiert. Wenn Sie die Ansicht schließen, gehen die Anhänge verloren.";
+/* loco:63d27324e186fd10f243ce82 */
+"webViewCantHandleAction" = "Es wurde keine Anwendung gefunden, die diese Aktion bearbeitet";
+
/* loco:63d7e73a70ff84756f78f0b3 */
"threadListHeaderUnreadCountMore" = "99 ungelesen";
+/* loco:63e0c11f3fd6c70fb7723192 */
+"contentDescriptionIconFolderSelected" = "Ordner ausgewählt";
+
/* loco:63e11e5771d13a480771a162 */
"SettingsEnableNotifications" = "Benachrichtigungen einschalten";
@@ -896,6 +937,9 @@
/* loco:63edf28c20221c0f3a6d30a2 */
"threadListDeletionConfirmationAlertTitle-plural" = "%d Nachrichten löschen";
+/* loco:63edf7b80fc3f74f890dd0c4 */
+"threadListDeletionConfirmationAlertTitle-plural-many" = "";
+
/* loco:63ee0cbbad127b43f8650242 */
"newFolderDialogMovePositiveButton" = "Erstellen und verschieben";
@@ -926,6 +970,9 @@
/* loco:6400bb16c117ec0d866887b5 */
"messageHideQuotedText" = "Das Gespräch ausblenden";
+/* loco:6401f75b518ceb46ca297432 */
+"googlePlayServicesAreRequired" = "Google Play Services sind erforderlich";
+
/* loco:640836aa0f09a82bfa0da572 */
"searchAllMessages" = "Alle Meldungen";
@@ -962,15 +1009,33 @@
/* loco:641d60b6f5c19a18734d9192 */
"urlUserReportiOS" = "https://feedback.userreport.com/efffca51-9fab-4c6c-95d8-148d5a0fc8a9/";
+/* loco:641d6129a45dca299a5fdf02 */
+"urlUserReportAndroid" = "https://feedback.userreport.com/8f405fa0-e5b0-49e7-85d8-477b64ea48f7/";
+
/* loco:641db88f88b1f63cb05aad52 */
"tooManyRecipients" = "Sie können diese Adresse nicht hinzufügen, da Sie die Höchstzahl an Empfängern erreicht haben";
/* loco:642179d9733cd4523d03dcf2 */
"accentColorSystemTitle" = "Farbe des Systems";
+/* loco:6422a877e133d2121822bf72 */
+"contentDescriptionButtonDelete" = "%@ löschen";
+
+/* loco:6422d0ea459bcf36c5099782 */
+"contentDescriptionDownloadIndicator" = "Herunterladen von";
+
+/* loco:6422da8be273963aac69e902 */
+"contentDescriptionAccountSelection" = "Ausgewähltes Konto";
+
/* loco:6422e08598a76c7f98590952 */
"contentDescriptionButtonExpandRecipients" = "Siehe Details";
+/* loco:6422e0f363700359984130d3 */
+"contentDescriptionOpenMailActions" = "Mail-Aktionen öffnen";
+
+/* loco:64241f061119dc0dd86b0272 */
+"contentDescriptionButtonOpenSystemSettings" = "Öffnen Sie die Benachrichtigungseinstellungen des Systems";
+
/* loco:642430a9ce12f43b026e7ef2 */
"contentDescriptionSelectedItem" = "Ausgewählte";
@@ -980,6 +1045,9 @@
/* loco:64243b4a9a9a1f26302e33f3 */
"emailWithoutSubjectDescription" = "Sie sind dabei, eine Nachricht ohne Betreff zu senden. Möchten Sie fortfahren?";
+/* loco:6424488fdb64c92719278303 */
+"contentDescriptionButtonDeleteSignature" = "Unterschrift löschen";
+
/* loco:64245322f2b3ab37ed1b26b2 */
"buttonContinue" = "Weiter";
@@ -1055,6 +1123,12 @@
/* loco:6450d3f93e5fcf13c9037b02 */
"errorMailboxLocked" = "Diese Mailbox ist gesperrt";
+/* loco:64522a87e05780ced808fd62 */
+"errorCorruptAttachment" = "Der Entwurf konnte nicht bearbeitet werden, ein Anhang ist beschädigt";
+
+/* loco:64539f465ac8a1dc1b0a87c2 */
+"contentDescriptionButtonDeleteQuote" = "Zitierte Nachrichten löschen";
+
/* loco:645b485e158c37a429012092 */
"buttonLoadMore" = "Weitere Gespräche laden";
@@ -1222,3 +1296,9 @@
/* loco:64c21f7b8b1932323c047792 */
"newAccountStorageDrive" = "15 GB kDrive-Speicher";
+
+/* loco:64c389458fb94726cf010c52 */
+"snackbarDownloadInProgress" = "Download läuft…";
+
+/* loco:64ca16484dbf65eb9b0ea862 */
+"PleaseLogInFirst" = "Bitte melden Sie sich zuerst in der ikMail-App an.";
diff --git a/MailResources/Localizable/en.lproj/Localizable.strings b/MailResources/Localizable/en.lproj/Localizable.strings
index b5a076b4f..05bcf84b4 100644
--- a/MailResources/Localizable/en.lproj/Localizable.strings
+++ b/MailResources/Localizable/en.lproj/Localizable.strings
@@ -3,9 +3,8 @@
* Project: kMail
* Release: Working copy
* Locale: en, English
- * Tagged: ios
- * Exported by: Ambroise Decouttere
- * Exported at: Thu, 27 Jul 2023 16:53:47 +0200
+ * Exported by: Adrien Coye
+ * Exported at: Wed, 02 Aug 2023 10:42:34 +0200
*/
/* loco:6256793050618f7416758a32 */
@@ -584,6 +583,12 @@
/* loco:62bc67601f560d734f2fbe58 */
"snackbarContactSaved" = "New contact successfully registered";
+/* loco:62bd4fa9c71d395cd431e6a2 */
+"NSFaceIDUsageDescription" = "Face ID will be used to unlock the application.";
+
+/* loco:62bd538485526660827f95d2 */
+"NSContactsUsageDescription" = "Local contacts will be used to show recipient info. We will NOT send them to the server.";
+
/* loco:62bd85a64880c362bd5ece72 */
"snackbarEmailCopiedToClipboard" = "Email address copied to the clipboard";
@@ -710,6 +715,9 @@
/* loco:62cd644f5141ee164b494272 */
"snackbarDisplayProblemReported" = "Display problem reported";
+/* loco:62ce828ff799954b2054aac2 */
+"NSCameraUsageDescription" = "Camera will be used to attach photo to your email";
+
/* loco:62cecdf0413c882ede7451c2 */
"reportPhishingTitle" = "Report a phishing attempt";
@@ -719,6 +727,9 @@
/* loco:62cfb94ffe73905627441c62 */
"settingsOptionSystemTheme" = "System";
+/* loco:62cfdf508559983b710b7bd2 */
+"threadListHeaderLastUpdateNow" = "now";
+
/* loco:62d5561a4fa44b4a25143fc2 */
"searchFilterRead" = "Read";
@@ -788,6 +799,12 @@
/* loco:634ea0a369dac5469e1ce6d2 */
"noConversationSelected" = "No conversation selected in %@";
+/* loco:6357867da9aca53d914670a2 */
+"settingsSwipeLeft" = "Left swipe";
+
+/* loco:63578695fa87a96c446068f2 */
+"settingsSwipeRight" = "Right swipe";
+
/* loco:635fab674606bd5f944f7784 */
"workInProgressTitle" = "Available soon";
@@ -803,6 +820,15 @@
/* loco:637ce6f461e6ca5762134f53 */
"noFolderTitle" = "You don’t have any folder yet…";
+/* loco:6384d9c01d10203fe203b7e2 */
+"notificationSyncDraftChannelName" = "Saving drafts";
+
+/* loco:6384dc46d8b6482c3f1962a4 */
+"notificationSyncMessagesChannelName" = "Retrieving new emails";
+
+/* loco:6384e1c927eaf70c585c8732 */
+"notificationNewMessagesChannelName" = "New emails";
+
/* loco:638f5a17e9a49034ae006a92 */
"snackbarThreadDeletedPermanently" = "Conversation deleted";
@@ -839,6 +865,15 @@
/* loco:63a474bd648cd86d56444012 */
"confirmLogoutDescription" = "Are you sure you want to log out from %@?";
+/* loco:63bc30711f2f796a1c262c92 */
+"newMessageNotificationSummary" = "%d new message";
+
+/* loco:63bc31091f2f796a1c262c95 */
+"newMessageNotificationSummary-plural" = "%d new messages";
+
+/* loco:63bc31577985706f5870b192 */
+"newMessageNotificationSummary-plural-many" = "";
+
/* loco:63c64f5d53fe7b61a46310c3 */
"newFolderDialogHint" = "Folder name";
@@ -851,9 +886,15 @@
/* loco:63d14902e3c4eb4832777c42 */
"errorCancelAttachmentsUploadInProgress" = "Some attachments are still being imported. If you close the view attachments will be lost.";
+/* loco:63d27324e186fd10f243ce82 */
+"webViewCantHandleAction" = "No application has been found to handle this action";
+
/* loco:63d7e73a70ff84756f78f0b3 */
"threadListHeaderUnreadCountMore" = "99+ unread";
+/* loco:63e0c11f3fd6c70fb7723192 */
+"contentDescriptionIconFolderSelected" = "Folder selected";
+
/* loco:63e11e5771d13a480771a162 */
"SettingsEnableNotifications" = "Enable notifications";
@@ -896,6 +937,9 @@
/* loco:63edf28c20221c0f3a6d30a2 */
"threadListDeletionConfirmationAlertTitle-plural" = "Delete %d messages";
+/* loco:63edf7b80fc3f74f890dd0c4 */
+"threadListDeletionConfirmationAlertTitle-plural-many" = "";
+
/* loco:63ee0cbbad127b43f8650242 */
"newFolderDialogMovePositiveButton" = "Create and move";
@@ -926,6 +970,9 @@
/* loco:6400bb16c117ec0d866887b5 */
"messageHideQuotedText" = "Hide the conversation";
+/* loco:6401f75b518ceb46ca297432 */
+"googlePlayServicesAreRequired" = "Google Play Services are required";
+
/* loco:640836aa0f09a82bfa0da572 */
"searchAllMessages" = "All messages";
@@ -962,15 +1009,33 @@
/* loco:641d60b6f5c19a18734d9192 */
"urlUserReportiOS" = "https://feedback.userreport.com/5f64b035-33e5-4e71-9572-dc9d1d451c18/";
+/* loco:641d6129a45dca299a5fdf02 */
+"urlUserReportAndroid" = "https://feedback.userreport.com/dbf02b8c-36f7-4388-839c-a2b6a3029704/";
+
/* loco:641db88f88b1f63cb05aad52 */
"tooManyRecipients" = "You can’t add this address because you have reached the limit of recipients";
/* loco:642179d9733cd4523d03dcf2 */
"accentColorSystemTitle" = "System color";
+/* loco:6422a877e133d2121822bf72 */
+"contentDescriptionButtonDelete" = "Delete %@";
+
+/* loco:6422d0ea459bcf36c5099782 */
+"contentDescriptionDownloadIndicator" = "Downloading";
+
+/* loco:6422da8be273963aac69e902 */
+"contentDescriptionAccountSelection" = "Selected account";
+
/* loco:6422e08598a76c7f98590952 */
"contentDescriptionButtonExpandRecipients" = "See details";
+/* loco:6422e0f363700359984130d3 */
+"contentDescriptionOpenMailActions" = "Open mail actions";
+
+/* loco:64241f061119dc0dd86b0272 */
+"contentDescriptionButtonOpenSystemSettings" = "Open system’s notification settings";
+
/* loco:642430a9ce12f43b026e7ef2 */
"contentDescriptionSelectedItem" = "Selected";
@@ -980,6 +1045,9 @@
/* loco:64243b4a9a9a1f26302e33f3 */
"emailWithoutSubjectDescription" = "You are about to send a message without a subject. Do you want to continue?";
+/* loco:6424488fdb64c92719278303 */
+"contentDescriptionButtonDeleteSignature" = "Delete signature";
+
/* loco:64245322f2b3ab37ed1b26b2 */
"buttonContinue" = "Continue";
@@ -1055,6 +1123,12 @@
/* loco:6450d3f93e5fcf13c9037b02 */
"errorMailboxLocked" = "This mailbox is locked";
+/* loco:64522a87e05780ced808fd62 */
+"errorCorruptAttachment" = "Failed to handle draft, an attachment is corrupted";
+
+/* loco:64539f465ac8a1dc1b0a87c2 */
+"contentDescriptionButtonDeleteQuote" = "Delete quoted messages";
+
/* loco:645b485e158c37a429012092 */
"buttonLoadMore" = "Load more conversations";
@@ -1222,3 +1296,9 @@
/* loco:64c21f7b8b1932323c047792 */
"newAccountStorageDrive" = "15 GB of kDrive storage";
+
+/* loco:64c389458fb94726cf010c52 */
+"snackbarDownloadInProgress" = "Download in progress…";
+
+/* loco:64ca16484dbf65eb9b0ea862 */
+"PleaseLogInFirst" = "Please login in the ikMail app first.";
diff --git a/MailResources/Localizable/es.lproj/Localizable.strings b/MailResources/Localizable/es.lproj/Localizable.strings
index 2dca542bc..da05a42b5 100644
--- a/MailResources/Localizable/es.lproj/Localizable.strings
+++ b/MailResources/Localizable/es.lproj/Localizable.strings
@@ -3,9 +3,8 @@
* Project: kMail
* Release: Working copy
* Locale: es, Spanish
- * Tagged: ios
- * Exported by: Ambroise Decouttere
- * Exported at: Thu, 27 Jul 2023 16:53:47 +0200
+ * Exported by: Adrien Coye
+ * Exported at: Wed, 02 Aug 2023 10:42:34 +0200
*/
/* loco:6256793050618f7416758a32 */
@@ -584,6 +583,12 @@
/* loco:62bc67601f560d734f2fbe58 */
"snackbarContactSaved" = "Nuevo contacto registrado correctamente";
+/* loco:62bd4fa9c71d395cd431e6a2 */
+"NSFaceIDUsageDescription" = "Face ID se utilizará para desbloquear la aplicación.";
+
+/* loco:62bd538485526660827f95d2 */
+"NSContactsUsageDescription" = "Los contactos locales se utilizarán para mostrar la información del destinatario. NO los enviaremos al servidor.";
+
/* loco:62bd85a64880c362bd5ece72 */
"snackbarEmailCopiedToClipboard" = "Dirección de correo electrónico copiada en el portapapeles";
@@ -710,6 +715,9 @@
/* loco:62cd644f5141ee164b494272 */
"snackbarDisplayProblemReported" = "Problema de visualización notificado";
+/* loco:62ce828ff799954b2054aac2 */
+"NSCameraUsageDescription" = "La cámara se utilizará para adjuntar la foto a su correo electrónico";
+
/* loco:62cecdf0413c882ede7451c2 */
"reportPhishingTitle" = "Informar de un intento de phishing";
@@ -719,6 +727,9 @@
/* loco:62cfb94ffe73905627441c62 */
"settingsOptionSystemTheme" = "Sistema";
+/* loco:62cfdf508559983b710b7bd2 */
+"threadListHeaderLastUpdateNow" = "ahora";
+
/* loco:62d5561a4fa44b4a25143fc2 */
"searchFilterRead" = "Leer";
@@ -788,6 +799,12 @@
/* loco:634ea0a369dac5469e1ce6d2 */
"noConversationSelected" = "Ninguna conversación seleccionada en %@";
+/* loco:6357867da9aca53d914670a2 */
+"settingsSwipeLeft" = "Deslizar a la izquierda";
+
+/* loco:63578695fa87a96c446068f2 */
+"settingsSwipeRight" = "Deslizar a la derecha";
+
/* loco:635fab674606bd5f944f7784 */
"workInProgressTitle" = "Disponible en breve";
@@ -803,6 +820,15 @@
/* loco:637ce6f461e6ca5762134f53 */
"noFolderTitle" = "Aún no tienes ninguna carpeta…";
+/* loco:6384d9c01d10203fe203b7e2 */
+"notificationSyncDraftChannelName" = "Guardar borradores";
+
+/* loco:6384dc46d8b6482c3f1962a4 */
+"notificationSyncMessagesChannelName" = "Recuperación de nuevos correos electrónicos";
+
+/* loco:6384e1c927eaf70c585c8732 */
+"notificationNewMessagesChannelName" = "Nuevos correos electrónicos";
+
/* loco:638f5a17e9a49034ae006a92 */
"snackbarThreadDeletedPermanently" = "Conversación eliminada";
@@ -839,6 +865,15 @@
/* loco:63a474bd648cd86d56444012 */
"confirmLogoutDescription" = "¿Seguro que quieres desconectarte de %@?";
+/* loco:63bc30711f2f796a1c262c92 */
+"newMessageNotificationSummary" = "%d nuevo mensaje";
+
+/* loco:63bc31091f2f796a1c262c95 */
+"newMessageNotificationSummary-plural" = "%d nuevos mensajes";
+
+/* loco:63bc31577985706f5870b192 */
+"newMessageNotificationSummary-plural-many" = "";
+
/* loco:63c64f5d53fe7b61a46310c3 */
"newFolderDialogHint" = "Nombre de la carpeta";
@@ -851,9 +886,15 @@
/* loco:63d14902e3c4eb4832777c42 */
"errorCancelAttachmentsUploadInProgress" = "Algunos archivos adjuntos se siguen importando. Si cierra la vista se perderán los archivos adjuntos.";
+/* loco:63d27324e186fd10f243ce82 */
+"webViewCantHandleAction" = "No se ha encontrado ninguna aplicación que gestione esta acción";
+
/* loco:63d7e73a70ff84756f78f0b3 */
"threadListHeaderUnreadCountMore" = "99 no leídos";
+/* loco:63e0c11f3fd6c70fb7723192 */
+"contentDescriptionIconFolderSelected" = "Carpeta seleccionada";
+
/* loco:63e11e5771d13a480771a162 */
"SettingsEnableNotifications" = "Activar las notificaciones";
@@ -896,6 +937,9 @@
/* loco:63edf28c20221c0f3a6d30a2 */
"threadListDeletionConfirmationAlertTitle-plural" = "Borrar %d mensajes";
+/* loco:63edf7b80fc3f74f890dd0c4 */
+"threadListDeletionConfirmationAlertTitle-plural-many" = "";
+
/* loco:63ee0cbbad127b43f8650242 */
"newFolderDialogMovePositiveButton" = "Crear y mover";
@@ -926,6 +970,9 @@
/* loco:6400bb16c117ec0d866887b5 */
"messageHideQuotedText" = "Ocultar la conversación";
+/* loco:6401f75b518ceb46ca297432 */
+"googlePlayServicesAreRequired" = "Se requieren los servicios de Google Play";
+
/* loco:640836aa0f09a82bfa0da572 */
"searchAllMessages" = "Todos los mensajes";
@@ -962,15 +1009,33 @@
/* loco:641d60b6f5c19a18734d9192 */
"urlUserReportiOS" = "https://feedback.userreport.com/bc05436b-f496-4b6e-934a-338b4b0f1cda/";
+/* loco:641d6129a45dca299a5fdf02 */
+"urlUserReportAndroid" = "https://feedback.userreport.com/f0daec0a-6950-4891-a064-7d040bdce127/";
+
/* loco:641db88f88b1f63cb05aad52 */
"tooManyRecipients" = "No puede añadir esta dirección porque ha alcanzado el límite de destinatarios";
/* loco:642179d9733cd4523d03dcf2 */
"accentColorSystemTitle" = "Color del sistema";
+/* loco:6422a877e133d2121822bf72 */
+"contentDescriptionButtonDelete" = "Borrar %@";
+
+/* loco:6422d0ea459bcf36c5099782 */
+"contentDescriptionDownloadIndicator" = "Descargar";
+
+/* loco:6422da8be273963aac69e902 */
+"contentDescriptionAccountSelection" = "Cuenta seleccionada";
+
/* loco:6422e08598a76c7f98590952 */
"contentDescriptionButtonExpandRecipients" = "Ver detalles";
+/* loco:6422e0f363700359984130d3 */
+"contentDescriptionOpenMailActions" = "Acciones de mensajes abiertos";
+
+/* loco:64241f061119dc0dd86b0272 */
+"contentDescriptionButtonOpenSystemSettings" = "Abrir la configuración de notificaciones del sistema";
+
/* loco:642430a9ce12f43b026e7ef2 */
"contentDescriptionSelectedItem" = "Selección";
@@ -980,6 +1045,9 @@
/* loco:64243b4a9a9a1f26302e33f3 */
"emailWithoutSubjectDescription" = "Está a punto de enviar un mensaje sin asunto. ¿Desea continuar?";
+/* loco:6424488fdb64c92719278303 */
+"contentDescriptionButtonDeleteSignature" = "Borrar firma";
+
/* loco:64245322f2b3ab37ed1b26b2 */
"buttonContinue" = "Continuar";
@@ -1055,6 +1123,12 @@
/* loco:6450d3f93e5fcf13c9037b02 */
"errorMailboxLocked" = "Esta dirección de correo electrónico está bloqueada";
+/* loco:64522a87e05780ced808fd62 */
+"errorCorruptAttachment" = "No se ha podido gestionar el borrador, un archivo adjunto está dañado";
+
+/* loco:64539f465ac8a1dc1b0a87c2 */
+"contentDescriptionButtonDeleteQuote" = "Borrar mensajes citados";
+
/* loco:645b485e158c37a429012092 */
"buttonLoadMore" = "Cargar más conversaciones";
@@ -1222,3 +1296,9 @@
/* loco:64c21f7b8b1932323c047792 */
"newAccountStorageDrive" = "15 GB de almacenamiento kDrive";
+
+/* loco:64c389458fb94726cf010c52 */
+"snackbarDownloadInProgress" = "Descarga en proceso…";
+
+/* loco:64ca16484dbf65eb9b0ea862 */
+"PleaseLogInFirst" = "Inicia sesión primero en la aplicación ikMail.";
diff --git a/MailResources/Localizable/fr.lproj/Localizable.strings b/MailResources/Localizable/fr.lproj/Localizable.strings
index 0380c56a3..c9616ba65 100644
--- a/MailResources/Localizable/fr.lproj/Localizable.strings
+++ b/MailResources/Localizable/fr.lproj/Localizable.strings
@@ -3,9 +3,8 @@
* Project: kMail
* Release: Working copy
* Locale: fr, French
- * Tagged: ios
- * Exported by: Ambroise Decouttere
- * Exported at: Thu, 27 Jul 2023 16:53:47 +0200
+ * Exported by: Adrien Coye
+ * Exported at: Wed, 02 Aug 2023 10:42:34 +0200
*/
/* loco:6256793050618f7416758a32 */
@@ -584,6 +583,12 @@
/* loco:62bc67601f560d734f2fbe58 */
"snackbarContactSaved" = "Nouveau contact enregistré avec succès";
+/* loco:62bd4fa9c71d395cd431e6a2 */
+"NSFaceIDUsageDescription" = "Face ID sera utilisé pour déverrouiller l’application.";
+
+/* loco:62bd538485526660827f95d2 */
+"NSContactsUsageDescription" = "Les contacts locaux seront utilisés pour afficher les informations sur les destinataires. Nous ne les enverrons PAS au serveur.";
+
/* loco:62bd85a64880c362bd5ece72 */
"snackbarEmailCopiedToClipboard" = "Adresse mail copiée dans le presse-papiers";
@@ -710,6 +715,9 @@
/* loco:62cd644f5141ee164b494272 */
"snackbarDisplayProblemReported" = "Problème d’affichage signalé";
+/* loco:62ce828ff799954b2054aac2 */
+"NSCameraUsageDescription" = "L'appareil photo sera utilisé pour joindre une photo à votre e-mail.";
+
/* loco:62cecdf0413c882ede7451c2 */
"reportPhishingTitle" = "Signaler une tentative d’hameçonnage";
@@ -719,6 +727,9 @@
/* loco:62cfb94ffe73905627441c62 */
"settingsOptionSystemTheme" = "Système";
+/* loco:62cfdf508559983b710b7bd2 */
+"threadListHeaderLastUpdateNow" = "à l’instant";
+
/* loco:62d5561a4fa44b4a25143fc2 */
"searchFilterRead" = "Lus";
@@ -788,6 +799,12 @@
/* loco:634ea0a369dac5469e1ce6d2 */
"noConversationSelected" = "Aucune conversation sélectionnée dans %@";
+/* loco:6357867da9aca53d914670a2 */
+"settingsSwipeLeft" = "Balayage vers la gauche";
+
+/* loco:63578695fa87a96c446068f2 */
+"settingsSwipeRight" = "Balayage vers la droite";
+
/* loco:635fab674606bd5f944f7784 */
"workInProgressTitle" = "Disponible prochainement";
@@ -803,6 +820,15 @@
/* loco:637ce6f461e6ca5762134f53 */
"noFolderTitle" = "Vous n’avez pas encore de dossier…";
+/* loco:6384d9c01d10203fe203b7e2 */
+"notificationSyncDraftChannelName" = "Sauvegarde des brouillons";
+
+/* loco:6384dc46d8b6482c3f1962a4 */
+"notificationSyncMessagesChannelName" = "Récupération des nouveaux emails";
+
+/* loco:6384e1c927eaf70c585c8732 */
+"notificationNewMessagesChannelName" = "Nouveaux e-mails";
+
/* loco:638f5a17e9a49034ae006a92 */
"snackbarThreadDeletedPermanently" = "Conversation supprimée";
@@ -839,6 +865,15 @@
/* loco:63a474bd648cd86d56444012 */
"confirmLogoutDescription" = "Êtes-vous sûr de vouloir vous déconnecter du compte %@ ?";
+/* loco:63bc30711f2f796a1c262c92 */
+"newMessageNotificationSummary" = "%d nouveau message";
+
+/* loco:63bc31091f2f796a1c262c95 */
+"newMessageNotificationSummary-plural" = "%d nouveaux messages";
+
+/* loco:63bc31577985706f5870b192 */
+"newMessageNotificationSummary-plural-many" = "%d nouveaux messages";
+
/* loco:63c64f5d53fe7b61a46310c3 */
"newFolderDialogHint" = "Nom du dossier";
@@ -851,9 +886,15 @@
/* loco:63d14902e3c4eb4832777c42 */
"errorCancelAttachmentsUploadInProgress" = "Certaines pièces jointes sont toujours en cours d'importation. Si vous fermez la vue, les pièces jointes seront perdues.";
+/* loco:63d27324e186fd10f243ce82 */
+"webViewCantHandleAction" = "Aucune application n’a été trouvée pour gérer cette action";
+
/* loco:63d7e73a70ff84756f78f0b3 */
"threadListHeaderUnreadCountMore" = "99+ non lus";
+/* loco:63e0c11f3fd6c70fb7723192 */
+"contentDescriptionIconFolderSelected" = "Dossier sélectionné";
+
/* loco:63e11e5771d13a480771a162 */
"SettingsEnableNotifications" = "Activer les notifications";
@@ -896,6 +937,9 @@
/* loco:63edf28c20221c0f3a6d30a2 */
"threadListDeletionConfirmationAlertTitle-plural" = "Supprimer %d messages";
+/* loco:63edf7b80fc3f74f890dd0c4 */
+"threadListDeletionConfirmationAlertTitle-plural-many" = "Supprimer %d de messages";
+
/* loco:63ee0cbbad127b43f8650242 */
"newFolderDialogMovePositiveButton" = "Créer et déplacer";
@@ -926,6 +970,9 @@
/* loco:6400bb16c117ec0d866887b5 */
"messageHideQuotedText" = "Cacher la conversation";
+/* loco:6401f75b518ceb46ca297432 */
+"googlePlayServicesAreRequired" = "Les Google Play Services sont requis";
+
/* loco:640836aa0f09a82bfa0da572 */
"searchAllMessages" = "Tous les messages";
@@ -962,15 +1009,33 @@
/* loco:641d60b6f5c19a18734d9192 */
"urlUserReportiOS" = "https://feedback.userreport.com/d6b0e711-0c77-459c-9d06-79f92264f221/";
+/* loco:641d6129a45dca299a5fdf02 */
+"urlUserReportAndroid" = "https://feedback.userreport.com/91aeb5c2-f609-4b9c-9787-755e83ed940c/";
+
/* loco:641db88f88b1f63cb05aad52 */
"tooManyRecipients" = "Vous ne pouvez pas ajouter cette adresse car vous avez atteint la limite de destinataires";
/* loco:642179d9733cd4523d03dcf2 */
"accentColorSystemTitle" = "Couleur du système";
+/* loco:6422a877e133d2121822bf72 */
+"contentDescriptionButtonDelete" = "Supprimer %@";
+
+/* loco:6422d0ea459bcf36c5099782 */
+"contentDescriptionDownloadIndicator" = "Téléchargement";
+
+/* loco:6422da8be273963aac69e902 */
+"contentDescriptionAccountSelection" = "Compte sélectionné";
+
/* loco:6422e08598a76c7f98590952 */
"contentDescriptionButtonExpandRecipients" = "Voir les détails";
+/* loco:6422e0f363700359984130d3 */
+"contentDescriptionOpenMailActions" = "Ouvrir les actions du message";
+
+/* loco:64241f061119dc0dd86b0272 */
+"contentDescriptionButtonOpenSystemSettings" = "Ouvrir les paramètres de notification du système";
+
/* loco:642430a9ce12f43b026e7ef2 */
"contentDescriptionSelectedItem" = "Sélectionné";
@@ -980,6 +1045,9 @@
/* loco:64243b4a9a9a1f26302e33f3 */
"emailWithoutSubjectDescription" = "Vous êtes sur le point d’envoyer un message sans objet. Voulez-vous continuer ?";
+/* loco:6424488fdb64c92719278303 */
+"contentDescriptionButtonDeleteSignature" = "Supprimer la signature";
+
/* loco:64245322f2b3ab37ed1b26b2 */
"buttonContinue" = "Continuer";
@@ -1055,6 +1123,12 @@
/* loco:6450d3f93e5fcf13c9037b02 */
"errorMailboxLocked" = "Cette adresse est bloquée";
+/* loco:64522a87e05780ced808fd62 */
+"errorCorruptAttachment" = "Échec du traitement du brouillon, une pièce jointe est corrompue";
+
+/* loco:64539f465ac8a1dc1b0a87c2 */
+"contentDescriptionButtonDeleteQuote" = "Supprimer les messages cités";
+
/* loco:645b485e158c37a429012092 */
"buttonLoadMore" = "Charger plus de conversations";
@@ -1222,3 +1296,9 @@
/* loco:64c21f7b8b1932323c047792 */
"newAccountStorageDrive" = "15 Go de stockage kDrive";
+
+/* loco:64c389458fb94726cf010c52 */
+"snackbarDownloadInProgress" = "Téléchargement en cours…";
+
+/* loco:64ca16484dbf65eb9b0ea862 */
+"PleaseLogInFirst" = "Veuillez d'abord vous connecter à l'application ikMail.";
diff --git a/MailResources/Localizable/it.lproj/Localizable.strings b/MailResources/Localizable/it.lproj/Localizable.strings
index aefa3485f..20549c5fc 100644
--- a/MailResources/Localizable/it.lproj/Localizable.strings
+++ b/MailResources/Localizable/it.lproj/Localizable.strings
@@ -3,9 +3,8 @@
* Project: kMail
* Release: Working copy
* Locale: it, Italian
- * Tagged: ios
- * Exported by: Ambroise Decouttere
- * Exported at: Thu, 27 Jul 2023 16:53:47 +0200
+ * Exported by: Adrien Coye
+ * Exported at: Wed, 02 Aug 2023 10:42:34 +0200
*/
/* loco:6256793050618f7416758a32 */
@@ -584,6 +583,12 @@
/* loco:62bc67601f560d734f2fbe58 */
"snackbarContactSaved" = "Nuovo contatto registrato con successo";
+/* loco:62bd4fa9c71d395cd431e6a2 */
+"NSFaceIDUsageDescription" = "Il Face ID verrà utilizzato per sbloccare l'applicazione.";
+
+/* loco:62bd538485526660827f95d2 */
+"NSContactsUsageDescription" = "I contatti locali saranno utilizzati per mostrare le informazioni sui destinatari. NON verranno inviati al server.";
+
/* loco:62bd85a64880c362bd5ece72 */
"snackbarEmailCopiedToClipboard" = "Indirizzo e-mail copiato negli appunti";
@@ -710,6 +715,9 @@
/* loco:62cd644f5141ee164b494272 */
"snackbarDisplayProblemReported" = "Problema di visualizzazione segnalato";
+/* loco:62ce828ff799954b2054aac2 */
+"NSCameraUsageDescription" = "La fotocamera verrà utilizzata per allegare le foto all'e-mail.";
+
/* loco:62cecdf0413c882ede7451c2 */
"reportPhishingTitle" = "Segnala un tentativo di phishing";
@@ -719,6 +727,9 @@
/* loco:62cfb94ffe73905627441c62 */
"settingsOptionSystemTheme" = "Sistema";
+/* loco:62cfdf508559983b710b7bd2 */
+"threadListHeaderLastUpdateNow" = "ora";
+
/* loco:62d5561a4fa44b4a25143fc2 */
"searchFilterRead" = "Leggi";
@@ -788,6 +799,12 @@
/* loco:634ea0a369dac5469e1ce6d2 */
"noConversationSelected" = "Nessuna conversazione selezionata in %@";
+/* loco:6357867da9aca53d914670a2 */
+"settingsSwipeLeft" = "Passaggio del dito a sinistra";
+
+/* loco:63578695fa87a96c446068f2 */
+"settingsSwipeRight" = "Passaggio del dito a destra";
+
/* loco:635fab674606bd5f944f7784 */
"workInProgressTitle" = "Disponibile a breve";
@@ -803,6 +820,15 @@
/* loco:637ce6f461e6ca5762134f53 */
"noFolderTitle" = "Non hai ancora nessuna cartella…";
+/* loco:6384d9c01d10203fe203b7e2 */
+"notificationSyncDraftChannelName" = "Salvataggio delle bozze";
+
+/* loco:6384dc46d8b6482c3f1962a4 */
+"notificationSyncMessagesChannelName" = "Recupero di nuove e-mail";
+
+/* loco:6384e1c927eaf70c585c8732 */
+"notificationNewMessagesChannelName" = "Nuove e-mail";
+
/* loco:638f5a17e9a49034ae006a92 */
"snackbarThreadDeletedPermanently" = "Conversazione cancellata";
@@ -839,6 +865,15 @@
/* loco:63a474bd648cd86d56444012 */
"confirmLogoutDescription" = "Sei sicuro di volerti disconnettere da %@?";
+/* loco:63bc30711f2f796a1c262c92 */
+"newMessageNotificationSummary" = "%d nuovo messaggio";
+
+/* loco:63bc31091f2f796a1c262c95 */
+"newMessageNotificationSummary-plural" = "%d nuovi messaggi";
+
+/* loco:63bc31577985706f5870b192 */
+"newMessageNotificationSummary-plural-many" = "";
+
/* loco:63c64f5d53fe7b61a46310c3 */
"newFolderDialogHint" = "Nome della cartella";
@@ -851,9 +886,15 @@
/* loco:63d14902e3c4eb4832777c42 */
"errorCancelAttachmentsUploadInProgress" = "Alcuni allegati vengono ancora importati. Se si chiude la vista, gli allegati andranno persi.";
+/* loco:63d27324e186fd10f243ce82 */
+"webViewCantHandleAction" = "Non è stata trovata alcuna applicazione in grado di gestire questa azione";
+
/* loco:63d7e73a70ff84756f78f0b3 */
"threadListHeaderUnreadCountMore" = "99 non letti";
+/* loco:63e0c11f3fd6c70fb7723192 */
+"contentDescriptionIconFolderSelected" = "Cartella selezionata";
+
/* loco:63e11e5771d13a480771a162 */
"SettingsEnableNotifications" = "Abilitazione delle notifiche";
@@ -896,6 +937,9 @@
/* loco:63edf28c20221c0f3a6d30a2 */
"threadListDeletionConfirmationAlertTitle-plural" = "Cancella %d messaggi";
+/* loco:63edf7b80fc3f74f890dd0c4 */
+"threadListDeletionConfirmationAlertTitle-plural-many" = "";
+
/* loco:63ee0cbbad127b43f8650242 */
"newFolderDialogMovePositiveButton" = "Crea e sposta";
@@ -926,6 +970,9 @@
/* loco:6400bb16c117ec0d866887b5 */
"messageHideQuotedText" = "Nascondi la conversazione";
+/* loco:6401f75b518ceb46ca297432 */
+"googlePlayServicesAreRequired" = "I servizi Google Play sono necessari";
+
/* loco:640836aa0f09a82bfa0da572 */
"searchAllMessages" = "Tutti i messaggi";
@@ -962,15 +1009,33 @@
/* loco:641d60b6f5c19a18734d9192 */
"urlUserReportiOS" = "https://feedback.userreport.com/b0fada78-1555-4c59-abad-423e887534d5/";
+/* loco:641d6129a45dca299a5fdf02 */
+"urlUserReportAndroid" = "https://feedback.userreport.com/3cc072b7-61ff-4592-b1aa-307a58fdcec9/";
+
/* loco:641db88f88b1f63cb05aad52 */
"tooManyRecipients" = "Non è possibile aggiungere questo indirizzo perché è stato raggiunto il limite di destinatari";
/* loco:642179d9733cd4523d03dcf2 */
"accentColorSystemTitle" = "Colore del sistema";
+/* loco:6422a877e133d2121822bf72 */
+"contentDescriptionButtonDelete" = "Cancella %@";
+
+/* loco:6422d0ea459bcf36c5099782 */
+"contentDescriptionDownloadIndicator" = "Scarica";
+
+/* loco:6422da8be273963aac69e902 */
+"contentDescriptionAccountSelection" = "Conto selezionato";
+
/* loco:6422e08598a76c7f98590952 */
"contentDescriptionButtonExpandRecipients" = "Vedi dettagli";
+/* loco:6422e0f363700359984130d3 */
+"contentDescriptionOpenMailActions" = "Azioni di apertura dei messaggi";
+
+/* loco:64241f061119dc0dd86b0272 */
+"contentDescriptionButtonOpenSystemSettings" = "Apri le impostazioni di notifica del sistema";
+
/* loco:642430a9ce12f43b026e7ef2 */
"contentDescriptionSelectedItem" = "Selezionato";
@@ -980,6 +1045,9 @@
/* loco:64243b4a9a9a1f26302e33f3 */
"emailWithoutSubjectDescription" = "State per inviare un messaggio senza oggetto. Volete continuare?";
+/* loco:6424488fdb64c92719278303 */
+"contentDescriptionButtonDeleteSignature" = "Cancella la firma";
+
/* loco:64245322f2b3ab37ed1b26b2 */
"buttonContinue" = "Continua";
@@ -1055,6 +1123,12 @@
/* loco:6450d3f93e5fcf13c9037b02 */
"errorMailboxLocked" = "Questo indirizzo e-mail è bloccato";
+/* loco:64522a87e05780ced808fd62 */
+"errorCorruptAttachment" = "Impossibile gestire la bozza, un allegato è danneggiato";
+
+/* loco:64539f465ac8a1dc1b0a87c2 */
+"contentDescriptionButtonDeleteQuote" = "Eliminare i messaggi citati";
+
/* loco:645b485e158c37a429012092 */
"buttonLoadMore" = "Carica altre conversazioni";
@@ -1222,3 +1296,9 @@
/* loco:64c21f7b8b1932323c047792 */
"newAccountStorageDrive" = "15 GB di memoria kDrive";
+
+/* loco:64c389458fb94726cf010c52 */
+"snackbarDownloadInProgress" = "Download in corso…";
+
+/* loco:64ca16484dbf65eb9b0ea862 */
+"PleaseLogInFirst" = "Effettuare prima il login nell'app ikMail.";
diff --git a/MailShareExtension/Base.lproj/MainInterface.storyboard b/MailShareExtension/Base.lproj/MainInterface.storyboard
new file mode 100644
index 000000000..34049b372
--- /dev/null
+++ b/MailShareExtension/Base.lproj/MainInterface.storyboard
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MailShareExtension/ComposeMessageWrapperView.swift b/MailShareExtension/ComposeMessageWrapperView.swift
new file mode 100644
index 000000000..581a01ef0
--- /dev/null
+++ b/MailShareExtension/ComposeMessageWrapperView.swift
@@ -0,0 +1,76 @@
+/*
+ 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 InfomaniakCore
+import InfomaniakCoreUI
+import InfomaniakDI
+import MailCore
+import MailResources
+import Social
+import SwiftUI
+import UIKit
+
+struct ComposeMessageWrapperView: View {
+ private var itemProviders: [NSItemProvider]
+ private var dismissHandler: SimpleClosure
+
+ @State private var draft: Draft
+
+ @LazyInjectService private var accountManager: AccountManager
+
+ init(dismissHandler: @escaping SimpleClosure, itemProviders: [NSItemProvider], draft: Draft = Draft()) {
+ _draft = State(wrappedValue: draft)
+ self.dismissHandler = dismissHandler
+ self.itemProviders = itemProviders
+ }
+
+ var body: some View {
+ if let mailboxManager = accountManager.currentMailboxManager {
+ ComposeMessageView.newMessage(draft, mailboxManager: mailboxManager, itemProviders: itemProviders)
+ .environmentObject(mailboxManager)
+ .environment(\.dismissModal) {
+ dismissHandler(())
+ }
+ } else {
+ PleaseLoginView(tapHandler: dismissHandler)
+ }
+ }
+}
+
+struct PleaseLoginView: View {
+ @State var slide = Slide.onBoardingSlides.first!
+
+ var tapHandler: SimpleClosure
+
+ var body: some View {
+ VStack {
+ MailShareExtensionAsset.logoText.swiftUIImage
+ .resizable()
+ .scaledToFit()
+ .frame(height: UIConstants.onboardingLogoHeight)
+ .padding(.top, UIConstants.onboardingLogoPaddingTop)
+ Text(MailResourcesStrings.Localizable.pleaseLogInFirst)
+ .textStyle(.header2)
+ .padding(.top, UIConstants.onboardingLogoPaddingTop)
+ LottieView(configuration: slide.lottieConfiguration!)
+ Spacer()
+ }.onTapGesture {
+ tapHandler(())
+ }
+ }
+}
diff --git a/MailShareExtension/Info.plist b/MailShareExtension/Info.plist
new file mode 100644
index 000000000..7c89d6499
--- /dev/null
+++ b/MailShareExtension/Info.plist
@@ -0,0 +1,38 @@
+
+
+
+
+ CFBundleVersion
+ $(CURRENT_PROJECT_VERSION)
+ CFBundleShortVersionString
+ $(MARKETING_VERSION)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleDisplayName
+ $(PRODUCT_NAME)
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ AppIdentifierPrefix
+ $(AppIdentifierPrefix)
+ NSExtension
+
+ NSExtensionAttributes
+
+ NSExtensionActivationRule
+ SUBQUERY (extensionItems, $extensionItem, SUBQUERY ($extensionItem.attachments, $attachment, (ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.data")).@count == $extensionItem.attachments.@count ).@count > 0
+
+ NSExtensionMainStoryboard
+ MainInterface
+ NSExtensionPointIdentifier
+ com.apple.share-services
+
+
+
diff --git a/MailShareExtension/Proxy/ApplicationState.swift b/MailShareExtension/Proxy/ApplicationState.swift
new file mode 100644
index 000000000..926be5016
--- /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 MailCore
+import UIKit
+
+public struct ApplicationState: ApplicationStatable {
+ public var applicationState: UIApplication.State? {
+ nil
+ }
+}
diff --git a/MailShareExtension/Proxy/CacheManager.swift b/MailShareExtension/Proxy/CacheManager.swift
new file mode 100644
index 000000000..49f24d499
--- /dev/null
+++ b/MailShareExtension/Proxy/CacheManager.swift
@@ -0,0 +1,27 @@
+/*
+ Infomaniak Mail - iOS App
+ Copyright (C) 2022 Infomaniak Network SA
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+import Foundation
+import UIKit
+
+/// A cache manager that works in Extension mode
+public final class CacheManager: CacheManageable {
+ public func refreshCacheData() {
+ // NOOP in shareExtension
+ }
+}
diff --git a/MailShareExtension/Proxy/OrientationManager.swift b/MailShareExtension/Proxy/OrientationManager.swift
new file mode 100644
index 000000000..0c315b132
--- /dev/null
+++ b/MailShareExtension/Proxy/OrientationManager.swift
@@ -0,0 +1,32 @@
+/*
+ Infomaniak Mail - iOS App
+ Copyright (C) 2022 Infomaniak Network SA
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+import Foundation
+import InfomaniakCoreUI
+import UIKit
+
+/// An OrientationManager that works in Extension mode
+public final class OrientationManager: OrientationManageable {
+ public var orientationLock = UIInterfaceOrientationMask.all
+
+ public func setOrientationLock(_ orientation: UIInterfaceOrientationMask) {
+ // NOOP in share extension
+ }
+
+ public var interfaceOrientation: UIInterfaceOrientation?
+}
diff --git a/MailShareExtension/Proxy/RemoteNotificationRegistrer.swift b/MailShareExtension/Proxy/RemoteNotificationRegistrer.swift
new file mode 100644
index 000000000..c3ecc243e
--- /dev/null
+++ b/MailShareExtension/Proxy/RemoteNotificationRegistrer.swift
@@ -0,0 +1,27 @@
+/*
+ Infomaniak Mail - iOS App
+ Copyright (C) 2022 Infomaniak Network SA
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see .
+ */
+
+import Foundation
+import UIKit
+
+/// A RemoteNotificationRegistrer that works in Extension mode
+public final class RemoteNotificationRegistrer: RemoteNotificationRegistrable {
+ public func register() {
+ // NOOP in share extension
+ }
+}
diff --git a/MailShareExtension/ShareExtension.entitlements b/MailShareExtension/ShareExtension.entitlements
new file mode 100644
index 000000000..483c59d33
--- /dev/null
+++ b/MailShareExtension/ShareExtension.entitlements
@@ -0,0 +1,14 @@
+
+
+
+
+ com.apple.security.application-groups
+
+ group.com.infomaniak.mail
+
+ keychain-access-groups
+
+ $(AppIdentifierPrefix)com.infomaniak.mail
+
+
+
diff --git a/MailShareExtension/ShareViewController.swift b/MailShareExtension/ShareViewController.swift
new file mode 100644
index 000000000..27bb48aa2
--- /dev/null
+++ b/MailShareExtension/ShareViewController.swift
@@ -0,0 +1,94 @@
+/*
+ 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 InfomaniakCoreUI
+import InfomaniakDI
+import MailCore
+import Social
+import SwiftUI
+import UIKit
+
+final class ShareNavigationViewController: UIViewController {
+ /// Making sure the DI is registered at a very early stage of the app launch.
+ private let dependencyInjectionHook = EarlyDIHook()
+
+ @LazyInjectService private var accountManager: AccountManager
+
+ private func overrideSnackBarPresenter(contextView: UIView) {
+ let snackBarPresenter = Factory(type: SnackBarPresentable.self) { _, _ in
+ SnackBarPresenter(contextView: contextView)
+ }
+ SimpleResolver.sharedResolver.store(factory: snackBarPresenter)
+ }
+
+ override public func viewDidLoad() {
+ super.viewDidLoad()
+
+ overrideSnackBarPresenter(contextView: view)
+
+ // Set theme
+ overrideUserInterfaceStyle = UserDefaults.shared.theme.interfaceStyle
+ view.tintColor = UserDefaults.shared.accentColor.primary.color
+
+ // Modify sheet size on iPadOS, property is ignored on iOS
+ preferredContentSize = CGSize(width: 540, height: 620)
+
+ // Make sure we are handling [NSExtensionItem]
+ guard let extensionItems: [NSExtensionItem] = extensionContext?.inputItems.compactMap { $0 as? NSExtensionItem },
+ !extensionItems.isEmpty else {
+ dismiss(animated: true)
+ return
+ }
+
+ let itemProviders: [NSItemProvider] = extensionItems.compactMap(\.attachments).flatMap { $0 }
+ guard !itemProviders.isEmpty else {
+ dismiss(animated: true)
+ return
+ }
+
+ /// make sure we load the contact list asap.
+ if let currentContactManager = accountManager.currentContactManager {
+ Task {
+ try await currentContactManager.fetchContactsAndAddressBooks()
+ }
+ }
+
+ // We need to go threw wrapping to use SwiftUI in an NSExtension.
+ let rootView = ComposeMessageWrapperView(dismissHandler: {
+ self.dismiss(animated: true)
+ },
+ itemProviders: itemProviders)
+ .defaultAppStorage(.shared)
+ let hostingController = UIHostingController(rootView: rootView)
+ hostingController.view.translatesAutoresizingMaskIntoConstraints = false
+ addChild(hostingController)
+ view.addSubview(hostingController.view)
+ hostingController.didMove(toParent: self)
+
+ NSLayoutConstraint.activate([
+ hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+ hostingController.view.topAnchor.constraint(equalTo: view.topAnchor, constant: 0),
+ hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
+ ])
+ }
+
+ override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
+ extensionContext!.completeRequest(returningItems: nil, completionHandler: nil)
+ }
+}
diff --git a/MailTests/MailboxManagerTests.swift b/MailTests/MailboxManagerTests.swift
index 60d9943e9..7ed20a28e 100644
--- a/MailTests/MailboxManagerTests.swift
+++ b/MailTests/MailboxManagerTests.swift
@@ -18,6 +18,7 @@
import Foundation
import InfomaniakCore
+import InfomaniakDI
import InfomaniakLogin
@testable import MailCore
import XCTest
@@ -27,7 +28,8 @@ final class MailboxManagerTests: XCTestCase {
override class func setUp() {
super.setUp()
- mailboxManager = AccountManager.instance.getMailboxManager(for: Env.mailboxId, userId: Env.userId)
+ @InjectService var accountManager: AccountManager
+ mailboxManager = accountManager.getMailboxManager(for: Env.mailboxId, userId: Env.userId)
let token = ApiToken(accessToken: Env.token,
expiresIn: Int.max,
diff --git a/MailTests/SignatureTests.swift b/MailTests/SignatureTests.swift
index d65df772c..31f2dbb21 100644
--- a/MailTests/SignatureTests.swift
+++ b/MailTests/SignatureTests.swift
@@ -23,7 +23,7 @@ import SwiftSoup
import XCTest
final class SignatureTests: XCTestCase {
- /// Some randon HTML content
+ /// Some random HTML content
static let someMailContent = "
Hello"
/// A basic signature wrapped in the "editorUserSignature" class
diff --git a/Project.swift b/Project.swift
index a216d2316..60862aa31 100644
--- a/Project.swift
+++ b/Project.swift
@@ -18,18 +18,16 @@
import Foundation
import ProjectDescription
-
-let deploymentTarget = DeploymentTarget.iOS(targetVersion: "15.0", devices: [.iphone, .ipad])
-let baseSettings = SettingsDictionary()
- .currentProjectVersion("1")
- .marketingVersion("1.0.3")
- .automaticCodeSigning(devTeam: "864VDCS2QY")
+import ProjectDescriptionHelpers
let project = Project(name: "Mail",
packages: [
.package(url: "https://github.com/Infomaniak/ios-login", .upToNextMajor(from: "4.0.0")),
.package(url: "https://github.com/Infomaniak/ios-dependency-injection", .upToNextMajor(from: "1.1.6")),
- .package(url: "https://github.com/Infomaniak/ios-core", .revision("4eaefd644f75d833d6b1009dd94a9d6d674ccb53")),
+ .package(
+ url: "https://github.com/Infomaniak/ios-core",
+ .revision("d779a9f6619615a4b4a91fa9d3fbb48415a27470")
+ ),
.package(url: "https://github.com/Infomaniak/ios-core-ui", .upToNextMajor(from: "2.5.2")),
.package(url: "https://github.com/Infomaniak/ios-notifications", .upToNextMajor(from: "3.0.0")),
.package(url: "https://github.com/Infomaniak/ios-create-account", .upToNextMajor(from: "1.1.0")),
@@ -41,7 +39,10 @@ let project = Project(name: "Mail",
.package(url: "https://github.com/flowbe/SwiftRegex", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/matomo-org/matomo-sdk-ios", .upToNextMajor(from: "7.5.1")),
.package(url: "https://github.com/siteline/SwiftUI-Introspect", .upToNextMajor(from: "0.9.0")),
- .package(url: "https://github.com/Ambrdctr/SQRichTextEditor", .revision("04737b7694ecc6cfd78631bce5fc370f310e7e14")),
+ .package(
+ url: "https://github.com/Ambrdctr/SQRichTextEditor",
+ .revision("04737b7694ecc6cfd78631bce5fc370f310e7e14")
+ ),
.package(url: "https://github.com/markiv/SwiftUI-Shimmer", .upToNextMajor(from: "1.0.1")),
.package(url: "https://github.com/dkk/WrappingHStack", .upToNextMajor(from: "2.0.0")),
.package(url: "https://github.com/kean/Nuke", .upToNextMajor(from: "12.1.3")),
@@ -56,7 +57,7 @@ let project = Project(name: "Mail",
platform: .iOS,
product: .app,
bundleId: "com.infomaniak.mail",
- deploymentTarget: deploymentTarget,
+ deploymentTarget: Constants.deploymentTarget,
infoPlist: "Mail/Info.plist",
sources: "Mail/**",
resources: [
@@ -69,12 +70,11 @@ let project = Project(name: "Mail",
"MailResources/**/*.js"
],
entitlements: "MailResources/Mail.entitlements",
- scripts: [
- .post(path: "scripts/lint.sh", name: "Swiftlint")
- ],
+ scripts: [Constants.swiftlintScript],
dependencies: [
.target(name: "MailCore"),
.target(name: "MailNotificationServiceExtension"),
+ .target(name: "MailShareExtension"),
.package(product: "Introspect"),
.package(product: "SQRichTextEditor"),
.package(product: "Shimmer"),
@@ -84,7 +84,7 @@ let project = Project(name: "Mail",
.package(product: "Popovers"),
.package(product: "SwiftUIBackports")
],
- settings: .settings(base: baseSettings),
+ settings: .settings(base: Constants.baseSettings),
environment: ["hostname": "\(ProcessInfo.processInfo.hostName)."]),
Target(name: "MailTests",
platform: .iOS,
@@ -106,12 +106,51 @@ let project = Project(name: "Mail",
.target(name: "Mail")
]
),
+ Target(
+ name: "MailShareExtension",
+ platform: .iOS,
+ product: .appExtension,
+ bundleId: "com.infomaniak.mail.ShareExtension",
+ deploymentTarget: Constants.deploymentTarget,
+ infoPlist: .file(path: "MailShareExtension/Info.plist"),
+ sources: ["MailShareExtension/**",
+ "Mail/Views/**",
+ "Mail/Components/**",
+ "Mail/Helpers/**",
+ "Mail/Utils/**",
+ "Mail/Views/**",
+ "Mail/Proxy/Protocols/**"],
+ resources: [
+ "MailShareExtension/Base.lproj/MainInterface.storyboard",
+ "Mail/**/*.storyboard",
+ "MailResources/**/*.xcassets",
+ "MailResources/**/*.strings",
+ "MailResources/**/*.stringsdict",
+ "MailResources/**/*.json",
+ "MailResources/**/*.css",
+ "MailResources/**/*.js"
+ ],
+ entitlements: "MailShareExtension/ShareExtension.entitlements",
+ scripts: [Constants.swiftlintScript],
+ dependencies: [
+ .target(name: "MailCore"),
+ .package(product: "Introspect"),
+ .package(product: "SQRichTextEditor"),
+ .package(product: "Shimmer"),
+ .package(product: "WrappingHStack"),
+ .package(product: "Lottie"),
+ .package(product: "NavigationBackport"),
+ .package(product: "Popovers"),
+ .package(product: "SwiftUIBackports")
+ ],
+ settings: .settings(base: Constants.baseSettings)
+ ),
Target(
name: "MailNotificationServiceExtension",
platform: .iOS,
product: .appExtension,
bundleId: "com.infomaniak.mail.NotificationServiceExtension",
- deploymentTarget: deploymentTarget,
+ deploymentTarget: Constants.deploymentTarget,
infoPlist: .extendingDefault(with: [
"AppIdentifierPrefix": "$(AppIdentifierPrefix)",
"CFBundleDisplayName": "$(PRODUCT_NAME)",
@@ -127,14 +166,14 @@ let project = Project(name: "Mail",
dependencies: [
.target(name: "MailCore")
],
- settings: .settings(base: baseSettings)
+ settings: .settings(base: Constants.baseSettings)
),
Target(
name: "MailResources",
platform: .iOS,
product: .staticLibrary,
bundleId: "com.infomaniak.mail.resources",
- deploymentTarget: deploymentTarget,
+ deploymentTarget: Constants.deploymentTarget,
infoPlist: .default,
resources: [
"MailResources/**/*.xcassets",
@@ -144,14 +183,14 @@ let project = Project(name: "Mail",
"MailResources/**/*.css",
"MailResources/**/*.js"
],
- settings: .settings(base: baseSettings)
+ settings: .settings(base: Constants.baseSettings)
),
Target(
name: "MailCore",
platform: .iOS,
product: .framework,
bundleId: "com.infomaniak.mail.core",
- deploymentTarget: deploymentTarget,
+ deploymentTarget: Constants.deploymentTarget,
infoPlist: "MailCore/Info.plist",
sources: "MailCore/**",
dependencies: [
@@ -172,7 +211,7 @@ let project = Project(name: "Mail",
.package(product: "NukeUI"),
.package(product: "SwiftSoup")
],
- settings: .settings(base: baseSettings)
+ settings: .settings(base: Constants.baseSettings)
)
],
fileHeaderTemplate: .file("file-header-template.txt"))
diff --git a/Tuist/ProjectDescriptionHelpers/Constants.swift b/Tuist/ProjectDescriptionHelpers/Constants.swift
new file mode 100644
index 000000000..31070de2c
--- /dev/null
+++ b/Tuist/ProjectDescriptionHelpers/Constants.swift
@@ -0,0 +1,30 @@
+/*
+ Infomaniak kDrive - 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 ProjectDescription
+
+public enum Constants {
+ public static let baseSettings = SettingsDictionary()
+ .currentProjectVersion("1")
+ .marketingVersion("1.0.3")
+ .automaticCodeSigning(devTeam: "864VDCS2QY")
+
+ public static let deploymentTarget = DeploymentTarget.iOS(targetVersion: "15.0", devices: [.iphone, .ipad])
+
+ public static let swiftlintScript = TargetScript.post(path: "scripts/lint.sh", name: "Swiftlint")
+}
diff --git a/Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift b/Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift
new file mode 100644
index 000000000..4d2d540a2
--- /dev/null
+++ b/Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift
@@ -0,0 +1,23 @@
+/*
+ Infomaniak kDrive - 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 ProjectDescription
+
+public extension Target {
+ // TODO: move contructors here if needed
+}