From b94730b63d6004d30d6efede2606a8e12f88a825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 22 Jun 2023 11:40:10 +0200 Subject: [PATCH 01/61] feat: initial project setup share extension --- .../Base.lproj/MainInterface.storyboard | 24 ++++++++++ MailShareExtension/Info.plist | 18 ++++++++ .../ShareExtension.entitlements | 14 ++++++ MailShareExtension/ShareViewController.swift | 41 +++++++++++++++++ Project.swift | 46 ++++++++++++------- .../ProjectDescriptionHelpers/Constants.swift | 30 ++++++++++++ .../ExtensionTarget.swift | 23 ++++++++++ 7 files changed, 179 insertions(+), 17 deletions(-) create mode 100644 MailShareExtension/Base.lproj/MainInterface.storyboard create mode 100644 MailShareExtension/Info.plist create mode 100644 MailShareExtension/ShareExtension.entitlements create mode 100644 MailShareExtension/ShareViewController.swift create mode 100644 Tuist/ProjectDescriptionHelpers/Constants.swift create mode 100644 Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift diff --git a/MailShareExtension/Base.lproj/MainInterface.storyboard b/MailShareExtension/Base.lproj/MainInterface.storyboard new file mode 100644 index 000000000..286a50894 --- /dev/null +++ b/MailShareExtension/Base.lproj/MainInterface.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MailShareExtension/Info.plist b/MailShareExtension/Info.plist new file mode 100644 index 000000000..4b1f7e705 --- /dev/null +++ b/MailShareExtension/Info.plist @@ -0,0 +1,18 @@ + + + + + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + TRUEPREDICATE + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.share-services + + + 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..43b613507 --- /dev/null +++ b/MailShareExtension/ShareViewController.swift @@ -0,0 +1,41 @@ +/* + Infomaniak Mail - iOS App + Copyright (C) 2022 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import UIKit +import Social + +class ShareViewController: SLComposeServiceViewController { + + override func isContentValid() -> Bool { + // Do validation of contentText and/or NSExtensionContext attachments here + return true + } + + override func didSelectPost() { + // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments. + + // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context. + self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + } + + override func configurationItems() -> [Any]! { + // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here. + return [] + } + +} diff --git a/Project.swift b/Project.swift index 5e8cf2183..79b1340bf 100644 --- a/Project.swift +++ b/Project.swift @@ -18,12 +18,7 @@ import Foundation import ProjectDescription - -let deploymentTarget = DeploymentTarget.iOS(targetVersion: "15.0", devices: [.iphone, .ipad]) -let baseSettings = SettingsDictionary() - .currentProjectVersion("1") - .marketingVersion("1.0.0") - .automaticCodeSigning(devTeam: "864VDCS2QY") +import ProjectDescriptionHelpers let project = Project(name: "Mail", packages: [ @@ -56,7 +51,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 +64,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 +78,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 +100,30 @@ let project = Project(name: "Mail", .target(name: "Mail") ] ), + Target( + name: "MailShareExtension", + platform: .iOS, + product: .appExtension, + bundleId: "com.infomaniak.mail.MailShareExtension", + deploymentTarget: Constants.deploymentTarget, + infoPlist: .file(path: "MailShareExtension/Info.plist"), + sources: "MailShareExtension/**", + resources:[ + "MailShareExtension/Base.lproj/MainInterface.storyboard", + "MailShareExtension/ShareExtension.entitlements" + ], + entitlements: "MailResources/Mail.entitlements", + dependencies: [ + .target(name: "MailCore") + ], + 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 +139,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 +156,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 +184,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..ee26e3844 --- /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.0") + .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..74c4e2b86 --- /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 +} From 61b028df69cfdff80381b1c6857f03dcb7189d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 27 Jun 2023 17:36:15 +0200 Subject: [PATCH 02/61] feat: wrap SwUI view on share ext open --- .../Base.lproj/MainInterface.storyboard | 15 ++++--- MailShareExtension/Info.plist | 22 +++++++++- MailShareExtension/ShareViewController.swift | 43 ++++++++++++------- 3 files changed, 58 insertions(+), 22 deletions(-) diff --git a/MailShareExtension/Base.lproj/MainInterface.storyboard b/MailShareExtension/Base.lproj/MainInterface.storyboard index 286a50894..34049b372 100644 --- a/MailShareExtension/Base.lproj/MainInterface.storyboard +++ b/MailShareExtension/Base.lproj/MainInterface.storyboard @@ -1,24 +1,27 @@ - + + - + + - + - + - + - + + diff --git a/MailShareExtension/Info.plist b/MailShareExtension/Info.plist index 4b1f7e705..7c89d6499 100644 --- a/MailShareExtension/Info.plist +++ b/MailShareExtension/Info.plist @@ -2,12 +2,32 @@ + 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 - TRUEPREDICATE + SUBQUERY (extensionItems, $extensionItem, SUBQUERY ($extensionItem.attachments, $attachment, (ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.data")).@count == $extensionItem.attachments.@count ).@count > 0 NSExtensionMainStoryboard MainInterface diff --git a/MailShareExtension/ShareViewController.swift b/MailShareExtension/ShareViewController.swift index 43b613507..1c32086cf 100644 --- a/MailShareExtension/ShareViewController.swift +++ b/MailShareExtension/ShareViewController.swift @@ -16,26 +16,39 @@ along with this program. If not, see . */ -import UIKit +import InfomaniakCoreUI import Social +import SwiftUI +import UIKit -class ShareViewController: SLComposeServiceViewController { - - override func isContentValid() -> Bool { - // Do validation of contentText and/or NSExtensionContext attachments here - return true +class ShareNavigationViewController: TitleSizeAdjustingNavigationController { + override public func viewDidLoad() { + super.viewDidLoad() + + // Modify sheet size on iPadOS, property is ignored on iOS + preferredContentSize = CGSize(width: 540, height: 620) + + guard let attachments = (extensionContext?.inputItems.first as? NSExtensionItem)?.attachments else { + dismiss(animated: true) + return + } + + // To my knowledge, we need to go threw wrapping to use SwiftUI here. + let childView = UIHostingController(rootView: SwiftUIView()) + addChild(childView) + childView.view.frame = self.view.bounds + self.view.addSubview(childView.view) + childView.didMove(toParent: self) } - override func didSelectPost() { - // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments. - - // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context. - self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) + override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + extensionContext!.completeRequest(returningItems: nil, completionHandler: nil) } +} - override func configurationItems() -> [Any]! { - // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here. - return [] +struct SwiftUIView: View { + var body: some View { + Text("test") + .background(.red) } - } From 15e3769254b9ef2a1cb4262ff24834a2044ed5c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 28 Jun 2023 17:13:17 +0200 Subject: [PATCH 03/61] feat(WIP): split code so views can be embedded in share ext --- Mail/AppDelegate.swift | 7 +-- Mail/SceneDelegate.swift | 11 +++-- Mail/Utils/View+Extension.swift | 34 +++++++++----- Mail/Views/Attachment/AttachmentPreview.swift | 4 +- .../Items/MenuDrawerItemsListView.swift | 8 +++- Mail/Views/Onboarding/OnboardingView.swift | 7 ++- Mail/Views/Proxy/AccountSwitcher.swift | 19 ++++++++ Mail/Views/Proxy/CacheManager.swift | 36 +++++++++++++++ Mail/Views/Proxy/OpenURL.swift | 44 ++++++++++++++++++ Mail/Views/Proxy/OrientationLock.swift | 45 +++++++++++++++++++ .../Proxy/RemoteNotificationRegistrer.swift | 36 +++++++++++++++ Mail/Views/Proxy/RootViewController.swift | 39 ++++++++++++++++ ...ettingsNotificationsInstructionsView.swift | 7 +-- .../General/SettingsNotificationsView.swift | 5 +-- Mail/Views/Settings/SettingsOptionView.swift | 4 +- Mail/Views/SplitView.swift | 5 ++- Mail/Views/Switch User/AccountCellView.swift | 5 ++- Mail/Views/Switch User/AccountListView.swift | 3 +- Mail/Views/Switch User/AccountView.swift | 8 +++- .../Thread List/ThreadListModifiers.swift | 3 +- Mail/Views/Thread/WebView.swift | 5 ++- Project.swift | 28 ++++++++++-- 22 files changed, 326 insertions(+), 37 deletions(-) create mode 100644 Mail/Views/Proxy/AccountSwitcher.swift create mode 100644 Mail/Views/Proxy/CacheManager.swift create mode 100644 Mail/Views/Proxy/OpenURL.swift create mode 100644 Mail/Views/Proxy/OrientationLock.swift create mode 100644 Mail/Views/Proxy/RemoteNotificationRegistrer.swift create mode 100644 Mail/Views/Proxy/RootViewController.swift diff --git a/Mail/AppDelegate.swift b/Mail/AppDelegate.swift index 3948de868..32bc30e72 100644 --- a/Mail/AppDelegate.swift +++ b/Mail/AppDelegate.swift @@ -28,11 +28,12 @@ import Sentry import SwiftUI import UIKit -@main +@main @available(iOSApplicationExtension, unavailable) class AppDelegate: UIResponder, UIApplicationDelegate { private let notificationCenterDelegate = NotificationCenterDelegate() private var accountManager: AccountManager! - static var orientationLock = UIInterfaceOrientationMask.all + + @LazyInjectService var orientationManager: OrientationManageable func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { @@ -93,7 +94,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { - return AppDelegate.orientationLock + return orientationManager.orientationLock } func refreshCacheData() { diff --git a/Mail/SceneDelegate.swift b/Mail/SceneDelegate.swift index 234813ac4..4eaaf53eb 100644 --- a/Mail/SceneDelegate.swift +++ b/Mail/SceneDelegate.swift @@ -27,6 +27,8 @@ import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate, AccountManagerDelegate { var window: UIWindow? + @LazyInjectService var cacheManager: CacheManageable + private var accountManager: AccountManager! @LazyInjectService var appLockHelper: AppLockHelper @@ -69,7 +71,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, AccountManagerDelegate func sceneWillEnterForeground(_ scene: UIScene) { // Called as the scene transitions from the background to the foreground. // Use this method to undo the changes made on entering the background. - (UIApplication.shared.delegate as? AppDelegate)?.refreshCacheData() + cacheManager.refreshCacheData() if UserDefaults.shared.isAppLockEnabled && appLockHelper.isAppLocked { showLockView() @@ -130,7 +132,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, AccountManagerDelegate func switchAccount(_ account: Account, mailbox: Mailbox? = nil) { accountManager.switchAccount(newAccount: account) - (UIApplication.shared.delegate as? AppDelegate)?.refreshCacheData() + cacheManager.refreshCacheData() if let mailbox = mailbox { switchMailbox(mailbox) @@ -181,7 +183,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, AccountManagerDelegate guard let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return false } if Constants.isMailTo(url) { - NotificationCenter.default.post(name: .onOpenedMailTo, object: IdentifiableURLComponents(urlComponents: urlComponents)) + NotificationCenter.default.post( + name: .onOpenedMailTo, + object: IdentifiableURLComponents(urlComponents: urlComponents) + ) } return true diff --git a/Mail/Utils/View+Extension.swift b/Mail/Utils/View+Extension.swift index c1b9a7ec9..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)) @@ -103,11 +118,10 @@ extension View { } func emptyState(isEmpty: Bool, @ViewBuilder emptyView: () -> T) -> some View where T: View { - self - .overlay { - if isEmpty { - emptyView() - } + overlay { + if isEmpty { + emptyView() } + } } } diff --git a/Mail/Views/Attachment/AttachmentPreview.swift b/Mail/Views/Attachment/AttachmentPreview.swift index 69119cb8b..e9ff8b6d0 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: RootViewControllerFetcheable + 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/Menu Drawer/Items/MenuDrawerItemsListView.swift b/Mail/Views/Menu Drawer/Items/MenuDrawerItemsListView.swift index 989718c4a..bf03ff0e0 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 + @LazyInjectService var urlNavigator: URLNavigable + 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) + urlNavigator.openUrl(URLConstants.importMails.url) } if mailboxCanRestoreEmails { MenuDrawerItemCell( @@ -57,6 +59,8 @@ struct MenuDrawerItemsHelpListView: View { @State private var isShowingHelp = false @State private var isShowingBugTracker = false + @LazyInjectService var urlNavigator: URLNavigable + var body: some View { MenuDrawerItemsListView { MenuDrawerItemCell(icon: MailResourcesAsset.feedback, @@ -83,7 +87,7 @@ struct MenuDrawerItemsHelpListView: View { if AccountManager.instance.currentAccount?.user?.isStaff == true { isShowingBugTracker.toggle() } else if let userReportURL = URL(string: MailResourcesStrings.Localizable.urlUserReportiOS) { - UIApplication.shared.open(userReportURL) + urlNavigator.openUrl(userReportURL) } } } diff --git a/Mail/Views/Onboarding/OnboardingView.swift b/Mail/Views/Onboarding/OnboardingView.swift index 405f75975..9072375e7 100644 --- a/Mail/Views/Onboarding/OnboardingView.swift +++ b/Mail/Views/Onboarding/OnboardingView.swift @@ -71,6 +71,7 @@ struct Slide: Identifiable { class LoginHandler: InfomaniakLoginDelegate, ObservableObject { @LazyInjectService var loginService: InfomaniakLoginable @LazyInjectService var matomo: MatomoUtils + @LazyInjectService var remoteNotificationRegistrer: RemoteNotificationRegistrable @Published var isLoading = false @Published var isPresentingErrorAlert = false @@ -128,7 +129,7 @@ class LoginHandler: InfomaniakLoginDelegate, ObservableObject { do { _ = try await AccountManager.instance.createAndSetCurrentAccount(code: code, codeVerifier: verifier) sceneDelegate?.showMainView() - UIApplication.shared.registerForRemoteNotifications() + remoteNotificationRegistrer.register() } catch let error as MailError where error == MailError.noMailbox { sceneDelegate?.showNoMailboxView() } catch { @@ -152,6 +153,8 @@ struct OnboardingView: View { @Environment(\.window) private var window @Environment(\.dismiss) private var dismiss + @LazyInjectService var orientationManager: OrientationManageable + @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor @State private var selection: Int @@ -246,7 +249,7 @@ struct OnboardingView: View { if UIDevice.current.userInterfaceIdiom == .phone { UIDevice.current .setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation") - AppDelegate.orientationLock = .portrait + orientationManager.orientationLock = .portrait UIViewController.attemptRotationToDeviceOrientation() } } diff --git a/Mail/Views/Proxy/AccountSwitcher.swift b/Mail/Views/Proxy/AccountSwitcher.swift new file mode 100644 index 000000000..1ac69b5ee --- /dev/null +++ b/Mail/Views/Proxy/AccountSwitcher.swift @@ -0,0 +1,19 @@ +/* + 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 diff --git a/Mail/Views/Proxy/CacheManager.swift b/Mail/Views/Proxy/CacheManager.swift new file mode 100644 index 000000000..199bee177 --- /dev/null +++ b/Mail/Views/Proxy/CacheManager.swift @@ -0,0 +1,36 @@ +/* + 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 handle rotation lock +public protocol CacheManageable { + func refreshCacheData() +} + +public final class CacheManagerStub: CacheManageable { + public func refreshCacheData() {} +} + +@available(iOSApplicationExtension, unavailable) +public final class CacheManager: CacheManageable { + public func refreshCacheData() { + (UIApplication.shared.delegate as? AppDelegate)?.refreshCacheData() + } +} diff --git a/Mail/Views/Proxy/OpenURL.swift b/Mail/Views/Proxy/OpenURL.swift new file mode 100644 index 000000000..601119c3f --- /dev/null +++ b/Mail/Views/Proxy/OpenURL.swift @@ -0,0 +1,44 @@ +/* + 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 open URL in an abstract way +public protocol URLNavigable { + func openUrl(_ url: URL) + func openUrlIfPossible(_ url: URL) +} + +public struct URLNavigatorStub: URLNavigable { + public func openUrl(_ url: URL) {} + public func openUrlIfPossible(_ url: URL) {} +} + +@available(iOSApplicationExtension, unavailable) +public struct URLNavigator: URLNavigable { + public func openUrl(_ url: URL) { + UIApplication.shared.open(url) + } + + public func openUrlIfPossible(_ url: URL) { + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + } +} diff --git a/Mail/Views/Proxy/OrientationLock.swift b/Mail/Views/Proxy/OrientationLock.swift new file mode 100644 index 000000000..c27cf8226 --- /dev/null +++ b/Mail/Views/Proxy/OrientationLock.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 Foundation +import InfomaniakCoreUI +import UIKit + +/// Something that can handle rotation lock +public protocol OrientationManageable { + var orientationLock: UIInterfaceOrientationMask { get set } + + var interfaceOrientation: UIInterfaceOrientation? { get } +} + +public final class OrientationManagerStub: OrientationManageable { + + public var orientationLock = UIInterfaceOrientationMask.all + + public var interfaceOrientation: UIInterfaceOrientation? +} + +@available(iOSApplicationExtension, unavailable) +public final class OrientationManager: OrientationManageable { + /// Default to .all + public var orientationLock = UIInterfaceOrientationMask.all + + public var interfaceOrientation: UIInterfaceOrientation? { + UIApplication.shared.mainSceneKeyWindow?.windowScene?.interfaceOrientation + } +} diff --git a/Mail/Views/Proxy/RemoteNotificationRegistrer.swift b/Mail/Views/Proxy/RemoteNotificationRegistrer.swift new file mode 100644 index 000000000..10331e690 --- /dev/null +++ b/Mail/Views/Proxy/RemoteNotificationRegistrer.swift @@ -0,0 +1,36 @@ +/* + 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 handle rotation lock +public protocol RemoteNotificationRegistrable { + func register() +} + +public final class RemoteNotificationRegistrerStub: RemoteNotificationRegistrable { + public func register() {} +} + +@available(iOSApplicationExtension, unavailable) +public final class RemoteNotificationRegistrer: RemoteNotificationRegistrable { + public func register() { + UIApplication.shared.registerForRemoteNotifications() + } +} diff --git a/Mail/Views/Proxy/RootViewController.swift b/Mail/Views/Proxy/RootViewController.swift new file mode 100644 index 000000000..4e82091fc --- /dev/null +++ b/Mail/Views/Proxy/RootViewController.swift @@ -0,0 +1,39 @@ +/* + 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 + +/// Something that can fetch the Root View Controller +public protocol RootViewControllerFetcheable { + var rootViewController: UIViewController? { get } +} + +public struct RootViewControllerFetcherStub: RootViewControllerFetcheable { + public var rootViewController: UIViewController? { + nil + } +} + +@available(iOSApplicationExtension, unavailable) +public struct RootViewControllerFetcher: RootViewControllerFetcheable { + public var rootViewController: UIViewController? { + UIApplication.shared.mainSceneKeyWindow?.rootViewController + } +} diff --git a/Mail/Views/Settings/General/SettingsNotificationsInstructionsView.swift b/Mail/Views/Settings/General/SettingsNotificationsInstructionsView.swift index 22d00fe0d..e26dc8cdb 100644 --- a/Mail/Views/Settings/General/SettingsNotificationsInstructionsView.swift +++ b/Mail/Views/Settings/General/SettingsNotificationsInstructionsView.swift @@ -18,10 +18,13 @@ import MailResources import SwiftUI +import InfomaniakDI struct SettingsNotificationsInstructionsView: View { @Environment(\.dismiss) private var dismiss + @LazyInjectService private var urlNavigator: URLNavigable + var body: some View { VStack(alignment: .leading, spacing: 24) { Text(MailResourcesStrings.Localizable.alertNotificationsDisabledTitle) @@ -40,9 +43,7 @@ struct SettingsNotificationsInstructionsView: View { return } - if UIApplication.shared.canOpenURL(settingsUrl) { - UIApplication.shared.open(settingsUrl) - } + urlNavigator.openUrlIfPossible(settingsUrl) } } diff --git a/Mail/Views/Settings/General/SettingsNotificationsView.swift b/Mail/Views/Settings/General/SettingsNotificationsView.swift index 6b364d3b0..6c01f1ff0 100644 --- a/Mail/Views/Settings/General/SettingsNotificationsView.swift +++ b/Mail/Views/Settings/General/SettingsNotificationsView.swift @@ -27,6 +27,7 @@ import SwiftUI struct SettingsNotificationsView: View { @LazyInjectService private var notificationService: InfomaniakNotifications @LazyInjectService private var matomo: MatomoUtils + @LazyInjectService private var urlNavigator: URLNavigable @AppStorage(UserDefaults.shared.key(.notificationsEnabled)) private var notificationsEnabled = DefaultPreferences .notificationsEnabled @@ -48,9 +49,7 @@ struct SettingsNotificationsView: View { return } - if UIApplication.shared.canOpenURL(settingsUrl) { - UIApplication.shared.open(settingsUrl) - } + urlNavigator.openUrlIfPossible(settingsUrl) } .mailButtonStyle(.link) } diff --git a/Mail/Views/Settings/SettingsOptionView.swift b/Mail/Views/Settings/SettingsOptionView.swift index 3d5ab80f3..18ec8d371 100644 --- a/Mail/Views/Settings/SettingsOptionView.swift +++ b/Mail/Views/Settings/SettingsOptionView.swift @@ -40,7 +40,9 @@ struct SettingsOptionView: View where OptionEnum: CaseIterable, Opti UserDefaults.shared[keyPath: keyPath] = selectedValue switch keyPath { case \.theme, \.accentColor: - UIApplication.shared.connectedScenes.forEach { ($0.delegate as? SceneDelegate)?.updateWindowUI() } + break + // FIXME +// UIApplication.shared.connectedScenes.forEach { ($0.delegate as? SceneDelegate)?.updateWindowUI() } default: break } diff --git a/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift index a4e5bcf97..f751a3ce2 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 @@ -48,6 +49,8 @@ struct SplitView: View { @StateObject private var navigationStore = NavigationStore() @StateObject private var splitViewManager: SplitViewManager + @LazyInjectService private var orientationManager: OrientationManageable + let mailboxManager: MailboxManager private var isCompact: Bool { @@ -119,7 +122,7 @@ struct SplitView: View { self.mailToURLComponents = identifiableURLComponents.object as? IdentifiableURLComponents } .onAppear { - AppDelegate.orientationLock = .all + orientationManager.orientationLock = .all } .task { await fetchSignatures() diff --git a/Mail/Views/Switch User/AccountCellView.swift b/Mail/Views/Switch User/AccountCellView.swift index f3b0271f7..01a8ec1e9 100644 --- a/Mail/Views/Switch User/AccountCellView.swift +++ b/Mail/Views/Switch User/AccountCellView.swift @@ -49,7 +49,10 @@ struct AccountCellView: View { matomo.track(eventWithCategory: .account, name: "switch") withAnimation { selectedUserId = selectedUserId == account.userId ? nil : account.userId - (window?.windowScene?.delegate as? SceneDelegate)?.switchAccount(account) + /// TODO extract switch account logic + if !Bundle.main.isExtension { + (window?.windowScene?.delegate as? SceneDelegate)?.switchAccount(account) + } } } label: { AccountHeaderCell(account: account, isSelected: Binding(get: { diff --git a/Mail/Views/Switch User/AccountListView.swift b/Mail/Views/Switch User/AccountListView.swift index 8e309e603..bf0188d13 100644 --- a/Mail/Views/Switch User/AccountListView.swift +++ b/Mail/Views/Switch User/AccountListView.swift @@ -61,6 +61,7 @@ struct AccountListView: View { @State var isShowingNewAccountView = false @LazyInjectService private var matomo: MatomoUtils + @LazyInjectService private var orientationManager: OrientationManageable var body: some View { ScrollView { @@ -79,7 +80,7 @@ struct AccountListView: View { isShowingNewAccountView = true } .fullScreenCover(isPresented: $isShowingNewAccountView, onDismiss: { - AppDelegate.orientationLock = .all + orientationManager.orientationLock = .all }, content: { OnboardingView(page: 4, isScrollEnabled: false) }) diff --git a/Mail/Views/Switch User/AccountView.swift b/Mail/Views/Switch User/AccountView.swift index 39bd1b282..3f65e0dcc 100644 --- a/Mail/Views/Switch User/AccountView.swift +++ b/Mail/Views/Switch User/AccountView.swift @@ -25,17 +25,21 @@ import MailResources import Sentry import SwiftUI +@available(iOSApplicationExtension, unavailable) class AccountViewDelegate: DeleteAccountDelegate { @MainActor func didCompleteDeleteAccount() { guard let account = AccountManager.instance.currentAccount else { return } - let window = UIApplication.shared.mainSceneKeyWindow + AccountManager.instance.removeTokenAndAccount(token: account.token) + + let window = UIApplication.shared.mainSceneKeyWindow if let nextAccount = AccountManager.instance.accounts.first { (window?.windowScene?.delegate as? SceneDelegate)?.switchAccount(nextAccount) IKSnackBar.showSnackBar(message: "Account deleted") } else { (window?.windowScene?.delegate as? SceneDelegate)?.showLoginView() } + AccountManager.instance.saveAccounts() } @@ -45,6 +49,7 @@ class AccountViewDelegate: DeleteAccountDelegate { } } +@available(iOSApplicationExtension, unavailable) struct AccountView: View { @Environment(\.dismiss) private var dismiss @Environment(\.window) private var window @@ -164,6 +169,7 @@ struct AccountView: View { } } +@available(iOSApplicationExtension, unavailable) struct AccountView_Previews: PreviewProvider { static var previews: some View { AccountView(mailboxes: [PreviewHelper.sampleMailbox]) diff --git a/Mail/Views/Thread List/ThreadListModifiers.swift b/Mail/Views/Thread List/ThreadListModifiers.swift index bd4d8bc6a..3e9e8b1e2 100644 --- a/Mail/Views/Thread List/ThreadListModifiers.swift +++ b/Mail/Views/Thread List/ThreadListModifiers.swift @@ -167,7 +167,8 @@ struct ThreadListToolbar: ViewModifier { ) .navigationBarTitleDisplayMode(.inline) .sheet(isPresented: $isShowingSwitchAccount) { - AccountView(mailboxes: AccountManager.instance.mailboxes) + // FIXME +// AccountView(mailboxes: AccountManager.instance.mailboxes) } } } diff --git a/Mail/Views/Thread/WebView.swift b/Mail/Views/Thread/WebView.swift index 447eeb912..98a73f793 100644 --- a/Mail/Views/Thread/WebView.swift +++ b/Mail/Views/Thread/WebView.swift @@ -16,6 +16,7 @@ along with this program. If not, see . */ +import InfomaniakDI import MailCore import SwiftUI import WebKit @@ -34,6 +35,8 @@ struct WebView: UIViewRepresentable { class Coordinator: NSObject, WKNavigationDelegate { var parent: WebView + @LazyInjectService var urlNavigator: URLNavigable + init(_ parent: WebView) { self.parent = parent } @@ -70,7 +73,7 @@ struct WebView: UIViewRepresentable { if navigationAction.navigationType == .linkActivated { if let url = navigationAction.request.url { decisionHandler(.cancel) - UIApplication.shared.open(url) + urlNavigator.openUrl(url) } } else { decisionHandler(.allow) diff --git a/Project.swift b/Project.swift index 79b1340bf..580b19ba3 100644 --- a/Project.swift +++ b/Project.swift @@ -107,14 +107,36 @@ let project = Project(name: "Mail", bundleId: "com.infomaniak.mail.MailShareExtension", deploymentTarget: Constants.deploymentTarget, infoPlist: .file(path: "MailShareExtension/Info.plist"), - sources: "MailShareExtension/**", + sources: ["MailShareExtension/**", + "Mail/**", + "Mail/Views/New Message/**", + "Mail/Views/**", + "Mail/Components/**", + "Mail/Helpers/**", + "Mail/Utils/**", + "Mail/Views/**"], resources:[ "MailShareExtension/Base.lproj/MainInterface.storyboard", - "MailShareExtension/ShareExtension.entitlements" + "MailShareExtension/ShareExtension.entitlements", + "Mail/**/*.storyboard", + "MailResources/**/*.xcassets", + "MailResources/**/*.strings", + "MailResources/**/*.stringsdict", + "MailResources/**/*.json", + "MailResources/**/*.css", + "MailResources/**/*.js" ], entitlements: "MailResources/Mail.entitlements", dependencies: [ - .target(name: "MailCore") + .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) ), From 3b9571e790fb80499cc5f9326903e55e6f27fc45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 29 Jun 2023 13:15:01 +0200 Subject: [PATCH 04/61] feat: kMail share extension builds with all classes of the main project, thanks to some refactor using DI to abstract types not availlable in Extension mode feat: AccountManager is now availlable with DI instead of static ref. --- Mail/AppDelegate.swift | 37 ++++++++++++++++--- .../NotificationCenterDelegate.swift | 13 ++++--- Mail/{ => Helpers}/SceneDelegate.swift | 4 +- .../Implementation}/CacheManager.swift | 9 ----- .../Implementation}/OrientationLock.swift | 18 ++------- .../RemoteNotificationRegistrer.swift | 9 ----- .../Implementation}/RootViewController.swift | 21 +++++------ .../Implementation/URLNavigator.swift} | 11 ------ .../Protocols/CacheManageable.swift} | 5 +++ .../Protocols/OrientationManageable.swift | 32 ++++++++++++++++ .../RemoteNotificationRegistrable.swift | 24 ++++++++++++ Mail/Proxy/Protocols/RootViewManageable.swift | 32 ++++++++++++++++ Mail/Proxy/Protocols/URLNavigable.swift | 29 +++++++++++++++ .../Views/Alerts/LogoutConfirmationView.swift | 8 ++-- Mail/Views/Attachment/AttachmentPreview.swift | 2 +- .../Actions/ActionsViewModel.swift | 3 +- .../Bottom sheets/ContactActionsView.swift | 7 +++- Mail/{ => Views}/LockedAppView.swift | 0 .../Items/MenuDrawerItemsListView.swift | 5 ++- .../MailboxManagement/MailboxCell.swift | 4 +- .../MailboxesManagementView.swift | 11 ++++-- .../New Message/AutocompletionView.swift | 5 ++- Mail/Views/Onboarding/OnboardingView.swift | 19 +++++----- Mail/Views/Search/SearchViewModel.swift | 5 ++- .../General/SettingsNotificationsView.swift | 7 ++-- .../Views/Settings/General/SettingsView.swift | 7 +++- Mail/Views/Settings/SettingsOptionView.swift | 10 ++--- Mail/Views/SplitView.swift | 2 +- Mail/Views/Switch User/AccountListView.swift | 18 ++++++--- Mail/Views/Switch User/AccountView.swift | 32 +++++++++++----- Mail/Views/Switch User/AddMailboxView.swift | 4 +- .../Thread List/ThreadListModifiers.swift | 6 +-- MailCore/API/MailApiFetcher.swift | 11 ++++-- MailCore/Cache/AccountManager.swift | 5 +-- MailCore/Models/MergedContact.swift | 8 +++- MailCore/Models/Recipient.swift | 17 ++++++--- MailCore/Utils/NotificationsHelper.swift | 5 ++- MailCore/Utils/URLSchemeHandler.swift | 7 +++- .../NotificationService.swift | 6 ++- MailShareExtension/Proxy/CacheManager.swift | 25 +++++++++++++ .../Proxy/OrientationManager.swift | 31 ++++++++++++++++ .../Proxy/RemoteNotificationRegistrer.swift | 25 +++++++++++++ .../Proxy/RootViewManager.swift | 34 +++++++++++++++++ MailShareExtension/Proxy/URLNavigator.swift | 26 +++++++++++++ Project.swift | 5 +-- 45 files changed, 457 insertions(+), 147 deletions(-) rename Mail/{ => Helpers}/NotificationCenterDelegate.swift (82%) rename Mail/{ => Helpers}/SceneDelegate.swift (98%) rename Mail/{Views/Proxy => Proxy/Implementation}/CacheManager.swift (82%) rename Mail/{Views/Proxy => Proxy/Implementation}/OrientationLock.swift (73%) rename Mail/{Views/Proxy => Proxy/Implementation}/RemoteNotificationRegistrer.swift (80%) rename Mail/{Views/Proxy => Proxy/Implementation}/RootViewController.swift (66%) rename Mail/{Views/Proxy/OpenURL.swift => Proxy/Implementation/URLNavigator.swift} (78%) rename Mail/{Views/Proxy/AccountSwitcher.swift => Proxy/Protocols/CacheManageable.swift} (86%) create mode 100644 Mail/Proxy/Protocols/OrientationManageable.swift create mode 100644 Mail/Proxy/Protocols/RemoteNotificationRegistrable.swift create mode 100644 Mail/Proxy/Protocols/RootViewManageable.swift create mode 100644 Mail/Proxy/Protocols/URLNavigable.swift rename Mail/{ => Views}/LockedAppView.swift (100%) create mode 100644 MailShareExtension/Proxy/CacheManager.swift create mode 100644 MailShareExtension/Proxy/OrientationManager.swift create mode 100644 MailShareExtension/Proxy/RemoteNotificationRegistrer.swift create mode 100644 MailShareExtension/Proxy/RootViewManager.swift create mode 100644 MailShareExtension/Proxy/URLNavigator.swift diff --git a/Mail/AppDelegate.swift b/Mail/AppDelegate.swift index 32bc30e72..8b2ebd8a7 100644 --- a/Mail/AppDelegate.swift +++ b/Mail/AppDelegate.swift @@ -31,16 +31,15 @@ import UIKit @main @available(iOSApplicationExtension, unavailable) class AppDelegate: UIResponder, UIApplicationDelegate { private let notificationCenterDelegate = NotificationCenterDelegate() - private var accountManager: AccountManager! - - @LazyInjectService var orientationManager: OrientationManageable + + @LazyInjectService private var orientationManager: OrientationManageable + @LazyInjectService private var accountManager: AccountManager func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { Logging.initLogging() setupDI() DDLogInfo("Application starting in foreground ? \(UIApplication.shared.applicationState != .background)") - accountManager = AccountManager.instance ApiFetcher.decoder.dateDecodingStrategy = .iso8601 UNUserNotificationCenter.current().delegate = notificationCenterDelegate @@ -98,7 +97,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } func refreshCacheData() { - guard let currentAccount = AccountManager.instance.currentAccount else { + guard let currentAccount = accountManager.currentAccount else { return } @@ -142,6 +141,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let draftManager = Factory(type: DraftManager.self) { _, _ in DraftManager() } + let accountManager = Factory(type: AccountManager.self) { _, _ in + AccountManager() + } SimpleResolver.sharedResolver.store(factory: networkLoginService) SimpleResolver.sharedResolver.store(factory: loginService) @@ -152,5 +154,30 @@ class AppDelegate: UIResponder, UIApplicationDelegate { SimpleResolver.sharedResolver.store(factory: matomoUtils) SimpleResolver.sharedResolver.store(factory: avoider) SimpleResolver.sharedResolver.store(factory: draftManager) + SimpleResolver.sharedResolver.store(factory: accountManager) + + setupProxyInDI() + } + + private func setupProxyInDI() { + let factories = [ + Factory(type: CacheManageable.self) { _, _ in + CacheManager() + }, + Factory(type: OrientationManageable.self) { _, _ in + OrientationManager() + }, + Factory(type: RemoteNotificationRegistrable.self) { _, _ in + RemoteNotificationRegistrer() + }, + Factory(type: RootViewManageable.self) { _, _ in + RootViewManager() + }, + Factory(type: URLNavigable.self) { _, _ in + URLNavigator() + } + ] + + factories.forEach { SimpleResolver.sharedResolver.store(factory: $0) } } } diff --git a/Mail/NotificationCenterDelegate.swift b/Mail/Helpers/NotificationCenterDelegate.swift similarity index 82% rename from Mail/NotificationCenterDelegate.swift rename to Mail/Helpers/NotificationCenterDelegate.swift index 7bcc51ae0..2ec61bc17 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,18 +27,20 @@ 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.currentAccount.userId != mailboxManager.mailbox.userId { - if let switchedAccount = AccountManager.instance.accounts + if accountManager.currentMailboxManager?.mailbox != mailboxManager.mailbox { + if accountManager.currentAccount.userId != mailboxManager.mailbox.userId { + if let switchedAccount = accountManager.accounts .first(where: { $0.userId == mailboxManager.mailbox.userId }) { (scene?.delegate as? SceneDelegate)?.switchAccount(switchedAccount, mailbox: mailbox) } diff --git a/Mail/SceneDelegate.swift b/Mail/Helpers/SceneDelegate.swift similarity index 98% rename from Mail/SceneDelegate.swift rename to Mail/Helpers/SceneDelegate.swift index 4eaaf53eb..39198223e 100644 --- a/Mail/SceneDelegate.swift +++ b/Mail/Helpers/SceneDelegate.swift @@ -28,9 +28,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, AccountManagerDelegate var window: UIWindow? @LazyInjectService var cacheManager: CacheManageable - - private var accountManager: AccountManager! @LazyInjectService var appLockHelper: AppLockHelper + @LazyInjectService private var accountManager: AccountManager func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. @@ -38,7 +37,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, AccountManagerDelegate // This delegate does not imply the connecting scene or session are new (see // `application:configurationForConnectingSceneSession` instead). guard let _ = (scene as? UIWindowScene) else { return } - accountManager = AccountManager.instance accountManager.delegate = self updateWindowUI() setupLaunch() diff --git a/Mail/Views/Proxy/CacheManager.swift b/Mail/Proxy/Implementation/CacheManager.swift similarity index 82% rename from Mail/Views/Proxy/CacheManager.swift rename to Mail/Proxy/Implementation/CacheManager.swift index 199bee177..80156d73e 100644 --- a/Mail/Views/Proxy/CacheManager.swift +++ b/Mail/Proxy/Implementation/CacheManager.swift @@ -19,15 +19,6 @@ import Foundation import UIKit -/// Something that can handle rotation lock -public protocol CacheManageable { - func refreshCacheData() -} - -public final class CacheManagerStub: CacheManageable { - public func refreshCacheData() {} -} - @available(iOSApplicationExtension, unavailable) public final class CacheManager: CacheManageable { public func refreshCacheData() { diff --git a/Mail/Views/Proxy/OrientationLock.swift b/Mail/Proxy/Implementation/OrientationLock.swift similarity index 73% rename from Mail/Views/Proxy/OrientationLock.swift rename to Mail/Proxy/Implementation/OrientationLock.swift index c27cf8226..e4352b0bc 100644 --- a/Mail/Views/Proxy/OrientationLock.swift +++ b/Mail/Proxy/Implementation/OrientationLock.swift @@ -20,25 +20,15 @@ import Foundation import InfomaniakCoreUI import UIKit -/// Something that can handle rotation lock -public protocol OrientationManageable { - var orientationLock: UIInterfaceOrientationMask { get set } - - var interfaceOrientation: UIInterfaceOrientation? { get } -} - -public final class OrientationManagerStub: OrientationManageable { - - public var orientationLock = UIInterfaceOrientationMask.all - - public var interfaceOrientation: UIInterfaceOrientation? -} - @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/Views/Proxy/RemoteNotificationRegistrer.swift b/Mail/Proxy/Implementation/RemoteNotificationRegistrer.swift similarity index 80% rename from Mail/Views/Proxy/RemoteNotificationRegistrer.swift rename to Mail/Proxy/Implementation/RemoteNotificationRegistrer.swift index 10331e690..35a754849 100644 --- a/Mail/Views/Proxy/RemoteNotificationRegistrer.swift +++ b/Mail/Proxy/Implementation/RemoteNotificationRegistrer.swift @@ -19,15 +19,6 @@ import Foundation import UIKit -/// Something that can handle rotation lock -public protocol RemoteNotificationRegistrable { - func register() -} - -public final class RemoteNotificationRegistrerStub: RemoteNotificationRegistrable { - public func register() {} -} - @available(iOSApplicationExtension, unavailable) public final class RemoteNotificationRegistrer: RemoteNotificationRegistrable { public func register() { diff --git a/Mail/Views/Proxy/RootViewController.swift b/Mail/Proxy/Implementation/RootViewController.swift similarity index 66% rename from Mail/Views/Proxy/RootViewController.swift rename to Mail/Proxy/Implementation/RootViewController.swift index 4e82091fc..0ea945af1 100644 --- a/Mail/Views/Proxy/RootViewController.swift +++ b/Mail/Proxy/Implementation/RootViewController.swift @@ -20,20 +20,17 @@ import Foundation import InfomaniakCoreUI import UIKit -/// Something that can fetch the Root View Controller -public protocol RootViewControllerFetcheable { - var rootViewController: UIViewController? { get } -} - -public struct RootViewControllerFetcherStub: RootViewControllerFetcheable { +@available(iOSApplicationExtension, unavailable) +public struct RootViewManager: RootViewManageable { public var rootViewController: UIViewController? { - nil + self.mainSceneKeyWindow?.rootViewController + } + + public var mainSceneKeyWindow: UIWindow? { + UIApplication.shared.mainSceneKeyWindow } -} -@available(iOSApplicationExtension, unavailable) -public struct RootViewControllerFetcher: RootViewControllerFetcheable { - public var rootViewController: UIViewController? { - UIApplication.shared.mainSceneKeyWindow?.rootViewController + public func updateAllWindowUI() { + UIApplication.shared.connectedScenes.forEach { ($0.delegate as? SceneDelegate)?.updateWindowUI() } } } diff --git a/Mail/Views/Proxy/OpenURL.swift b/Mail/Proxy/Implementation/URLNavigator.swift similarity index 78% rename from Mail/Views/Proxy/OpenURL.swift rename to Mail/Proxy/Implementation/URLNavigator.swift index 601119c3f..1488b6186 100644 --- a/Mail/Views/Proxy/OpenURL.swift +++ b/Mail/Proxy/Implementation/URLNavigator.swift @@ -19,17 +19,6 @@ import Foundation import UIKit -/// Something that can open URL in an abstract way -public protocol URLNavigable { - func openUrl(_ url: URL) - func openUrlIfPossible(_ url: URL) -} - -public struct URLNavigatorStub: URLNavigable { - public func openUrl(_ url: URL) {} - public func openUrlIfPossible(_ url: URL) {} -} - @available(iOSApplicationExtension, unavailable) public struct URLNavigator: URLNavigable { public func openUrl(_ url: URL) { diff --git a/Mail/Views/Proxy/AccountSwitcher.swift b/Mail/Proxy/Protocols/CacheManageable.swift similarity index 86% rename from Mail/Views/Proxy/AccountSwitcher.swift rename to Mail/Proxy/Protocols/CacheManageable.swift index 1ac69b5ee..0941b482f 100644 --- a/Mail/Views/Proxy/AccountSwitcher.swift +++ b/Mail/Proxy/Protocols/CacheManageable.swift @@ -17,3 +17,8 @@ */ 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..ef2b359b9 --- /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..9ae29e73a --- /dev/null +++ b/Mail/Proxy/Protocols/RootViewManageable.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 fetch the Root View Controller +public protocol RootViewManageable { + /// The current rootViewController + var rootViewController: UIViewController? { get } + + /// The current mainSceneKeyWindow + var mainSceneKeyWindow: UIWindow? { get } + + /// Call updateWindowUI on all connected scenes + func updateAllWindowUI() +} diff --git a/Mail/Proxy/Protocols/URLNavigable.swift b/Mail/Proxy/Protocols/URLNavigable.swift new file mode 100644 index 000000000..49af98131 --- /dev/null +++ b/Mail/Proxy/Protocols/URLNavigable.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 open URL in an abstract way +public protocol URLNavigable { + /// Try to open an URL + func openUrl(_ url: URL) + + /// Check if app can open URL first, then try to open it + func openUrlIfPossible(_ url: URL) +} diff --git a/Mail/Views/Alerts/LogoutConfirmationView.swift b/Mail/Views/Alerts/LogoutConfirmationView.swift index d05b62460..819a1b564 100644 --- a/Mail/Views/Alerts/LogoutConfirmationView.swift +++ b/Mail/Views/Alerts/LogoutConfirmationView.swift @@ -27,6 +27,8 @@ import SwiftUI struct LogoutConfirmationView: View { @Environment(\.window) private var window + @LazyInjectService private var accountManager: AccountManager + let account: Account var body: some View { @@ -46,13 +48,13 @@ struct LogoutConfirmationView: View { @InjectService var notificationService: InfomaniakNotifications await notificationService.removeStoredTokenFor(userId: account.userId) } - AccountManager.instance.removeTokenAndAccount(token: account.token) - if let nextAccount = AccountManager.instance.accounts.first { + accountManager.removeTokenAndAccount(token: account.token) + if let nextAccount = accountManager.accounts.first { (window?.windowScene?.delegate as? SceneDelegate)?.switchAccount(nextAccount) } else { (window?.windowScene?.delegate as? SceneDelegate)?.showLoginView() } - AccountManager.instance.saveAccounts() + accountManager.saveAccounts() } } diff --git a/Mail/Views/Attachment/AttachmentPreview.swift b/Mail/Views/Attachment/AttachmentPreview.swift index e9ff8b6d0..43d32344a 100644 --- a/Mail/Views/Attachment/AttachmentPreview.swift +++ b/Mail/Views/Attachment/AttachmentPreview.swift @@ -30,7 +30,7 @@ struct AttachmentPreview: View { @Environment(\.verticalSizeClass) var sizeClass - @LazyInjectService var rootViewControllerFetcher: RootViewControllerFetcheable + @LazyInjectService var rootViewControllerFetcher: RootViewManageable var body: some View { NavigationView { diff --git a/Mail/Views/Bottom sheets/Actions/ActionsViewModel.swift b/Mail/Views/Bottom sheets/Actions/ActionsViewModel.swift index 92639bda0..f417a33d5 100644 --- a/Mail/Views/Bottom sheets/Actions/ActionsViewModel.swift +++ b/Mail/Views/Bottom sheets/Actions/ActionsViewModel.swift @@ -221,6 +221,7 @@ enum ActionsTarget: Equatable, Identifiable { @Published var listActions: [Action] = [] @LazyInjectService private var matomo: MatomoUtils + @LazyInjectService private var accountManager: AccountManager init(mailboxManager: MailboxManager, target: ActionsTarget, @@ -289,7 +290,7 @@ enum ActionsTarget: Equatable, Identifiable { let archive = message.folder?.role != .archive let unread = !message.seen let star = message.flagged - let isStaff = AccountManager.instance.currentAccount?.user?.isStaff ?? false + let isStaff = accountManager.currentAccount?.user?.isStaff ?? false let tempListActions: [Action?] = [ archive ? .archive : .moveToInbox, unread ? .markAsRead : .markAsUnread, diff --git a/Mail/Views/Bottom sheets/ContactActionsView.swift b/Mail/Views/Bottom sheets/ContactActionsView.swift index 837b537e4..bd7d14e4b 100644 --- a/Mail/Views/Bottom sheets/ContactActionsView.swift +++ b/Mail/Views/Bottom sheets/ContactActionsView.swift @@ -26,7 +26,9 @@ import SwiftUI struct ContactActionsView: View { @EnvironmentObject var mailboxManager: MailboxManager @Environment(\.dismiss) var dismiss + @LazyInjectService private var matomo: MatomoUtils + @LazyInjectService private var accountManager: AccountManager @State private var writtenToRecipient: Recipient? @@ -35,7 +37,8 @@ struct ContactActionsView: View { init(recipient: Recipient) { self.recipient = recipient - let isRemoteContact = AccountManager.instance.currentContactManager?.getContact(for: recipient)?.remote != nil + @InjectService var accountManager: AccountManager + let isRemoteContact = accountManager.currentContactManager?.getContact(for: recipient)?.remote != nil if isRemoteContact { actions = [.writeEmailAction, .copyEmailAction] } else { @@ -128,7 +131,7 @@ struct ContactActionsView: View { private func addToContacts() { Task { await tryOrDisplayError { - try await AccountManager.instance.currentContactManager?.addContact(recipient: recipient) + try await accountManager.currentContactManager?.addContact(recipient: recipient) IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.snackbarContactSaved) } } 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 bf03ff0e0..582f7d5e0 100644 --- a/Mail/Views/Menu Drawer/Items/MenuDrawerItemsListView.swift +++ b/Mail/Views/Menu Drawer/Items/MenuDrawerItemsListView.swift @@ -59,7 +59,8 @@ struct MenuDrawerItemsHelpListView: View { @State private var isShowingHelp = false @State private var isShowingBugTracker = false - @LazyInjectService var urlNavigator: URLNavigable + @LazyInjectService private var urlNavigator: URLNavigable + @LazyInjectService private var accountManager: AccountManager var body: some View { MenuDrawerItemsListView { @@ -84,7 +85,7 @@ struct MenuDrawerItemsHelpListView: View { } private func sendFeedback() { - if AccountManager.instance.currentAccount?.user?.isStaff == true { + if accountManager.currentAccount?.user?.isStaff == true { isShowingBugTracker.toggle() } else if let userReportURL = URL(string: MailResourcesStrings.Localizable.urlUserReportiOS) { urlNavigator.openUrl(userReportURL) diff --git a/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift b/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift index 793d3de0f..daa411510 100644 --- a/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift +++ b/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift @@ -44,10 +44,12 @@ struct MailboxCell: View { @Environment(\.mailboxCellStyle) private var style: Style @Environment(\.window) private var window + @LazyInjectService private var accountManager: AccountManager + let mailbox: Mailbox private var isSelected: Bool { - return AccountManager.instance.currentMailboxManager?.mailbox.objectId == mailbox.objectId + return accountManager.currentMailboxManager?.mailbox.objectId == mailbox.objectId } private var detailNumber: Int? { diff --git a/Mail/Views/Menu Drawer/MailboxManagement/MailboxesManagementView.swift b/Mail/Views/Menu Drawer/MailboxManagement/MailboxesManagementView.swift index 572192785..3ae30ff3f 100644 --- a/Mail/Views/Menu Drawer/MailboxManagement/MailboxesManagementView.swift +++ b/Mail/Views/Menu Drawer/MailboxManagement/MailboxesManagementView.swift @@ -28,10 +28,15 @@ struct MailboxesManagementView: View { @EnvironmentObject var mailboxManager: MailboxManager @EnvironmentObject var navigationDrawerState: NavigationDrawerState + @LazyInjectService private var accountManager: AccountManager + @ObservedResults( Mailbox.self, configuration: MailboxInfosManager.instance.realmConfiguration, - where: { $0.userId == AccountManager.instance.currentUserId }, + where: { + @InjectService var accountManager: AccountManager + return $0.userId == accountManager.currentUserId + }, sortDescriptor: SortDescriptor(keyPath: \Mailbox.mailboxId) ) private var mailboxes @@ -85,8 +90,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/AutocompletionView.swift b/Mail/Views/New Message/AutocompletionView.swift index 9d40090a9..c253fd4af 100644 --- a/Mail/Views/New Message/AutocompletionView.swift +++ b/Mail/Views/New Message/AutocompletionView.swift @@ -17,6 +17,7 @@ */ import Combine +import InfomaniakDI import MailCore import MailResources import RealmSwift @@ -30,6 +31,8 @@ struct AutocompletionView: View { @Binding var autocompletion: [Recipient] @Binding var addedRecipients: RealmSwift.List + @LazyInjectService private var accountManager: AccountManager + let addRecipient: @MainActor (Recipient) -> Void var body: some View { @@ -62,7 +65,7 @@ struct AutocompletionView: View { } private func updateAutocompletion(_ search: String) { - guard let contactManager = AccountManager.instance.currentContactManager else { + guard let contactManager = accountManager.currentContactManager else { withAnimation { autocompletion = [] } diff --git a/Mail/Views/Onboarding/OnboardingView.swift b/Mail/Views/Onboarding/OnboardingView.swift index 9072375e7..f81f2a71e 100644 --- a/Mail/Views/Onboarding/OnboardingView.swift +++ b/Mail/Views/Onboarding/OnboardingView.swift @@ -68,11 +68,12 @@ struct Slide: Identifiable { } @MainActor -class LoginHandler: InfomaniakLoginDelegate, ObservableObject { - @LazyInjectService var loginService: InfomaniakLoginable - @LazyInjectService var matomo: MatomoUtils - @LazyInjectService var remoteNotificationRegistrer: RemoteNotificationRegistrable - +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 + @Published var isLoading = false @Published var isPresentingErrorAlert = false var sceneDelegate: SceneDelegate? @@ -124,17 +125,17 @@ class LoginHandler: InfomaniakLoginDelegate, ObservableObject { private func loginSuccessful(code: String, codeVerifier verifier: String) { matomo.track(eventWithCategory: .account, name: "loggedIn") - let previousAccount = AccountManager.instance.currentAccount + let previousAccount = accountManager.currentAccount Task { do { - _ = try await AccountManager.instance.createAndSetCurrentAccount(code: code, codeVerifier: verifier) + _ = try await accountManager.createAndSetCurrentAccount(code: code, codeVerifier: verifier) sceneDelegate?.showMainView() remoteNotificationRegistrer.register() } catch let error as MailError where error == MailError.noMailbox { sceneDelegate?.showNoMailboxView() } catch { if let previousAccount = previousAccount { - AccountManager.instance.switchAccount(newAccount: previousAccount) + accountManager.switchAccount(newAccount: previousAccount) } IKSnackBar.showSnackBar(message: error.localizedDescription) } @@ -249,7 +250,7 @@ struct OnboardingView: View { if UIDevice.current.userInterfaceIdiom == .phone { UIDevice.current .setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation") - orientationManager.orientationLock = .portrait + orientationManager.setOrientationLock(.portrait) UIViewController.attemptRotationToDeviceOrientation() } } diff --git a/Mail/Views/Search/SearchViewModel.swift b/Mail/Views/Search/SearchViewModel.swift index c23f1de74..4504ed344 100644 --- a/Mail/Views/Search/SearchViewModel.swift +++ b/Mail/Views/Search/SearchViewModel.swift @@ -97,7 +97,8 @@ enum SearchState { @Published var isLoading = false @LazyInjectService var matomo: MatomoUtils - + @LazyInjectService private var accountManager: AccountManager + let searchFolder: Folder var resourceNext: String? var lastSearch = "" @@ -128,7 +129,7 @@ enum SearchState { } func updateContactSuggestion() { - let contactManager = AccountManager.instance.currentContactManager + let contactManager = accountManager.currentContactManager let autocompleteContacts = contactManager?.contacts(matching: searchValue) ?? [] var autocompleteRecipients = autocompleteContacts.map { Recipient(email: $0.email, name: $0.name) } // Append typed email diff --git a/Mail/Views/Settings/General/SettingsNotificationsView.swift b/Mail/Views/Settings/General/SettingsNotificationsView.swift index 6c01f1ff0..e042593c1 100644 --- a/Mail/Views/Settings/General/SettingsNotificationsView.swift +++ b/Mail/Views/Settings/General/SettingsNotificationsView.swift @@ -28,6 +28,7 @@ struct SettingsNotificationsView: View { @LazyInjectService private var notificationService: InfomaniakNotifications @LazyInjectService private var matomo: MatomoUtils @LazyInjectService private var urlNavigator: URLNavigable + @LazyInjectService private var accountManager: AccountManager @AppStorage(UserDefaults.shared.key(.notificationsEnabled)) private var notificationsEnabled = DefaultPreferences .notificationsEnabled @@ -75,7 +76,7 @@ struct SettingsNotificationsView: View { if subscribedTopics != nil && notificationsEnabled { IKDivider() - ForEach(AccountManager.instance.mailboxes) { mailbox in + ForEach(accountManager.mailboxes) { mailbox in Toggle(isOn: Binding(get: { notificationsEnabled && subscribedTopics?.contains(mailbox.notificationTopicName) == true }, set: { on in @@ -131,7 +132,7 @@ struct SettingsNotificationsView: View { } func currentTopics() async { - let currentSubscription = await notificationService.subscriptionForUser(id: AccountManager.instance.currentUserId) + let currentSubscription = await notificationService.subscriptionForUser(id: accountManager.currentUserId) withAnimation { self.subscribedTopics = currentSubscription?.topics } @@ -139,7 +140,7 @@ struct SettingsNotificationsView: View { func updateTopicsForCurrentUserIfNeeded() { Task { - guard let currentApiFetcher = AccountManager.instance.currentMailboxManager?.apiFetcher, + guard let currentApiFetcher = accountManager.currentMailboxManager?.apiFetcher, let subscribedTopics else { return } await notificationService.updateTopicsIfNeeded(subscribedTopics, userApiFetcher: currentApiFetcher) } diff --git a/Mail/Views/Settings/General/SettingsView.swift b/Mail/Views/Settings/General/SettingsView.swift index 154f7b186..493dc268c 100644 --- a/Mail/Views/Settings/General/SettingsView.swift +++ b/Mail/Views/Settings/General/SettingsView.swift @@ -18,11 +18,14 @@ import InfomaniakCore import InfomaniakCoreUI +import InfomaniakDI import MailCore import MailResources import SwiftUI struct SettingsView: View { + @LazyInjectService private var accountManager: AccountManager + @AppStorage(UserDefaults.shared.key(.threadDensity)) private var density = DefaultPreferences.threadDensity @AppStorage(UserDefaults.shared.key(.theme)) private var theme = DefaultPreferences.theme @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor @@ -34,8 +37,8 @@ struct SettingsView: View { Text(MailResourcesStrings.Localizable.settingsSectionEmailAddresses) .textStyle(.bodySmallSecondary) - ForEach(AccountManager.instance.mailboxes) { mailbox in - if let mailboxManager = AccountManager.instance.getMailboxManager(for: mailbox) { + ForEach(accountManager.mailboxes) { mailbox in + 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 18ec8d371..2dfb73775 100644 --- a/Mail/Views/Settings/SettingsOptionView.swift +++ b/Mail/Views/Settings/SettingsOptionView.swift @@ -34,23 +34,23 @@ struct SettingsOptionView: View where OptionEnum: CaseIterable, Opti private let matomoValue: Float? private let matomoName: KeyPath? + @LazyInjectService private var rootViewManager: RootViewManageable + @LazyInjectService private var matomo: MatomoUtils + @State private var values: [OptionEnum] @State private var selectedValue: OptionEnum { didSet { UserDefaults.shared[keyPath: keyPath] = selectedValue switch keyPath { case \.theme, \.accentColor: - break - // FIXME -// UIApplication.shared.connectedScenes.forEach { ($0.delegate as? SceneDelegate)?.updateWindowUI() } + rootViewManager.updateAllWindowUI() + default: break } } } - @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 f751a3ce2..eb8207116 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -122,7 +122,7 @@ struct SplitView: View { self.mailToURLComponents = identifiableURLComponents.object as? IdentifiableURLComponents } .onAppear { - orientationManager.orientationLock = .all + orientationManager.setOrientationLock(.all) } .task { await fetchSignatures() diff --git a/Mail/Views/Switch User/AccountListView.swift b/Mail/Views/Switch User/AccountListView.swift index bf0188d13..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 } } } @@ -62,6 +67,7 @@ struct AccountListView: View { @LazyInjectService private var matomo: MatomoUtils @LazyInjectService private var orientationManager: OrientationManageable + @LazyInjectService private var accountManager: AccountManager var body: some View { ScrollView { @@ -80,7 +86,7 @@ struct AccountListView: View { isShowingNewAccountView = true } .fullScreenCover(isPresented: $isShowingNewAccountView, onDismiss: { - orientationManager.orientationLock = .all + orientationManager.setOrientationLock(.all) }, content: { OnboardingView(page: 4, isScrollEnabled: false) }) @@ -92,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 3f65e0dcc..c3337bbff 100644 --- a/Mail/Views/Switch User/AccountView.swift +++ b/Mail/Views/Switch User/AccountView.swift @@ -25,22 +25,26 @@ import MailResources import Sentry import SwiftUI -@available(iOSApplicationExtension, unavailable) -class AccountViewDelegate: DeleteAccountDelegate { +final class AccountViewDelegate: DeleteAccountDelegate { + @LazyInjectService private var rootViewManager: RootViewManageable + @LazyInjectService private var accountManager: AccountManager + @MainActor func didCompleteDeleteAccount() { - guard let account = AccountManager.instance.currentAccount else { return } + guard let account = accountManager.currentAccount else { + return + } - AccountManager.instance.removeTokenAndAccount(token: account.token) + accountManager.removeTokenAndAccount(token: account.token) - let window = UIApplication.shared.mainSceneKeyWindow - if let nextAccount = AccountManager.instance.accounts.first { + let window = rootViewManager.mainSceneKeyWindow + if let nextAccount = accountManager.accounts.first { (window?.windowScene?.delegate as? SceneDelegate)?.switchAccount(nextAccount) IKSnackBar.showSnackBar(message: "Account deleted") } else { (window?.windowScene?.delegate as? SceneDelegate)?.showLoginView() } - AccountManager.instance.saveAccounts() + accountManager.saveAccounts() } @MainActor func didFailDeleteAccount(error: InfomaniakLoginError) { @@ -49,7 +53,6 @@ class AccountViewDelegate: DeleteAccountDelegate { } } -@available(iOSApplicationExtension, unavailable) struct AccountView: View { @Environment(\.dismiss) private var dismiss @Environment(\.window) private var window @@ -57,15 +60,24 @@ struct AccountView: View { @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor @LazyInjectService private var matomo: MatomoUtils + @LazyInjectService private var accountManager: AccountManager + + private let account: Account = { + let accountManager = LazyInjectService().wrappedValue + return accountManager.currentAccount + }() - private let account = AccountManager.instance.currentAccount! @State private var isShowingLogoutAlert = false @State private var isShowingDeleteAccount = false @State private var delegate = AccountViewDelegate() @State var mailboxes: [Mailbox] - let selectedMailbox = AccountManager.instance.currentMailboxManager?.mailbox + let selectedMailbox: Mailbox? = { + let accountManager = LazyInjectService().wrappedValue + return accountManager.currentMailboxManager?.mailbox + }() + var otherMailbox: [Mailbox] { return mailboxes.filter { $0.mailboxId != selectedMailbox?.mailboxId } } diff --git a/Mail/Views/Switch User/AddMailboxView.swift b/Mail/Views/Switch User/AddMailboxView.swift index dcdf65048..b647ddc2d 100644 --- a/Mail/Views/Switch User/AddMailboxView.swift +++ b/Mail/Views/Switch User/AddMailboxView.swift @@ -25,6 +25,8 @@ import SwiftUI struct AddMailboxView: View { @Environment(\.dismiss) var dismiss + @LazyInjectService private var accountManager: AccountManager + var completion: (Mailbox?) -> Void @State private var newAddress = "" @@ -92,7 +94,7 @@ struct AddMailboxView: View { private func addMailbox() { Task { do { - try await AccountManager.instance.addMailbox(mail: newAddress, password: password) { mailbox in + try await accountManager.addMailbox(mail: newAddress, password: password) { mailbox in @InjectService var matomo: MatomoUtils matomo.track(eventWithCategory: .account, name: "addMailboxConfirm") completion(mailbox) diff --git a/Mail/Views/Thread List/ThreadListModifiers.swift b/Mail/Views/Thread List/ThreadListModifiers.swift index 3e9e8b1e2..0674d2474 100644 --- a/Mail/Views/Thread List/ThreadListModifiers.swift +++ b/Mail/Views/Thread List/ThreadListModifiers.swift @@ -53,6 +53,7 @@ struct ThreadListCellAppearance: ViewModifier { struct ThreadListToolbar: ViewModifier { @LazyInjectService private var matomo: MatomoUtils + @LazyInjectService private var accountManager: AccountManager @Environment(\.isCompactWindow) var isCompactWindow @@ -123,7 +124,7 @@ struct ThreadListToolbar: ViewModifier { Button { isShowingSwitchAccount.toggle() } label: { - AvatarView(avatarDisplayable: AccountManager.instance.currentAccount.user) + AvatarView(avatarDisplayable: accountManager.currentAccount.user) } .accessibilityLabel(MailResourcesStrings.Localizable.contentDescriptionUserAvatar) } @@ -167,8 +168,7 @@ struct ThreadListToolbar: ViewModifier { ) .navigationBarTitleDisplayMode(.inline) .sheet(isPresented: $isShowingSwitchAccount) { - // FIXME -// AccountView(mailboxes: AccountManager.instance.mailboxes) + AccountView(mailboxes: accountManager.mailboxes) } } } diff --git a/MailCore/API/MailApiFetcher.swift b/MailCore/API/MailApiFetcher.swift index 1b004afc1..83d70a259 100644 --- a/MailCore/API/MailApiFetcher.swift +++ b/MailCore/API/MailApiFetcher.swift @@ -362,13 +362,16 @@ public class MailApiFetcher: ApiFetcher { } } -class SyncedAuthenticator: OAuthAuthenticator { +final class SyncedAuthenticator: OAuthAuthenticator { + + @LazyInjectService private var accountManager: AccountManager + override func refresh( _ credential: OAuthAuthenticator.Credential, for session: Session, completion: @escaping (Result) -> Void ) { - AccountManager.instance.refreshTokenLockedQueue.async { + accountManager.refreshTokenLockedQueue.async { @InjectService var keychainHelper: KeychainHelper @InjectService var networkLoginService: InfomaniakNetworkLoginable @@ -383,8 +386,8 @@ class SyncedAuthenticator: OAuthAuthenticator { } // Maybe someone else refreshed our token - AccountManager.instance.reloadTokensAndAccounts() - if let token = AccountManager.instance.getTokenForUserId(credential.userId), + self.accountManager.reloadTokensAndAccounts() + if let token = self.accountManager.getTokenForUserId(credential.userId), token.expirationDate > credential.expirationDate { SentrySDK .addBreadcrumb(token.generateBreadcrumb(level: .info, message: "Refreshing token - Success with local")) diff --git a/MailCore/Cache/AccountManager.swift b/MailCore/Cache/AccountManager.swift index 2a62a3116..e83f5271f 100644 --- a/MailCore/Cache/AccountManager.swift +++ b/MailCore/Cache/AccountManager.swift @@ -67,7 +67,7 @@ public extension InfomaniakNetworkLoginable { } } -public class AccountManager: RefreshTokenDelegate { +public final class AccountManager: RefreshTokenDelegate { @LazyInjectService var networkLoginService: InfomaniakNetworkLoginable @LazyInjectService var keychainHelper: KeychainHelper @LazyInjectService var bugTracker: BugTracker @@ -78,7 +78,6 @@ public class AccountManager: RefreshTokenDelegate { private static let group = "com.infomaniak.mail" public static let appGroup = "group." + group public static let accessGroup: String = AccountManager.appIdentifierPrefix + AccountManager.group - public static var instance = AccountManager() private let tag = "ch.infomaniak.token".data(using: .utf8)! public var currentAccount: Account! public var accounts = [Account]() @@ -132,7 +131,7 @@ public class AccountManager: RefreshTokenDelegate { private var contactManagers = [String: ContactManager]() private var apiFetchers = [Int: MailApiFetcher]() - private init() { + public init() { currentMailboxId = UserDefaults.shared.currentMailboxId currentUserId = UserDefaults.shared.currentMailUserId diff --git a/MailCore/Models/MergedContact.swift b/MailCore/Models/MergedContact.swift index 0bd28ff40..6c6af743f 100644 --- a/MailCore/Models/MergedContact.swift +++ b/MailCore/Models/MergedContact.swift @@ -19,6 +19,7 @@ import Contacts import Foundation import InfomaniakCore +import InfomaniakDI import Nuke import RealmSwift import SwiftUI @@ -35,13 +36,15 @@ extension CNContact { } } -public class MergedContact { +public final class MergedContact { public var email: String public var remote: Contact? public var local: CNContact? private let contactFormatter = CNContactFormatter() + @LazyInjectService private var accountManager: AccountManager + public lazy var color: UIColor = { if let remoteColorHex = remote?.color, let colorFromHex = UIColor(hex: remoteColorHex) { @@ -74,6 +77,7 @@ public class MergedContact { } extension MergedContact: AvatarDisplayable { + public var avatarImageRequest: ImageRequest? { if let localContact = local, localContact.imageDataAvailable { var imageRequest = ImageRequest(id: localContact.identifier) { @@ -89,7 +93,7 @@ extension MergedContact: AvatarDisplayable { if let remoteAvatar = remote?.avatar { let avatarURL = Endpoint.resource(remoteAvatar).url - return AccountManager.instance.currentMailboxManager?.apiFetcher.authenticatedImageRequest(avatarURL) + return accountManager.currentMailboxManager?.apiFetcher.authenticatedImageRequest(avatarURL) } return nil diff --git a/MailCore/Models/Recipient.swift b/MailCore/Models/Recipient.swift index fd1bc0f57..5bbed58bb 100644 --- a/MailCore/Models/Recipient.swift +++ b/MailCore/Models/Recipient.swift @@ -17,6 +17,7 @@ */ import Foundation +import InfomaniakDI import MailResources import Nuke import RealmSwift @@ -36,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 @@ -73,11 +74,13 @@ public class Recipient: EmbeddedObject, Codable { } public var isCurrentUser: Bool { - return AccountManager.instance.currentAccount?.user.email == email + @InjectService var accountManager: AccountManager + return accountManager.currentAccount?.user.email == email } public var isMe: Bool { - return AccountManager.instance.currentMailboxManager?.mailbox.email == email + @InjectService var accountManager: AccountManager + return accountManager.currentMailboxManager?.mailbox.email == email } public lazy var nameComponents: (givenName: String, familyName: String?) = { @@ -115,7 +118,10 @@ public class Recipient: EmbeddedObject, Codable { return initials.joined().uppercased() }() - public lazy var contact: MergedContact? = AccountManager.instance.currentContactManager?.getContact(for: self) + public lazy var contact: MergedContact? = { + @InjectService var accountManager: AccountManager + return accountManager.currentContactManager?.getContact(for: self) + }() public var htmlDescription: String { let emailString = "<\(email)>" @@ -134,7 +140,8 @@ public class Recipient: EmbeddedObject, Codable { extension Recipient: AvatarDisplayable { public var avatarImageRequest: ImageRequest? { guard !(isCurrentUser && isMe) else { - return AccountManager.instance.currentAccount.user.avatarImageRequest + @InjectService var accountManager: AccountManager + return accountManager.currentAccount.user.avatarImageRequest } return contact?.avatarImageRequest } diff --git a/MailCore/Utils/NotificationsHelper.swift b/MailCore/Utils/NotificationsHelper.swift index 588de0c42..2a484d116 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 } } diff --git a/MailCore/Utils/URLSchemeHandler.swift b/MailCore/Utils/URLSchemeHandler.swift index f2d7bb968..248365944 100644 --- a/MailCore/Utils/URLSchemeHandler.swift +++ b/MailCore/Utils/URLSchemeHandler.swift @@ -17,15 +17,18 @@ */ 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) @@ -36,7 +39,7 @@ public class URLSchemeHandler: NSObject, WKURLSchemeHandler { components?.scheme = "https" var request = URLRequest(url: components!.url!) request.addValue( - "Bearer \(AccountManager.instance.currentAccount.token.accessToken)", + "Bearer \(accountManager.currentAccount.token.accessToken)", forHTTPHeaderField: "Authorization" ) let dataTask = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in diff --git a/MailNotificationServiceExtension/NotificationService.swift b/MailNotificationServiceExtension/NotificationService.swift index b80bed709..86fb2fb89 100644 --- a/MailNotificationServiceExtension/NotificationService.swift +++ b/MailNotificationServiceExtension/NotificationService.swift @@ -25,7 +25,9 @@ import MailResources import RealmSwift import UserNotifications -class NotificationService: UNNotificationServiceExtension { +final class NotificationService: UNNotificationServiceExtension { + @LazyInjectService private var accountManager: AccountManager + var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? @@ -91,7 +93,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/MailShareExtension/Proxy/CacheManager.swift b/MailShareExtension/Proxy/CacheManager.swift new file mode 100644 index 000000000..cfdcf9aa0 --- /dev/null +++ b/MailShareExtension/Proxy/CacheManager.swift @@ -0,0 +1,25 @@ +/* + 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() {} +} diff --git a/MailShareExtension/Proxy/OrientationManager.swift b/MailShareExtension/Proxy/OrientationManager.swift new file mode 100644 index 000000000..09fae4876 --- /dev/null +++ b/MailShareExtension/Proxy/OrientationManager.swift @@ -0,0 +1,31 @@ +/* + 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) { } + + public var interfaceOrientation: UIInterfaceOrientation? +} diff --git a/MailShareExtension/Proxy/RemoteNotificationRegistrer.swift b/MailShareExtension/Proxy/RemoteNotificationRegistrer.swift new file mode 100644 index 000000000..5213e2788 --- /dev/null +++ b/MailShareExtension/Proxy/RemoteNotificationRegistrer.swift @@ -0,0 +1,25 @@ +/* + 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() {} +} diff --git a/MailShareExtension/Proxy/RootViewManager.swift b/MailShareExtension/Proxy/RootViewManager.swift new file mode 100644 index 000000000..88e2656c8 --- /dev/null +++ b/MailShareExtension/Proxy/RootViewManager.swift @@ -0,0 +1,34 @@ +/* + 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 + +/// A RootViewManager that works in Extension mode +public struct RootViewManager: RootViewManageable { + public var rootViewController: UIViewController? { + nil + } + + public var mainSceneKeyWindow: UIWindow? { + nil + } + + public func updateAllWindowUI() {} +} diff --git a/MailShareExtension/Proxy/URLNavigator.swift b/MailShareExtension/Proxy/URLNavigator.swift new file mode 100644 index 000000000..1c15618a1 --- /dev/null +++ b/MailShareExtension/Proxy/URLNavigator.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 Foundation +import UIKit + +/// An URLNavigator that works in Extension mode +public struct URLNavigator: URLNavigable { + public func openUrl(_ url: URL) {} + public func openUrlIfPossible(_ url: URL) {} +} diff --git a/Project.swift b/Project.swift index 580b19ba3..ceac965bb 100644 --- a/Project.swift +++ b/Project.swift @@ -108,13 +108,12 @@ let project = Project(name: "Mail", deploymentTarget: Constants.deploymentTarget, infoPlist: .file(path: "MailShareExtension/Info.plist"), sources: ["MailShareExtension/**", - "Mail/**", - "Mail/Views/New Message/**", "Mail/Views/**", "Mail/Components/**", "Mail/Helpers/**", "Mail/Utils/**", - "Mail/Views/**"], + "Mail/Views/**", + "Mail/Proxy/Protocols/**"], resources:[ "MailShareExtension/Base.lproj/MainInterface.storyboard", "MailShareExtension/ShareExtension.entitlements", From 695e842e2c1407a61ea1d10bf73fe359aad8c658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 29 Jun 2023 15:17:15 +0200 Subject: [PATCH 05/61] feat: extract and factorise DI loading code feat: Present ComposeMessageView when sharing to ikMail --- Mail/AppDelegate.swift | 73 +---------- Mail/Helpers/AppAssembly.swift | 113 ++++++++++++++++++ .../New Message/ComposeMessageView.swift | 2 - MailShareExtension/ShareViewController.swift | 44 +++++-- 4 files changed, 150 insertions(+), 82 deletions(-) create mode 100644 Mail/Helpers/AppAssembly.swift diff --git a/Mail/AppDelegate.swift b/Mail/AppDelegate.swift index 8b2ebd8a7..1a16529cf 100644 --- a/Mail/AppDelegate.swift +++ b/Mail/AppDelegate.swift @@ -35,10 +35,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate { @LazyInjectService private var orientationManager: OrientationManageable @LazyInjectService private var accountManager: AccountManager + /// 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 { Logging.initLogging() - setupDI() + DDLogInfo("Application starting in foreground ? \(UIApplication.shared.applicationState != .background)") ApiFetcher.decoder.dateDecodingStrategy = .iso8601 @@ -112,72 +115,4 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } } - - 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 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 accountManager = Factory(type: AccountManager.self) { _, _ in - AccountManager() - } - - SimpleResolver.sharedResolver.store(factory: networkLoginService) - SimpleResolver.sharedResolver.store(factory: loginService) - SimpleResolver.sharedResolver.store(factory: notificationService) - SimpleResolver.sharedResolver.store(factory: keychainHelper) - 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: accountManager) - - setupProxyInDI() - } - - private func setupProxyInDI() { - let factories = [ - Factory(type: CacheManageable.self) { _, _ in - CacheManager() - }, - Factory(type: OrientationManageable.self) { _, _ in - OrientationManager() - }, - Factory(type: RemoteNotificationRegistrable.self) { _, _ in - RemoteNotificationRegistrer() - }, - Factory(type: RootViewManageable.self) { _, _ in - RootViewManager() - }, - Factory(type: URLNavigable.self) { _, _ in - URLNavigator() - } - ] - - factories.forEach { SimpleResolver.sharedResolver.store(factory: $0) } - } } diff --git a/Mail/Helpers/AppAssembly.swift b/Mail/Helpers/AppAssembly.swift new file mode 100644 index 000000000..a803549d6 --- /dev/null +++ b/Mail/Helpers/AppAssembly.swift @@ -0,0 +1,113 @@ +/* + 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 +import os.log + +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: SnackBarAvoider.self) { _, _ in + SnackBarAvoider() + }, + Factory(type: DraftManager.self) { _, _ in + DraftManager() + }, + Factory(type: AccountManager.self) { _, _ in + AccountManager() + } + ] + + 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() + }, + Factory(type: RootViewManageable.self) { _, _ in + RootViewManager() + }, + Factory(type: URLNavigable.self) { _, _ in + URLNavigator() + } + ] + + factories.registerFactoriesInDI() + } +} + +/// Something that loads the DI on init +public struct EarlyDIHook { + public init() { + // setup DI ASAP + os_log("EarlyDIHook") + ApplicationAssembly.setupDI() + } +} + diff --git a/Mail/Views/New Message/ComposeMessageView.swift b/Mail/Views/New Message/ComposeMessageView.swift index 666abcc15..5feaf145f 100644 --- a/Mail/Views/New Message/ComposeMessageView.swift +++ b/Mail/Views/New Message/ComposeMessageView.swift @@ -100,7 +100,6 @@ struct ComposeMessageView: View { Self.saveNewDraftInRealm(mailboxManager.getRealm(), draft: draft) _draft = StateRealmObject(wrappedValue: draft) - _isLoadingContent = State(wrappedValue: (draft.messageUid != nil && draft.remoteUUID.isEmpty) || messageReply != nil) _signatureManager = StateObject(wrappedValue: SignaturesManager(mailboxManager: mailboxManager)) @@ -138,7 +137,6 @@ struct ComposeMessageView: View { ScrollView { VStack(spacing: 0) { ComposeMessageHeaderView(draft: draft, focusedField: _focusedField, autocompletionType: $autocompletionType) - if autocompletionType == nil { ComposeMessageBodyView( draft: draft, diff --git a/MailShareExtension/ShareViewController.swift b/MailShareExtension/ShareViewController.swift index 1c32086cf..004a08974 100644 --- a/MailShareExtension/ShareViewController.swift +++ b/MailShareExtension/ShareViewController.swift @@ -17,11 +17,16 @@ */ import InfomaniakCoreUI +import InfomaniakDI +import MailCore import Social import SwiftUI import UIKit -class ShareNavigationViewController: TitleSizeAdjustingNavigationController { +final class ShareNavigationViewController: UIViewController { + /// Making sure the DI is registered at a very early stage of the app launch. + private let dependencyInjectionHook = EarlyDIHook() + override public func viewDidLoad() { super.viewDidLoad() @@ -32,13 +37,21 @@ class ShareNavigationViewController: TitleSizeAdjustingNavigationController { dismiss(animated: true) return } - - // To my knowledge, we need to go threw wrapping to use SwiftUI here. - let childView = UIHostingController(rootView: SwiftUIView()) - addChild(childView) - childView.view.frame = self.view.bounds - self.view.addSubview(childView.view) - childView.didMove(toParent: self) + + // We need to go threw wrapping to use SwiftUI in an NSExtension. + let hostingController = UIHostingController(rootView: ComposeMessageWrapperView()) + hostingController.view.backgroundColor = .clear + 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) { @@ -46,9 +59,18 @@ class ShareNavigationViewController: TitleSizeAdjustingNavigationController { } } -struct SwiftUIView: View { +struct ComposeMessageWrapperView: View { + @State private var draft = Draft() + @LazyInjectService private var accountManager: AccountManager + var body: some View { - Text("test") - .background(.red) + if let mailboxManager = accountManager.currentMailboxManager { + ComposeMessageView.newMessage(draft, mailboxManager: mailboxManager) + .environmentObject(mailboxManager) + .navigationTitle(Text("Test")) + } else { + Text("Please login in ikMail") + .background(.red) + } } } From 76805e53268c5bf73b48654e9f304db0d1cb80b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 30 Jun 2023 16:12:05 +0200 Subject: [PATCH 06/61] feat: share extension dismiss() working with a NSNotification feat: share extension working with images from photo library --- Mail/Utils/Notification+Name.swift | 1 + .../New Message/ComposeMessageBodyView.swift | 11 +++-- .../New Message/ComposeMessageView+Init.swift | 4 +- .../New Message/ComposeMessageView.swift | 19 +++++++-- .../ComposeMessageWrapperView.swift | 42 +++++++++++++++++++ MailShareExtension/ShareViewController.swift | 38 +++++++++-------- 6 files changed, 88 insertions(+), 27 deletions(-) create mode 100644 MailShareExtension/ComposeMessageWrapperView.swift diff --git a/Mail/Utils/Notification+Name.swift b/Mail/Utils/Notification+Name.swift index 1ee45690f..60f721ab5 100644 --- a/Mail/Utils/Notification+Name.swift +++ b/Mail/Utils/Notification+Name.swift @@ -21,5 +21,6 @@ import Foundation public extension Notification.Name { static let onUserTappedNotification = Notification.Name("userTappedNotification") static let dismissMoveSheetNotificationName = Notification.Name(rawValue: "sheetViewDismiss") + static let dismissDraftView = Notification.Name(rawValue: "draftDismiss") static let onOpenedMailTo = Notification.Name("onOpenedMailTo") } diff --git a/Mail/Views/New Message/ComposeMessageBodyView.swift b/Mail/Views/New Message/ComposeMessageBodyView.swift index 2f50aa82d..197400a57 100644 --- a/Mail/Views/New Message/ComposeMessageBodyView.swift +++ b/Mail/Views/New Message/ComposeMessageBodyView.swift @@ -79,7 +79,7 @@ struct ComposeMessageBodyView: View { case .error: // Unable to get signatures, "An error occurred" and close modal. IKSnackBar.showSnackBar(message: MailError.unknownError.localizedDescription) - dismiss() + dismissSheet() case .progress: break } @@ -114,7 +114,7 @@ struct ComposeMessageBodyView: View { } isLoadingContent = false } catch { - dismiss() + dismissSheet() IKSnackBar.showSnackBar(message: MailError.unknownError.localizedDescription) } } @@ -132,7 +132,7 @@ struct ComposeMessageBodyView: View { isLoadingContent = false } catch { - dismiss() + dismissSheet() IKSnackBar.showSnackBar(message: MailError.unknownError.localizedDescription) } } @@ -176,6 +176,11 @@ struct ComposeMessageBodyView: View { } attachmentsManager.completeUploadedAttachments() } + + private func dismissSheet() { + NotificationCenter.default.post(Notification(name: .dismissDraftView)) + dismiss() + } } struct ComposeMessageBodyView_Previews: PreviewProvider { diff --git a/Mail/Views/New Message/ComposeMessageView+Init.swift b/Mail/Views/New Message/ComposeMessageView+Init.swift index 012c74eb7..579433e73 100644 --- a/Mail/Views/New Message/ComposeMessageView+Init.swift +++ b/Mail/Views/New Message/ComposeMessageView+Init.swift @@ -23,8 +23,8 @@ 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 5feaf145f..fc513ce24 100644 --- a/Mail/Views/New Message/ComposeMessageView.swift +++ b/Mail/Views/New Message/ComposeMessageView.swift @@ -67,6 +67,7 @@ struct ComposeMessageView: View { @State private var isShowingCancelAttachmentsError = false @State private var autocompletionType: ComposeViewFieldType? @State private var editorFocus = false + @State private var initialAttachments: [Attachable] = [] /// Something to track the initial loading of a default signature @StateObject private var signatureManager: SignaturesManager @@ -95,7 +96,7 @@ struct ComposeMessageView: View { // MAK: - Int - 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) @@ -105,6 +106,7 @@ struct ComposeMessageView: View { _signatureManager = StateObject(wrappedValue: SignaturesManager(mailboxManager: mailboxManager)) _mailboxManager = StateObject(wrappedValue: mailboxManager) _attachmentsManager = StateObject(wrappedValue: AttachmentsManager(draft: draft, mailboxManager: mailboxManager)) + _initialAttachments = State(wrappedValue: attachments) } // MAK: - View @@ -113,6 +115,10 @@ struct ComposeMessageView: View { NavigationView { composeMessage } + .onAppear() { + attachmentsManager.importAttachments(attachments: initialAttachments) + initialAttachments = [] + } .interactiveDismissDisabled() .customAlert(isPresented: $alert.isShowing) { switch alert.state { @@ -126,7 +132,7 @@ struct ComposeMessageView: View { } .customAlert(isPresented: $isShowingCancelAttachmentsError) { AttachmentsUploadInProgressErrorView { - dismiss() + dismissSheet() } } .matomoView(view: ["ComposeMessage"]) @@ -203,7 +209,7 @@ struct ComposeMessageView: View { isShowingCancelAttachmentsError = true return } - dismiss() + dismissSheet() } private func didTouchSend() { @@ -222,7 +228,7 @@ struct ComposeMessageView: View { liveDraft.action = .send } } - dismiss() + dismissSheet() } private static func saveNewDraftInRealm(_ realm: Realm, draft: Draft) { @@ -233,6 +239,11 @@ struct ComposeMessageView: View { realm.add(draft, update: .modified) } } + + private func dismissSheet() { + NotificationCenter.default.post(Notification(name: .dismissDraftView)) + dismiss() + } } struct ComposeMessageView_Previews: PreviewProvider { diff --git a/MailShareExtension/ComposeMessageWrapperView.swift b/MailShareExtension/ComposeMessageWrapperView.swift new file mode 100644 index 000000000..78dac1e40 --- /dev/null +++ b/MailShareExtension/ComposeMessageWrapperView.swift @@ -0,0 +1,42 @@ +// +/* + 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 + +struct ComposeMessageWrapperView: View { + @State var itemProviders: [NSItemProvider] + @State private var draft = Draft() + @LazyInjectService private var accountManager: AccountManager + + var body: some View { + if let mailboxManager = accountManager.currentMailboxManager { + ComposeMessageView.newMessage(draft, mailboxManager: mailboxManager, itemProviders: itemProviders) + .environmentObject(mailboxManager) + .navigationTitle(Text("Test")) + } else { + Text("Please login in ikMail") + .background(.red) + } + } +} diff --git a/MailShareExtension/ShareViewController.swift b/MailShareExtension/ShareViewController.swift index 004a08974..ae57c7c9c 100644 --- a/MailShareExtension/ShareViewController.swift +++ b/MailShareExtension/ShareViewController.swift @@ -33,13 +33,26 @@ final class ShareNavigationViewController: UIViewController { // Modify sheet size on iPadOS, property is ignored on iOS preferredContentSize = CGSize(width: 540, height: 620) - guard let attachments = (extensionContext?.inputItems.first as? NSExtensionItem)?.attachments else { + // 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 + } + + // Listen to dismiss notification + NotificationCenter.default.addObserver(self, selector: #selector(willDismissDraft(notification:)), + name: .dismissDraftView, + object: nil) + // We need to go threw wrapping to use SwiftUI in an NSExtension. - let hostingController = UIHostingController(rootView: ComposeMessageWrapperView()) + let hostingController = UIHostingController(rootView: ComposeMessageWrapperView(itemProviders: itemProviders)) hostingController.view.backgroundColor = .clear hostingController.view.translatesAutoresizingMaskIntoConstraints = false addChild(hostingController) @@ -54,23 +67,12 @@ final class ShareNavigationViewController: UIViewController { ]) } - override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { - extensionContext!.completeRequest(returningItems: nil, completionHandler: nil) + @objc private func willDismissDraft(notification: NSNotification) { + print("willDismissDraft :\(notification)") + dismiss(animated: true) } -} - -struct ComposeMessageWrapperView: View { - @State private var draft = Draft() - @LazyInjectService private var accountManager: AccountManager - var body: some View { - if let mailboxManager = accountManager.currentMailboxManager { - ComposeMessageView.newMessage(draft, mailboxManager: mailboxManager) - .environmentObject(mailboxManager) - .navigationTitle(Text("Test")) - } else { - Text("Please login in ikMail") - .background(.red) - } + override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + extensionContext!.completeRequest(returningItems: nil, completionHandler: nil) } } From 2824782b972eea8447b1a422d918a3244e13047e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 30 Jun 2023 16:48:40 +0200 Subject: [PATCH 07/61] feat: reworked dismiss to work with environment --- Mail/Utils/Notification+Name.swift | 1 - .../Views/New Message/ComposeMessageBodyView.swift | 13 +++++-------- Mail/Views/New Message/ComposeMessageView.swift | 14 +++++--------- MailShareExtension/ComposeMessageWrapperView.swift | 6 ++++-- MailShareExtension/ShareViewController.swift | 14 +++----------- 5 files changed, 17 insertions(+), 31 deletions(-) diff --git a/Mail/Utils/Notification+Name.swift b/Mail/Utils/Notification+Name.swift index 60f721ab5..1ee45690f 100644 --- a/Mail/Utils/Notification+Name.swift +++ b/Mail/Utils/Notification+Name.swift @@ -21,6 +21,5 @@ import Foundation public extension Notification.Name { static let onUserTappedNotification = Notification.Name("userTappedNotification") static let dismissMoveSheetNotificationName = Notification.Name(rawValue: "sheetViewDismiss") - static let dismissDraftView = Notification.Name(rawValue: "draftDismiss") static let onOpenedMailTo = Notification.Name("onOpenedMailTo") } diff --git a/Mail/Views/New Message/ComposeMessageBodyView.swift b/Mail/Views/New Message/ComposeMessageBodyView.swift index 197400a57..b0c69a84c 100644 --- a/Mail/Views/New Message/ComposeMessageBodyView.swift +++ b/Mail/Views/New Message/ComposeMessageBodyView.swift @@ -22,6 +22,8 @@ import RealmSwift import SwiftUI struct ComposeMessageBodyView: View { + @Environment(\.dismissModal) var dismissModal + @EnvironmentObject private var mailboxManager: MailboxManager /// Something to track the initial loading of a default signature @@ -79,7 +81,7 @@ struct ComposeMessageBodyView: View { case .error: // Unable to get signatures, "An error occurred" and close modal. IKSnackBar.showSnackBar(message: MailError.unknownError.localizedDescription) - dismissSheet() + dismissModal() case .progress: break } @@ -114,7 +116,7 @@ struct ComposeMessageBodyView: View { } isLoadingContent = false } catch { - dismissSheet() + dismissModal() IKSnackBar.showSnackBar(message: MailError.unknownError.localizedDescription) } } @@ -132,7 +134,7 @@ struct ComposeMessageBodyView: View { isLoadingContent = false } catch { - dismissSheet() + dismissModal() IKSnackBar.showSnackBar(message: MailError.unknownError.localizedDescription) } } @@ -176,11 +178,6 @@ struct ComposeMessageBodyView: View { } attachmentsManager.completeUploadedAttachments() } - - private func dismissSheet() { - NotificationCenter.default.post(Notification(name: .dismissDraftView)) - dismiss() - } } struct ComposeMessageBodyView_Previews: PreviewProvider { diff --git a/Mail/Views/New Message/ComposeMessageView.swift b/Mail/Views/New Message/ComposeMessageView.swift index fc513ce24..fbcfb26b5 100644 --- a/Mail/Views/New Message/ComposeMessageView.swift +++ b/Mail/Views/New Message/ComposeMessageView.swift @@ -59,6 +59,7 @@ 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 @@ -115,7 +116,7 @@ struct ComposeMessageView: View { NavigationView { composeMessage } - .onAppear() { + .onAppear { attachmentsManager.importAttachments(attachments: initialAttachments) initialAttachments = [] } @@ -132,7 +133,7 @@ struct ComposeMessageView: View { } .customAlert(isPresented: $isShowingCancelAttachmentsError) { AttachmentsUploadInProgressErrorView { - dismissSheet() + dismissModal() } } .matomoView(view: ["ComposeMessage"]) @@ -209,7 +210,7 @@ struct ComposeMessageView: View { isShowingCancelAttachmentsError = true return } - dismissSheet() + dismissModal() } private func didTouchSend() { @@ -228,7 +229,7 @@ struct ComposeMessageView: View { liveDraft.action = .send } } - dismissSheet() + dismissModal() } private static func saveNewDraftInRealm(_ realm: Realm, draft: Draft) { @@ -239,11 +240,6 @@ struct ComposeMessageView: View { realm.add(draft, update: .modified) } } - - private func dismissSheet() { - NotificationCenter.default.post(Notification(name: .dismissDraftView)) - dismiss() - } } struct ComposeMessageView_Previews: PreviewProvider { diff --git a/MailShareExtension/ComposeMessageWrapperView.swift b/MailShareExtension/ComposeMessageWrapperView.swift index 78dac1e40..1ec1fa2cd 100644 --- a/MailShareExtension/ComposeMessageWrapperView.swift +++ b/MailShareExtension/ComposeMessageWrapperView.swift @@ -1,4 +1,3 @@ -// /* Infomaniak Mail - iOS App Copyright (C) 2022 Infomaniak Network SA @@ -25,6 +24,7 @@ import SwiftUI import UIKit struct ComposeMessageWrapperView: View { + @State var completionHandler: () -> Void @State var itemProviders: [NSItemProvider] @State private var draft = Draft() @LazyInjectService private var accountManager: AccountManager @@ -33,7 +33,9 @@ struct ComposeMessageWrapperView: View { if let mailboxManager = accountManager.currentMailboxManager { ComposeMessageView.newMessage(draft, mailboxManager: mailboxManager, itemProviders: itemProviders) .environmentObject(mailboxManager) - .navigationTitle(Text("Test")) + .environment(\.dismissModal) { + self.completionHandler() + } } else { Text("Please login in ikMail") .background(.red) diff --git a/MailShareExtension/ShareViewController.swift b/MailShareExtension/ShareViewController.swift index ae57c7c9c..4354daedc 100644 --- a/MailShareExtension/ShareViewController.swift +++ b/MailShareExtension/ShareViewController.swift @@ -46,13 +46,10 @@ final class ShareNavigationViewController: UIViewController { return } - // Listen to dismiss notification - NotificationCenter.default.addObserver(self, selector: #selector(willDismissDraft(notification:)), - name: .dismissDraftView, - object: nil) - // We need to go threw wrapping to use SwiftUI in an NSExtension. - let hostingController = UIHostingController(rootView: ComposeMessageWrapperView(itemProviders: itemProviders)) + let hostingController = UIHostingController(rootView: ComposeMessageWrapperView(completionHandler: { + self.dismiss(animated: true) + }, itemProviders: itemProviders)) hostingController.view.backgroundColor = .clear hostingController.view.translatesAutoresizingMaskIntoConstraints = false addChild(hostingController) @@ -67,11 +64,6 @@ final class ShareNavigationViewController: UIViewController { ]) } - @objc private func willDismissDraft(notification: NSNotification) { - print("willDismissDraft :\(notification)") - dismiss(animated: true) - } - override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { extensionContext!.completeRequest(returningItems: nil, completionHandler: nil) } From 1cb7146fc71f7978a4e7c5d464322ec2c5a82f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 3 Jul 2023 14:43:26 +0200 Subject: [PATCH 08/61] feat: open ikMail Scheme --- Mail/AppDelegate.swift | 13 ++++++++ Mail/Info.plist | 20 +++++++++--- .../ComposeMessageWrapperView.swift | 32 ++++++++++++++++--- .../Proxy/OrientationManager.swift | 5 ++- MailShareExtension/ShareViewController.swift | 18 ++++++++--- 5 files changed, 72 insertions(+), 16 deletions(-) diff --git a/Mail/AppDelegate.swift b/Mail/AppDelegate.swift index 1a16529cf..7393574c0 100644 --- a/Mail/AppDelegate.swift +++ b/Mail/AppDelegate.swift @@ -73,6 +73,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } + func application(_ application: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + // Determine who sent the URL. + let sendingAppID = options[.sourceApplication] + DDLogInfo("source application = \(sendingAppID ?? "Unknown")") + + let absoluteString = url.absoluteString + let success = (absoluteString.starts(with: "mailto") || absoluteString.starts(with: "mailto")) + DDLogInfo("URL handling = \(success ? "success" : "failure")") + return success + } + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { DDLogError("Failed registering for notifications: \(error)") } diff --git a/Mail/Info.plist b/Mail/Info.plist index 6a58d81b8..9b0b78657 100644 --- a/Mail/Info.plist +++ b/Mail/Info.plist @@ -36,6 +36,16 @@ mailto + + CFBundleTypeRole + Viewer + CFBundleURLName + com.infomaniak.mail.scheme + CFBundleURLSchemes + + ikmail + + CFBundleVersion $(CURRENT_PROJECT_VERSION) @@ -101,10 +111,10 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - NSAppTransportSecurity - - NSAllowsArbitraryLoadsInWebContent - - + NSAppTransportSecurity + + NSAllowsArbitraryLoadsInWebContent + + diff --git a/MailShareExtension/ComposeMessageWrapperView.swift b/MailShareExtension/ComposeMessageWrapperView.swift index 1ec1fa2cd..3651bf9e0 100644 --- a/MailShareExtension/ComposeMessageWrapperView.swift +++ b/MailShareExtension/ComposeMessageWrapperView.swift @@ -24,7 +24,8 @@ import SwiftUI import UIKit struct ComposeMessageWrapperView: View { - @State var completionHandler: () -> Void + @State var dismissHandler: () -> Void + @State var openAppHandler: () -> Void @State var itemProviders: [NSItemProvider] @State private var draft = Draft() @LazyInjectService private var accountManager: AccountManager @@ -34,11 +35,34 @@ struct ComposeMessageWrapperView: View { ComposeMessageView.newMessage(draft, mailboxManager: mailboxManager, itemProviders: itemProviders) .environmentObject(mailboxManager) .environment(\.dismissModal) { - self.completionHandler() + self.dismissHandler() } } else { - Text("Please login in ikMail") - .background(.red) + PleaseLoginView(openAppHandler: openAppHandler) + } + } +} + +struct PleaseLoginView: View { + @State var slide = Slide.onBoardingSlides.first! + + var openAppHandler: () -> Void + + var body: some View { + VStack { + MailShareExtensionAsset.logoText.swiftUIImage + .resizable() + .scaledToFit() + .frame(height: UIConstants.onboardingLogoHeight) + .padding(.top, UIConstants.onboardingLogoPaddingTop) + // TODO: i18n + Text("Please login in ikMail first") + .textStyle(.header2) + .padding(.top, UIConstants.onboardingLogoPaddingTop) + LottieView(configuration: slide.lottieConfiguration!) + Spacer() + }.onTapGesture { + openAppHandler() } } } diff --git a/MailShareExtension/Proxy/OrientationManager.swift b/MailShareExtension/Proxy/OrientationManager.swift index 09fae4876..0e243cc4f 100644 --- a/MailShareExtension/Proxy/OrientationManager.swift +++ b/MailShareExtension/Proxy/OrientationManager.swift @@ -22,10 +22,9 @@ import UIKit /// An OrientationManager that works in Extension mode public final class OrientationManager: OrientationManageable { - public var orientationLock = UIInterfaceOrientationMask.all - - public func setOrientationLock(_ orientation: UIInterfaceOrientationMask) { } + + public func setOrientationLock(_ orientation: UIInterfaceOrientationMask) {} public var interfaceOrientation: UIInterfaceOrientation? } diff --git a/MailShareExtension/ShareViewController.swift b/MailShareExtension/ShareViewController.swift index 4354daedc..010252d90 100644 --- a/MailShareExtension/ShareViewController.swift +++ b/MailShareExtension/ShareViewController.swift @@ -47,10 +47,20 @@ final class ShareNavigationViewController: UIViewController { } // We need to go threw wrapping to use SwiftUI in an NSExtension. - let hostingController = UIHostingController(rootView: ComposeMessageWrapperView(completionHandler: { - self.dismiss(animated: true) - }, itemProviders: itemProviders)) - hostingController.view.backgroundColor = .clear + let hostingController = UIHostingController(rootView: ComposeMessageWrapperView(dismissHandler: { + self.dismiss(animated: true) + }, openAppHandler: { + guard let extensionContext = self.extensionContext, + let ikDeeplink = URL(string: "ikmail:shareExtension") else { + return + } + + Task { + let result: Bool = await extensionContext.open(ikDeeplink) + assert(result == true, "should success") + } + }, + itemProviders: itemProviders)) hostingController.view.translatesAutoresizingMaskIntoConstraints = false addChild(hostingController) view.addSubview(hostingController.view) From aeeb83f2280be9ed1a049c6538752bb2efe4c628 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 3 Jul 2023 16:19:35 +0200 Subject: [PATCH 09/61] feat: dismiss share ext if loged out on tap, as open deeplink does not work in share ext. --- MailShareExtension/ShareViewController.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/MailShareExtension/ShareViewController.swift b/MailShareExtension/ShareViewController.swift index 010252d90..74754d584 100644 --- a/MailShareExtension/ShareViewController.swift +++ b/MailShareExtension/ShareViewController.swift @@ -56,8 +56,11 @@ final class ShareNavigationViewController: UIViewController { } Task { + // TODO: remove as does not work in share-ext let result: Bool = await extensionContext.open(ikDeeplink) - assert(result == true, "should success") + // assert(result == true, "should success") + + self.dismiss(animated: true) } }, itemProviders: itemProviders)) From b3946842c1599651209b610957d3700b5b58057f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 3 Jul 2023 17:10:37 +0200 Subject: [PATCH 10/61] feat: fix empty contact list in share extension --- MailShareExtension/ShareViewController.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/MailShareExtension/ShareViewController.swift b/MailShareExtension/ShareViewController.swift index 74754d584..765f52720 100644 --- a/MailShareExtension/ShareViewController.swift +++ b/MailShareExtension/ShareViewController.swift @@ -27,6 +27,8 @@ 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 + override public func viewDidLoad() { super.viewDidLoad() @@ -46,6 +48,13 @@ final class ShareNavigationViewController: UIViewController { 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 hostingController = UIHostingController(rootView: ComposeMessageWrapperView(dismissHandler: { self.dismiss(animated: true) From 94235a7de872a78f07cbd688a2dc6d1eed8e996e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Mon, 3 Jul 2023 18:08:48 +0200 Subject: [PATCH 11/61] feat: SnackBar works in Extension with DI --- Mail/Helpers/AppAssembly.swift | 3 + MailCore/Utils/IKSnackBar+Extension.swift | 39 ++++++++- MailCore/Utils/SnackBarPresentable.swift | 88 ++++++++++++++++++++ MailShareExtension/ShareViewController.swift | 12 +++ 4 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 MailCore/Utils/SnackBarPresentable.swift diff --git a/Mail/Helpers/AppAssembly.swift b/Mail/Helpers/AppAssembly.swift index a803549d6..ae924be87 100644 --- a/Mail/Helpers/AppAssembly.swift +++ b/Mail/Helpers/AppAssembly.swift @@ -73,6 +73,9 @@ enum ApplicationAssembly { }, Factory(type: AccountManager.self) { _, _ in AccountManager() + }, + Factory(type: SnackBarPresentable.self) { _, _ in + SnackBarPresenter() } ] diff --git a/MailCore/Utils/IKSnackBar+Extension.swift b/MailCore/Utils/IKSnackBar+Extension.swift index 8cf427145..1ecd86376 100644 --- a/MailCore/Utils/IKSnackBar+Extension.swift +++ b/MailCore/Utils/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 { @@ -59,18 +60,48 @@ public class SnackBarAvoider { public extension IKSnackBar { @discardableResult @MainActor + /// 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 showSnackBar( 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) + + 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 = action { - snackbar?.setAction(action).show() + snackbar.setAction(action).show() } else { - snackbar?.show() + snackbar.show() } return snackbar } diff --git a/MailCore/Utils/SnackBarPresentable.swift b/MailCore/Utils/SnackBarPresentable.swift new file mode 100644 index 000000000..fb8ec9470 --- /dev/null +++ b/MailCore/Utils/SnackBarPresentable.swift @@ -0,0 +1,88 @@ +/* + 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: move to core UI +// TODO: Use our type not the lib +public extension IKSnackBar { + enum Duration: Equatable { + case lengthLong + case lengthShort + case infinite + case custom(CGFloat) + + var value: CGFloat { + switch self { + case .lengthLong: + return 3.5 + case .lengthShort: + return 2 + case .infinite: + return -1 + case .custom(let duration): + return duration + } + } + } +} + +public protocol SnackBarPresentable { + func show(message: String) + 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, + duration: SnackBar.Duration = .lengthLong, + action: IKSnackBar.Action? = nil, + anchor: CGFloat = 0, + contextView: UIView? = nil + ) { + Task { @MainActor in + IKSnackBar.showSnackBar( + message: message, + duration: duration, + action: action, + anchor: anchor, + contextView: contextView + ) + } + } +} diff --git a/MailShareExtension/ShareViewController.swift b/MailShareExtension/ShareViewController.swift index 765f52720..a7f176ede 100644 --- a/MailShareExtension/ShareViewController.swift +++ b/MailShareExtension/ShareViewController.swift @@ -28,10 +28,20 @@ final class ShareNavigationViewController: UIViewController { private let dependencyInjectionHook = EarlyDIHook() @LazyInjectService private var accountManager: AccountManager + @LazyInjectService private var snackbarPresenter: SnackBarPresentable + + 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) + // Modify sheet size on iPadOS, property is ignored on iOS preferredContentSize = CGSize(width: 540, height: 620) @@ -55,6 +65,8 @@ final class ShareNavigationViewController: UIViewController { } } + self.snackbarPresenter.show(message: "test snackbar in ext") + // We need to go threw wrapping to use SwiftUI in an NSExtension. let hostingController = UIHostingController(rootView: ComposeMessageWrapperView(dismissHandler: { self.dismiss(animated: true) From 14b2112504da4f5f8d01fdcb79c3fc6241fefcdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 4 Jul 2023 11:00:01 +0200 Subject: [PATCH 12/61] fix(draft): dismiss of draft editor works in app context --- Mail/Views/New Message/ComposeMessageBodyView.swift | 12 +++++++++--- Mail/Views/New Message/ComposeMessageView.swift | 12 +++++++++--- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Mail/Views/New Message/ComposeMessageBodyView.swift b/Mail/Views/New Message/ComposeMessageBodyView.swift index b0c69a84c..27af0b5dc 100644 --- a/Mail/Views/New Message/ComposeMessageBodyView.swift +++ b/Mail/Views/New Message/ComposeMessageBodyView.swift @@ -81,7 +81,7 @@ struct ComposeMessageBodyView: View { case .error: // Unable to get signatures, "An error occurred" and close modal. IKSnackBar.showSnackBar(message: MailError.unknownError.localizedDescription) - dismissModal() + dismissMessageView() case .progress: break } @@ -116,7 +116,7 @@ struct ComposeMessageBodyView: View { } isLoadingContent = false } catch { - dismissModal() + dismissMessageView() IKSnackBar.showSnackBar(message: MailError.unknownError.localizedDescription) } } @@ -134,7 +134,7 @@ struct ComposeMessageBodyView: View { isLoadingContent = false } catch { - dismissModal() + dismissMessageView() IKSnackBar.showSnackBar(message: MailError.unknownError.localizedDescription) } } @@ -178,6 +178,12 @@ struct ComposeMessageBodyView: View { } attachmentsManager.completeUploadedAttachments() } + + /// Something to dismiss the view regardless of presentation context + private func dismissMessageView() { + dismissModal() + dismiss() + } } struct ComposeMessageBodyView_Previews: PreviewProvider { diff --git a/Mail/Views/New Message/ComposeMessageView.swift b/Mail/Views/New Message/ComposeMessageView.swift index fbcfb26b5..a21e49b08 100644 --- a/Mail/Views/New Message/ComposeMessageView.swift +++ b/Mail/Views/New Message/ComposeMessageView.swift @@ -133,7 +133,7 @@ struct ComposeMessageView: View { } .customAlert(isPresented: $isShowingCancelAttachmentsError) { AttachmentsUploadInProgressErrorView { - dismissModal() + dismissMessageView() } } .matomoView(view: ["ComposeMessage"]) @@ -205,12 +205,18 @@ struct ComposeMessageView: View { // MAK: - 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 } - dismissModal() + dismissMessageView() } private func didTouchSend() { @@ -229,7 +235,7 @@ struct ComposeMessageView: View { liveDraft.action = .send } } - dismissModal() + dismissMessageView() } private static func saveNewDraftInRealm(_ realm: Realm, draft: Draft) { From 417b0f315323c297cd1eb066923332ab01cc41b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 4 Jul 2023 11:35:26 +0200 Subject: [PATCH 13/61] feat: use app wise @LazyInjectService private var snackbarPresenter --- Mail/Helpers/WorkInProgress.swift | 4 +++- Mail/Views/Alerts/AddLinkView.swift | 5 ++-- Mail/Views/Alerts/ReportPhishingView.swift | 4 +++- .../Actions/ActionsViewModel.swift | 3 ++- .../Bottom sheets/ContactActionsView.swift | 5 ++-- .../ReportDisplayProblemView.swift | 4 +++- .../Bottom sheets/RestoreEmailsView.swift | 4 ++-- .../MailboxManagement/MailboxCell.swift | 3 ++- .../New Message/ComposeMessageBodyView.swift | 9 ++++--- .../ComposeMessageCellRecipients.swift | 8 ++++--- .../Recipients/RecipientChip.swift | 4 +++- Mail/Views/Onboarding/OnboardingView.swift | 5 ++-- Mail/Views/SplitView.swift | 3 ++- Mail/Views/Switch User/AccountView.swift | 5 ++-- Mail/Views/Switch User/AddMailboxView.swift | 3 ++- MailCore/Cache/DraftManager.swift | 24 +++++++++---------- MailCore/Utils/Error+Extension.swift | 10 ++++---- MailCore/Utils/SnackBarPresentable.swift | 5 ++++ MailShareExtension/ShareViewController.swift | 3 --- 19 files changed, 66 insertions(+), 45 deletions(-) diff --git a/Mail/Helpers/WorkInProgress.swift b/Mail/Helpers/WorkInProgress.swift index 711d7a063..15b13d156 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) + @LazyInjectService var snackbarPresenter: SnackBarPresentable + snackbarPresenter.show(message: MailResourcesStrings.Localizable.workInProgressTitle) } 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/ReportPhishingView.swift b/Mail/Views/Alerts/ReportPhishingView.swift index b7ef1bdf6..cf430723f 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/Bottom sheets/Actions/ActionsViewModel.swift b/Mail/Views/Bottom sheets/Actions/ActionsViewModel.swift index f417a33d5..949bda001 100644 --- a/Mail/Views/Bottom sheets/Actions/ActionsViewModel.swift +++ b/Mail/Views/Bottom sheets/Actions/ActionsViewModel.swift @@ -222,6 +222,7 @@ enum ActionsTarget: Equatable, Identifiable { @LazyInjectService private var matomo: MatomoUtils @LazyInjectService private var accountManager: AccountManager + @LazyInjectService private var snackbarPresenter: SnackBarPresentable init(mailboxManager: MailboxManager, target: ActionsTarget, @@ -443,7 +444,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/ContactActionsView.swift b/Mail/Views/Bottom sheets/ContactActionsView.swift index bd7d14e4b..2c2855afe 100644 --- a/Mail/Views/Bottom sheets/ContactActionsView.swift +++ b/Mail/Views/Bottom sheets/ContactActionsView.swift @@ -29,6 +29,7 @@ struct ContactActionsView: View { @LazyInjectService private var matomo: MatomoUtils @LazyInjectService private var accountManager: AccountManager + @LazyInjectService private var snackbarPresenter: SnackBarPresentable @State private var writtenToRecipient: Recipient? @@ -132,14 +133,14 @@ struct ContactActionsView: View { Task { await tryOrDisplayError { try await accountManager.currentContactManager?.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/Views/Menu Drawer/MailboxManagement/MailboxCell.swift b/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift index daa411510..d74d7b64f 100644 --- a/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift +++ b/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift @@ -45,6 +45,7 @@ struct MailboxCell: View { @Environment(\.window) private var window @LazyInjectService private var accountManager: AccountManager + @LazyInjectService private var snackbarPresenter: SnackBarPresentable let mailbox: Mailbox @@ -70,7 +71,7 @@ struct MailboxCell: View { ) { guard !isSelected else { return } guard mailbox.isPasswordValid else { - IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.frelatedMailbox) + snackbarPresenter.show(message: MailResourcesStrings.Localizable.frelatedMailbox) return } @InjectService var matomo: MatomoUtils diff --git a/Mail/Views/New Message/ComposeMessageBodyView.swift b/Mail/Views/New Message/ComposeMessageBodyView.swift index 27af0b5dc..bdafe6273 100644 --- a/Mail/Views/New Message/ComposeMessageBodyView.swift +++ b/Mail/Views/New Message/ComposeMessageBodyView.swift @@ -17,11 +17,14 @@ */ import InfomaniakCoreUI +import InfomaniakDI import MailCore import RealmSwift import SwiftUI struct ComposeMessageBodyView: View { + @LazyInjectService private var snackbarPresenter: SnackBarPresentable + @Environment(\.dismissModal) var dismissModal @EnvironmentObject private var mailboxManager: MailboxManager @@ -80,7 +83,7 @@ struct ComposeMessageBodyView: View { setSignature() case .error: // Unable to get signatures, "An error occurred" and close modal. - IKSnackBar.showSnackBar(message: MailError.unknownError.localizedDescription) + snackbarPresenter.show(message: MailError.unknownError.localizedDescription) dismissMessageView() case .progress: break @@ -117,7 +120,7 @@ struct ComposeMessageBodyView: View { isLoadingContent = false } catch { dismissMessageView() - IKSnackBar.showSnackBar(message: MailError.unknownError.localizedDescription) + snackbarPresenter.show(message: MailError.unknownError.localizedDescription) } } @@ -135,7 +138,7 @@ struct ComposeMessageBodyView: View { isLoadingContent = false } catch { dismissMessageView() - IKSnackBar.showSnackBar(message: MailError.unknownError.localizedDescription) + snackbarPresenter.show(message: MailError.unknownError.localizedDescription) } } 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/RecipientChip.swift b/Mail/Views/New Message/Recipients/RecipientChip.swift index a42d2c2f9..3a346ec7a 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 @@ -25,6 +26,7 @@ import SwiftUI struct RecipientChip: View { @Environment(\.window) private var window @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor + @LazyInjectService private var snackbarPresenter: SnackBarPresentable let recipient: Recipient let fieldType: ComposeViewFieldType @@ -46,7 +48,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 f81f2a71e..30f48d21b 100644 --- a/Mail/Views/Onboarding/OnboardingView.swift +++ b/Mail/Views/Onboarding/OnboardingView.swift @@ -73,7 +73,8 @@ final class LoginHandler: InfomaniakLoginDelegate, ObservableObject { @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 var sceneDelegate: SceneDelegate? @@ -137,7 +138,7 @@ final class LoginHandler: InfomaniakLoginDelegate, ObservableObject { if let previousAccount = previousAccount { accountManager.switchAccount(newAccount: previousAccount) } - IKSnackBar.showSnackBar(message: error.localizedDescription) + snackbarPresenter.show(message: error.localizedDescription) } isLoading = false } diff --git a/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift index eb8207116..eb02d9f0b 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -50,6 +50,7 @@ struct SplitView: View { @StateObject private var splitViewManager: SplitViewManager @LazyInjectService private var orientationManager: OrientationManageable + @LazyInjectService private var snackbarPresenter: SnackBarPresentable let mailboxManager: MailboxManager @@ -115,7 +116,7 @@ struct SplitView: View { if let tappedNotificationThread = tappedNotificationMessage?.originalThread { navigationStore.threadPath = [tappedNotificationThread] } else { - IKSnackBar.showSnackBar(message: MailError.localMessageNotFound.errorDescription) + snackbarPresenter.show(message: MailError.localMessageNotFound.errorDescription) } } .onReceive(NotificationCenter.default.publisher(for: .onOpenedMailTo)) { identifiableURLComponents in diff --git a/Mail/Views/Switch User/AccountView.swift b/Mail/Views/Switch User/AccountView.swift index c3337bbff..cd4c3bc75 100644 --- a/Mail/Views/Switch User/AccountView.swift +++ b/Mail/Views/Switch User/AccountView.swift @@ -28,6 +28,7 @@ import SwiftUI final class AccountViewDelegate: DeleteAccountDelegate { @LazyInjectService private var rootViewManager: RootViewManageable @LazyInjectService private var accountManager: AccountManager + @LazyInjectService private var snackbarPresenter: SnackBarPresentable @MainActor func didCompleteDeleteAccount() { guard let account = accountManager.currentAccount else { @@ -39,7 +40,7 @@ final class AccountViewDelegate: DeleteAccountDelegate { let window = rootViewManager.mainSceneKeyWindow if let nextAccount = accountManager.accounts.first { (window?.windowScene?.delegate as? SceneDelegate)?.switchAccount(nextAccount) - IKSnackBar.showSnackBar(message: "Account deleted") + snackbarPresenter.show(message: "Account deleted") } else { (window?.windowScene?.delegate as? SceneDelegate)?.showLoginView() } @@ -49,7 +50,7 @@ final class AccountViewDelegate: DeleteAccountDelegate { @MainActor func didFailDeleteAccount(error: InfomaniakLoginError) { SentrySDK.capture(error: error) - IKSnackBar.showSnackBar(message: "Failed to delete account") + snackbarPresenter.show(message: "Failed to delete account") } } diff --git a/Mail/Views/Switch User/AddMailboxView.swift b/Mail/Views/Switch User/AddMailboxView.swift index b647ddc2d..c1624966b 100644 --- a/Mail/Views/Switch User/AddMailboxView.swift +++ b/Mail/Views/Switch User/AddMailboxView.swift @@ -26,6 +26,7 @@ struct AddMailboxView: View { @Environment(\.dismiss) var dismiss @LazyInjectService private var accountManager: AccountManager + @LazyInjectService private var snackbarPresenter: SnackBarPresentable var completion: (Mailbox?) -> Void @@ -104,7 +105,7 @@ struct AddMailboxView: View { showError = true password = "" } - await IKSnackBar.showSnackBar(message: error.localizedDescription) + snackbarPresenter.show(message: error.localizedDescription) } } } diff --git a/MailCore/Cache/DraftManager.swift b/MailCore/Cache/DraftManager.swift index c5dd1842e..f0c78e2ef 100644 --- a/MailCore/Cache/DraftManager.swift +++ b/MailCore/Cache/DraftManager.swift @@ -73,6 +73,7 @@ public final class DraftManager { private static let saveExpirationSec = 3 @LazyInjectService private var matomo: MatomoUtils + @LazyInjectService private var snackbarPresenter: SnackBarPresentable /// Used by DI only public init() { @@ -94,15 +95,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 } + snackbarPresenter.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) + snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarEmailSending) var sendDate: Date? await draftQueue.cleanQueueElement(uuid: draft.localUUID) @@ -110,10 +110,10 @@ public final class DraftManager { do { let cancelableResponse = try await mailboxManager.send(draft: draft) - await IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.snackbarEmailSent) + snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarEmailSent) sendDate = cancelableResponse.scheduledDate } catch { - await IKSnackBar.showSnackBar(message: error.localizedDescription) + snackbarPresenter.show(message: error.localizedDescription) } await draftQueue.endBackgroundTask(uuid: draft.localUUID) return sendDate @@ -179,11 +179,11 @@ public final class DraftManager { } await saveDraftRemotely(draft: draft, mailboxManager: mailboxManager) - 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) - }) + snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarDraftSaved, + action: .init(title: MailResourcesStrings.Localizable.actionDelete) { [weak self] in + self?.matomo.track(eventWithCategory: .snackbar, name: "deleteDraft") + self?.deleteDraftSnackBarAction(draft: draft, mailboxManager: mailboxManager) + }) } private func refreshDraftFolder(latestSendDate: Date?, mailboxManager: MailboxManager) async throws { @@ -209,7 +209,7 @@ public final class DraftManager { await tryOrDisplayError { if let liveDraft = draft.thaw() { try await mailboxManager.delete(draft: liveDraft.freeze()) - await IKSnackBar.showSnackBar(message: MailResourcesStrings.Localizable.snackbarDraftDeleted) + snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarDraftDeleted) if let draftFolder = mailboxManager.getFolder(with: .draft)?.freeze() { await mailboxManager.refresh(folder: draftFolder) } diff --git a/MailCore/Utils/Error+Extension.swift b/MailCore/Utils/Error+Extension.swift index 7d147d170..b536b5763 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) { + @LazyInjectService var snackbarPresenter: SnackBarPresentable if let error = error as? MailError { if error.shouldDisplay && !Bundle.main.isExtension { - Task.detached { - await IKSnackBar.showSnackBar(message: error.errorDescription) - } + snackbarPresenter.show(message: error.errorDescription) } else { SentrySDK.capture(message: "Encountered error that we didn't display to the user") { scope in scope.setContext( @@ -53,9 +53,7 @@ private func displayErrorIfNeeded(error: Error) { } DDLogError("MailError: \(error)") } else if error.shouldDisplay && !Bundle.main.isExtension { - Task.detached { - await IKSnackBar.showSnackBar(message: error.localizedDescription) - } + snackbarPresenter.show(message: error.localizedDescription) DDLogError("Error: \(error)") } } diff --git a/MailCore/Utils/SnackBarPresentable.swift b/MailCore/Utils/SnackBarPresentable.swift index fb8ec9470..c32b1f82b 100644 --- a/MailCore/Utils/SnackBarPresentable.swift +++ b/MailCore/Utils/SnackBarPresentable.swift @@ -47,6 +47,7 @@ public extension IKSnackBar { public protocol SnackBarPresentable { func show(message: String) + func show(message: String, action: IKSnackBar.Action?) func show( message: String, duration: SnackBar.Duration, @@ -68,6 +69,10 @@ public final class SnackBarPresenter: SnackBarPresentable { 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, diff --git a/MailShareExtension/ShareViewController.swift b/MailShareExtension/ShareViewController.swift index a7f176ede..d9857ece3 100644 --- a/MailShareExtension/ShareViewController.swift +++ b/MailShareExtension/ShareViewController.swift @@ -28,7 +28,6 @@ final class ShareNavigationViewController: UIViewController { private let dependencyInjectionHook = EarlyDIHook() @LazyInjectService private var accountManager: AccountManager - @LazyInjectService private var snackbarPresenter: SnackBarPresentable private func overrideSnackBarPresenter(contextView: UIView) { let snackBarPresenter = Factory(type: SnackBarPresentable.self) { _, _ in @@ -65,8 +64,6 @@ final class ShareNavigationViewController: UIViewController { } } - self.snackbarPresenter.show(message: "test snackbar in ext") - // We need to go threw wrapping to use SwiftUI in an NSExtension. let hostingController = UIHostingController(rootView: ComposeMessageWrapperView(dismissHandler: { self.dismiss(animated: true) From 06cf460f847f9039b9219cdbd41da8c621c719ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 4 Jul 2023 15:19:36 +0200 Subject: [PATCH 14/61] feat(shareExtension): what is displayed with a snackbar in the app is now displayed with a notification while using the share extension feat: abstract application state, so it builds in share extension. feat: abstract the way to present a Snackbar within the app, so it can use a Notification within shareExtension thanks to MessagePresentable --- Mail/Helpers/AppAssembly.swift | 6 + .../Implementation/ApplicationState.swift | 26 ++++ .../New Message/ComposeMessageBodyView.swift | 8 +- MailCore/Cache/DraftManager.swift | 22 ++-- MailCore/Utils/ApplicationStatable.swift | 26 ++++ MailCore/Utils/MessagePresentable.swift | 116 ++++++++++++++++++ .../Proxy/ApplicationState.swift | 26 ++++ 7 files changed, 215 insertions(+), 15 deletions(-) create mode 100644 Mail/Proxy/Implementation/ApplicationState.swift create mode 100644 MailCore/Utils/ApplicationStatable.swift create mode 100644 MailCore/Utils/MessagePresentable.swift create mode 100644 MailShareExtension/Proxy/ApplicationState.swift diff --git a/Mail/Helpers/AppAssembly.swift b/Mail/Helpers/AppAssembly.swift index ae924be87..48970e6f4 100644 --- a/Mail/Helpers/AppAssembly.swift +++ b/Mail/Helpers/AppAssembly.swift @@ -76,6 +76,12 @@ enum ApplicationAssembly { }, Factory(type: SnackBarPresentable.self) { _, _ in SnackBarPresenter() + }, + Factory(type: MessagePresentable.self) { _, _ in + MessagePresenter() + }, + Factory(type: ApplicationStatable.self) { _, _ in + ApplicationState() } ] diff --git a/Mail/Proxy/Implementation/ApplicationState.swift b/Mail/Proxy/Implementation/ApplicationState.swift new file mode 100644 index 000000000..093238df3 --- /dev/null +++ b/Mail/Proxy/Implementation/ApplicationState.swift @@ -0,0 +1,26 @@ +/* + Infomaniak Mail - iOS App + Copyright (C) 2022 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import UIKit +import MailCore + +public struct ApplicationState: ApplicationStatable { + public var applicationState: UIApplication.State? { + UIApplication.shared.applicationState + } +} diff --git a/Mail/Views/New Message/ComposeMessageBodyView.swift b/Mail/Views/New Message/ComposeMessageBodyView.swift index bdafe6273..b1de1aa74 100644 --- a/Mail/Views/New Message/ComposeMessageBodyView.swift +++ b/Mail/Views/New Message/ComposeMessageBodyView.swift @@ -23,7 +23,7 @@ import RealmSwift import SwiftUI struct ComposeMessageBodyView: View { - @LazyInjectService private var snackbarPresenter: SnackBarPresentable + @LazyInjectService private var messagePresentable: MessagePresentable @Environment(\.dismissModal) var dismissModal @@ -83,7 +83,7 @@ struct ComposeMessageBodyView: View { setSignature() case .error: // Unable to get signatures, "An error occurred" and close modal. - snackbarPresenter.show(message: MailError.unknownError.localizedDescription) + messagePresentable.show(message: MailError.unknownError.localizedDescription) dismissMessageView() case .progress: break @@ -120,7 +120,7 @@ struct ComposeMessageBodyView: View { isLoadingContent = false } catch { dismissMessageView() - snackbarPresenter.show(message: MailError.unknownError.localizedDescription) + messagePresentable.show(message: MailError.unknownError.localizedDescription) } } @@ -138,7 +138,7 @@ struct ComposeMessageBodyView: View { isLoadingContent = false } catch { dismissMessageView() - snackbarPresenter.show(message: MailError.unknownError.localizedDescription) + messagePresentable.show(message: MailError.unknownError.localizedDescription) } } diff --git a/MailCore/Cache/DraftManager.swift b/MailCore/Cache/DraftManager.swift index f0c78e2ef..7e6c876e4 100644 --- a/MailCore/Cache/DraftManager.swift +++ b/MailCore/Cache/DraftManager.swift @@ -73,7 +73,7 @@ public final class DraftManager { private static let saveExpirationSec = 3 @LazyInjectService private var matomo: MatomoUtils - @LazyInjectService private var snackbarPresenter: SnackBarPresentable + @LazyInjectService private var messagePresentable: MessagePresentable /// Used by DI only public init() { @@ -96,13 +96,13 @@ public final class DraftManager { try await mailboxManager.save(draft: draft) } catch { guard error.shouldDisplay else { return } - snackbarPresenter.show(message: error.localizedDescription) + messagePresentable.show(message: error.localizedDescription) } await draftQueue.endBackgroundTask(uuid: draft.localUUID) } public func send(draft: Draft, mailboxManager: MailboxManager) async -> Date? { - snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarEmailSending) + messagePresentable.show(message: MailResourcesStrings.Localizable.snackbarEmailSending) var sendDate: Date? await draftQueue.cleanQueueElement(uuid: draft.localUUID) @@ -110,10 +110,10 @@ public final class DraftManager { do { let cancelableResponse = try await mailboxManager.send(draft: draft) - snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarEmailSent) + messagePresentable.show(message: MailResourcesStrings.Localizable.snackbarEmailSent) sendDate = cancelableResponse.scheduledDate } catch { - snackbarPresenter.show(message: error.localizedDescription) + messagePresentable.show(message: error.localizedDescription) } await draftQueue.endBackgroundTask(uuid: draft.localUUID) return sendDate @@ -179,11 +179,11 @@ public final class DraftManager { } await saveDraftRemotely(draft: draft, mailboxManager: mailboxManager) - snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarDraftSaved, - action: .init(title: MailResourcesStrings.Localizable.actionDelete) { [weak self] in - self?.matomo.track(eventWithCategory: .snackbar, name: "deleteDraft") - self?.deleteDraftSnackBarAction(draft: draft, mailboxManager: mailboxManager) - }) + let messageAction: MessageAction = (MailResourcesStrings.Localizable.actionDelete, { [weak self] in + self?.matomo.track(eventWithCategory: .snackbar, name: "deleteDraft") + self?.deleteDraftSnackBarAction(draft: draft, mailboxManager: mailboxManager) + }) + messagePresentable.show(message: MailResourcesStrings.Localizable.snackbarDraftSaved, action: messageAction) } private func refreshDraftFolder(latestSendDate: Date?, mailboxManager: MailboxManager) async throws { @@ -209,7 +209,7 @@ public final class DraftManager { await tryOrDisplayError { if let liveDraft = draft.thaw() { try await mailboxManager.delete(draft: liveDraft.freeze()) - snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarDraftDeleted) + messagePresentable.show(message: MailResourcesStrings.Localizable.snackbarDraftDeleted) if let draftFolder = mailboxManager.getFolder(with: .draft)?.freeze() { await mailboxManager.refresh(folder: draftFolder) } diff --git a/MailCore/Utils/ApplicationStatable.swift b/MailCore/Utils/ApplicationStatable.swift new file mode 100644 index 000000000..52651173b --- /dev/null +++ b/MailCore/Utils/ApplicationStatable.swift @@ -0,0 +1,26 @@ +/* + Infomaniak Mail - iOS App + Copyright (C) 2022 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import UIKit + +// TODO: Move to CoreUI + +/// Something that reads the application state if available +public protocol ApplicationStatable { + var applicationState: UIApplication.State? { get } +} diff --git a/MailCore/Utils/MessagePresentable.swift b/MailCore/Utils/MessagePresentable.swift new file mode 100644 index 000000000..83e5d4342 --- /dev/null +++ b/MailCore/Utils/MessagePresentable.swift @@ -0,0 +1,116 @@ +/* + Infomaniak Mail - iOS App + Copyright (C) 2022 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import CocoaLumberjackSwift +import Foundation +import InfomaniakCore +import InfomaniakCoreUI +import InfomaniakDI +import UserNotifications + +// TODO: Move to CoreUI / use with kDrive + +/// Something that can present a message to the user, abstracted of execution context (App / NSExtension) +/// +/// Will present a snackbar while main app is opened, a local notification in Extension or Background. +public protocol MessagePresentable { + /// Will present a snackbar while main app is opened, a local notification in Extension or Background. + /// - Parameter message: The message to display + func show(message: String) + + /// Will present a snackbar while main app is opened, a local notification in Extension or Background. + /// - Parameters: + /// - message: The message to display + /// - action: Title and closure associated with the action + func show(message: String, action: MessageAction) +} + +public typealias MessageAction = (name: String, closure: () -> Void) + +public final class MessagePresenter: MessagePresentable { + @LazyInjectService private var snackbarPresenter: SnackBarPresentable + @LazyInjectService private var applicationState: ApplicationStatable + + /// Used by DI + public init() { + // META: keep sonarcloud happy + } + + // MARK: - MessagePresentable + + public func show(message: String) { + showInContext(message: message, action: nil) + } + + public func show(message: String, action: MessageAction) { + showInContext(message: message, action: action) + } + + // MARK: - private + + private func showInContext(message: String, action: MessageAction?) { + // check not in extension mode + guard !Bundle.main.isExtension else { + presentInLocalNotification(message: message, action: action) + return + } + + // if app not in foreground, we use the local notifications + guard applicationState.applicationState == .active else { + presentInLocalNotification(message: message, action: action) + return + } + + // Present the message as we are in foreground app context + presentInSnackbar(message: message, action: action) + } + + // MARK: Private + + private func presentInSnackbar(message: String, action: MessageAction?) { + guard let action = action else { + snackbarPresenter.show(message: message) + return + } + + let snackBarAction = IKSnackBar.Action(title: action.name, action: action.closure) + snackbarPresenter.show(message: message, action: snackBarAction) + } + + private func presentInLocalNotification(message: String, action: MessageAction?) { + if action != nil { + DDLogError("Action not implemented in notifications for now") + } + + let content = UNMutableNotificationContent() + content.body = message + + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.3, repeats: false) + let uuidString = UUID().uuidString + let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: trigger) + let notificationCenter = UNUserNotificationCenter.current() + notificationCenter.add(request) { error in + DDLogError("MessagePresenter local notification error:\(String(describing: error)) ") + } + + // Self destruct this notification, as used only for user feedback + DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) { + notificationCenter.removeDeliveredNotifications(withIdentifiers: [uuidString]) + } + } +} diff --git a/MailShareExtension/Proxy/ApplicationState.swift b/MailShareExtension/Proxy/ApplicationState.swift new file mode 100644 index 000000000..43c701d41 --- /dev/null +++ b/MailShareExtension/Proxy/ApplicationState.swift @@ -0,0 +1,26 @@ +/* + Infomaniak Mail - iOS App + Copyright (C) 2022 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import UIKit +import MailCore + +public struct ApplicationState: ApplicationStatable { + public var applicationState: UIApplication.State? { + nil + } +} From f337a694b36b8a5557f52a203ad97dd15c826bea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Tue, 4 Jul 2023 15:32:29 +0200 Subject: [PATCH 15/61] feat: make sure we call applicationState from the main thread feat: use ApplicationStatable where possible --- Mail/AppDelegate.swift | 3 ++- MailCore/Utils/ApplicationStatable.swift | 2 +- MailCore/Utils/MessagePresentable.swift | 28 +++++++++++++----------- MailCore/Utils/NotificationsHelper.swift | 3 ++- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/Mail/AppDelegate.swift b/Mail/AppDelegate.swift index 7393574c0..bb161b013 100644 --- a/Mail/AppDelegate.swift +++ b/Mail/AppDelegate.swift @@ -34,6 +34,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { @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() @@ -42,7 +43,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { Logging.initLogging() - DDLogInfo("Application starting in foreground ? \(UIApplication.shared.applicationState != .background)") + DDLogInfo("Application starting in foreground ? \(applicationState.applicationState != .background)") ApiFetcher.decoder.dateDecodingStrategy = .iso8601 UNUserNotificationCenter.current().delegate = notificationCenterDelegate diff --git a/MailCore/Utils/ApplicationStatable.swift b/MailCore/Utils/ApplicationStatable.swift index 52651173b..191042f88 100644 --- a/MailCore/Utils/ApplicationStatable.swift +++ b/MailCore/Utils/ApplicationStatable.swift @@ -22,5 +22,5 @@ import UIKit /// Something that reads the application state if available public protocol ApplicationStatable { - var applicationState: UIApplication.State? { get } + @MainActor var applicationState: UIApplication.State? { get } } diff --git a/MailCore/Utils/MessagePresentable.swift b/MailCore/Utils/MessagePresentable.swift index 83e5d4342..45e1a8142 100644 --- a/MailCore/Utils/MessagePresentable.swift +++ b/MailCore/Utils/MessagePresentable.swift @@ -64,20 +64,22 @@ public final class MessagePresenter: MessagePresentable { // MARK: - private private func showInContext(message: String, action: MessageAction?) { - // check not in extension mode - guard !Bundle.main.isExtension else { - presentInLocalNotification(message: message, action: action) - return + 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) } - - // 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 diff --git a/MailCore/Utils/NotificationsHelper.swift b/MailCore/Utils/NotificationsHelper.swift index 2a484d116..1f5490b3d 100644 --- a/MailCore/Utils/NotificationsHelper.swift +++ b/MailCore/Utils/NotificationsHelper.swift @@ -107,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) From 49e35cdb474c5ef78110b9bbc7f877890052338c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 5 Jul 2023 11:39:48 +0200 Subject: [PATCH 16/61] feat: save current draft only on share ext close --- .../New Message/ComposeMessageBodyView.swift | 1 + .../New Message/ComposeMessageView.swift | 5 +- MailCore/Cache/DraftManager.swift | 38 +++++++------ MailCore/Cache/MailboxManager.swift | 1 + .../ComposeMessageWrapperView.swift | 57 ++++++++++++++++--- MailShareExtension/ShareViewController.swift | 13 ----- 6 files changed, 76 insertions(+), 39 deletions(-) diff --git a/Mail/Views/New Message/ComposeMessageBodyView.swift b/Mail/Views/New Message/ComposeMessageBodyView.swift index b1de1aa74..281265938 100644 --- a/Mail/Views/New Message/ComposeMessageBodyView.swift +++ b/Mail/Views/New Message/ComposeMessageBodyView.swift @@ -109,6 +109,7 @@ struct ComposeMessageBodyView: View { } } + /// Save draft remotely if never done before and update it with a remote ID. private func prepareCompleteDraft() async { guard draft.messageUid != nil && draft.remoteUUID.isEmpty else { return } diff --git a/Mail/Views/New Message/ComposeMessageView.swift b/Mail/Views/New Message/ComposeMessageView.swift index a21e49b08..4ac9aa3bd 100644 --- a/Mail/Views/New Message/ComposeMessageView.swift +++ b/Mail/Views/New Message/ComposeMessageView.swift @@ -168,7 +168,10 @@ struct ComposeMessageView: View { } } .onDisappear { - draftManager.syncDraft(mailboxManager: mailboxManager) + // Only process _all_ drafts if in main app only + if !Bundle.main.isExtension { + draftManager.syncDraft(mailboxManager: mailboxManager) + } } .overlay { if isLoadingContent || signatureManager.loadingSignatureState == .progress { diff --git a/MailCore/Cache/DraftManager.swift b/MailCore/Cache/DraftManager.swift index 7e6c876e4..2503d6eb6 100644 --- a/MailCore/Cache/DraftManager.swift +++ b/MailCore/Cache/DraftManager.swift @@ -128,7 +128,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: @@ -151,6 +151,26 @@ public final class DraftManager { } } + /// First save of a draft with the remote if needed + @discardableResult + public func initialSaveRemotely(draft: Draft, mailboxManager: MailboxManager) async -> Bool { + print("initialSaveRemotely:\(draft.localUUID)") + // 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 false + } + + await saveDraftRemotely(draft: draft, mailboxManager: mailboxManager) + let messageAction: MessageAction = (MailResourcesStrings.Localizable.actionDelete, { [weak self] in + self?.matomo.track(eventWithCategory: .snackbar, name: "deleteDraft") + self?.deleteDraftSnackBarAction(draft: draft, mailboxManager: mailboxManager) + }) + messagePresentable.show(message: MailResourcesStrings.Localizable.snackbarDraftSaved, action: messageAction) + return true + } + /// Check if once the Signature node is removed, we still have content func isDraftBodyEmptyOfChanges(_ body: String) throws -> Bool { guard !body.isEmpty else { @@ -170,22 +190,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) - let messageAction: MessageAction = (MailResourcesStrings.Localizable.actionDelete, { [weak self] in - self?.matomo.track(eventWithCategory: .snackbar, name: "deleteDraft") - self?.deleteDraftSnackBarAction(draft: draft, mailboxManager: mailboxManager) - }) - messagePresentable.show(message: MailResourcesStrings.Localizable.snackbarDraftSaved, action: messageAction) - } - private func refreshDraftFolder(latestSendDate: Date?, mailboxManager: MailboxManager) async throws { 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 f716ee9bd..496aabff2 100644 --- a/MailCore/Cache/MailboxManager.swift +++ b/MailCore/Cache/MailboxManager.swift @@ -1120,6 +1120,7 @@ public class MailboxManager: ObservableObject { return realm.objects(Draft.self).where { $0.action != nil } } + /// Where a new draft is saved in realm the first time if needed public func draft(partialDraft: Draft) async throws -> Draft? { guard let associatedMessage = getRealm().object(ofType: Message.self, forPrimaryKey: partialDraft.messageUid)?.freeze() else { return nil } diff --git a/MailShareExtension/ComposeMessageWrapperView.swift b/MailShareExtension/ComposeMessageWrapperView.swift index 3651bf9e0..5ef952bc8 100644 --- a/MailShareExtension/ComposeMessageWrapperView.swift +++ b/MailShareExtension/ComposeMessageWrapperView.swift @@ -23,22 +23,63 @@ import Social import SwiftUI import UIKit +// most definitely + +/// Represents `any` (ie. all of them not the type) curried closure, of arbitrary type. +typealias CurriedClosure = (Input) -> Output + +/// A closure that take no argument and return nothing, but technically curried. +typealias SimpleClosure = CurriedClosure + +/// Append a SimpleClosure closure to another one +func + (_ lhs: @escaping SimpleClosure, _ rhs: @escaping SimpleClosure) -> SimpleClosure { + let closure: SimpleClosure = { _ in + lhs(()) + rhs(()) + } + return closure +} + +// + struct ComposeMessageWrapperView: View { - @State var dismissHandler: () -> Void - @State var openAppHandler: () -> Void - @State var itemProviders: [NSItemProvider] - @State private var draft = Draft() + 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(initialValue: draft) + + // Append save draft action if possible + @InjectService var accountManager: AccountManager + if let mailboxManager = accountManager.currentMailboxManager { + let saveDraft: SimpleClosure = { _ in + let detached = draft.detached() + Task { + @InjectService var draftManager: DraftManager + _ = await draftManager.initialSaveRemotely(draft: detached, mailboxManager: mailboxManager) + } + } + self.dismissHandler = saveDraft + dismissHandler + } else { + 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) { - self.dismissHandler() + self.dismissHandler(()) } } else { - PleaseLoginView(openAppHandler: openAppHandler) + PleaseLoginView(tapHandler: dismissHandler) } } } @@ -46,7 +87,7 @@ struct ComposeMessageWrapperView: View { struct PleaseLoginView: View { @State var slide = Slide.onBoardingSlides.first! - var openAppHandler: () -> Void + var tapHandler: SimpleClosure var body: some View { VStack { @@ -62,7 +103,7 @@ struct PleaseLoginView: View { LottieView(configuration: slide.lottieConfiguration!) Spacer() }.onTapGesture { - openAppHandler() + tapHandler(()) } } } diff --git a/MailShareExtension/ShareViewController.swift b/MailShareExtension/ShareViewController.swift index d9857ece3..d4e44608a 100644 --- a/MailShareExtension/ShareViewController.swift +++ b/MailShareExtension/ShareViewController.swift @@ -67,19 +67,6 @@ final class ShareNavigationViewController: UIViewController { // We need to go threw wrapping to use SwiftUI in an NSExtension. let hostingController = UIHostingController(rootView: ComposeMessageWrapperView(dismissHandler: { self.dismiss(animated: true) - }, openAppHandler: { - guard let extensionContext = self.extensionContext, - let ikDeeplink = URL(string: "ikmail:shareExtension") else { - return - } - - Task { - // TODO: remove as does not work in share-ext - let result: Bool = await extensionContext.open(ikDeeplink) - // assert(result == true, "should success") - - self.dismiss(animated: true) - } }, itemProviders: itemProviders)) hostingController.view.translatesAutoresizingMaskIntoConstraints = false From de9131627cd63795d96cc7c7f2a375640aa5d005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 5 Jul 2023 12:42:54 +0200 Subject: [PATCH 17/61] feat: removed ikmail scheme core: reworked closure base type to be more abstact --- Mail/AppDelegate.swift | 3 +- Mail/Info.plist | 10 ----- .../ComposeMessageWrapperView.swift | 45 ++++++++++++++----- 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/Mail/AppDelegate.swift b/Mail/AppDelegate.swift index bb161b013..6edda7d6e 100644 --- a/Mail/AppDelegate.swift +++ b/Mail/AppDelegate.swift @@ -74,6 +74,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } + /// Validate deeplinks func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { @@ -82,7 +83,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { DDLogInfo("source application = \(sendingAppID ?? "Unknown")") let absoluteString = url.absoluteString - let success = (absoluteString.starts(with: "mailto") || absoluteString.starts(with: "mailto")) + let success = absoluteString.starts(with: "mailto") DDLogInfo("URL handling = \(success ? "success" : "failure")") return success } diff --git a/Mail/Info.plist b/Mail/Info.plist index 0927991f7..60010620c 100644 --- a/Mail/Info.plist +++ b/Mail/Info.plist @@ -36,16 +36,6 @@ mailto - - CFBundleTypeRole - Viewer - CFBundleURLName - com.infomaniak.mail.scheme - CFBundleURLSchemes - - ikmail - - CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/MailShareExtension/ComposeMessageWrapperView.swift b/MailShareExtension/ComposeMessageWrapperView.swift index 5ef952bc8..2c542a376 100644 --- a/MailShareExtension/ComposeMessageWrapperView.swift +++ b/MailShareExtension/ComposeMessageWrapperView.swift @@ -25,13 +25,38 @@ import UIKit // most definitely +/// Represents `any` (ie. all of them not the type) curried closure, of arbitrary type. +/// +/// Supports concurrency and error +typealias AsyncCurriedClosure = (Input) async throws -> Output + +/// Execute the closure without waiting, discarding result +postfix operator ~ +postfix func ~ (x: @escaping AsyncClosure) { + Task { + try? await x(()) + } +} + +/// A closure that take no argument and return nothing, but technically curried. +typealias AsyncClosure = AsyncCurriedClosure + +/// Append an AsyncClosure to another one +func + (_ lhs: @escaping AsyncClosure, _ rhs: @escaping AsyncClosure) -> AsyncClosure { + let closure: AsyncClosure = { _ in + try await lhs(()) + try await rhs(()) + } + return closure +} + /// Represents `any` (ie. all of them not the type) curried closure, of arbitrary type. typealias CurriedClosure = (Input) -> Output /// A closure that take no argument and return nothing, but technically curried. typealias SimpleClosure = CurriedClosure -/// Append a SimpleClosure closure to another one +/// Append a SimpleClosure to another one func + (_ lhs: @escaping SimpleClosure, _ rhs: @escaping SimpleClosure) -> SimpleClosure { let closure: SimpleClosure = { _ in lhs(()) @@ -44,24 +69,22 @@ func + (_ lhs: @escaping SimpleClosure, _ rhs: @escaping SimpleClosure) -> Simpl struct ComposeMessageWrapperView: View { private var itemProviders: [NSItemProvider] - private var dismissHandler: SimpleClosure + private var dismissHandler: AsyncClosure @State private var draft: Draft @LazyInjectService private var accountManager: AccountManager - init(dismissHandler: @escaping SimpleClosure, itemProviders: [NSItemProvider], draft: Draft = Draft()) { + init(dismissHandler: @escaping AsyncClosure, itemProviders: [NSItemProvider], draft: Draft = Draft()) { _draft = State(initialValue: draft) // Append save draft action if possible @InjectService var accountManager: AccountManager if let mailboxManager = accountManager.currentMailboxManager { - let saveDraft: SimpleClosure = { _ in + let saveDraft: AsyncClosure = { _ in let detached = draft.detached() - Task { - @InjectService var draftManager: DraftManager - _ = await draftManager.initialSaveRemotely(draft: detached, mailboxManager: mailboxManager) - } + @InjectService var draftManager: DraftManager + _ = await draftManager.initialSaveRemotely(draft: detached, mailboxManager: mailboxManager) } self.dismissHandler = saveDraft + dismissHandler } else { @@ -76,7 +99,7 @@ struct ComposeMessageWrapperView: View { ComposeMessageView.newMessage(draft, mailboxManager: mailboxManager, itemProviders: itemProviders) .environmentObject(mailboxManager) .environment(\.dismissModal) { - self.dismissHandler(()) + dismissHandler~ } } else { PleaseLoginView(tapHandler: dismissHandler) @@ -87,7 +110,7 @@ struct ComposeMessageWrapperView: View { struct PleaseLoginView: View { @State var slide = Slide.onBoardingSlides.first! - var tapHandler: SimpleClosure + var tapHandler: AsyncClosure var body: some View { VStack { @@ -103,7 +126,7 @@ struct PleaseLoginView: View { LottieView(configuration: slide.lottieConfiguration!) Spacer() }.onTapGesture { - tapHandler(()) + tapHandler~ } } } From bd4fced87e21f2b598b9d1edbab059f262f14926 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 5 Jul 2023 13:50:44 +0200 Subject: [PATCH 18/61] fix(shareExt): Realm issue --- MailCore/Cache/DraftManager.swift | 1 - MailShareExtension/ComposeMessageWrapperView.swift | 8 +++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/MailCore/Cache/DraftManager.swift b/MailCore/Cache/DraftManager.swift index 2503d6eb6..dcee5d934 100644 --- a/MailCore/Cache/DraftManager.swift +++ b/MailCore/Cache/DraftManager.swift @@ -154,7 +154,6 @@ public final class DraftManager { /// First save of a draft with the remote if needed @discardableResult public func initialSaveRemotely(draft: Draft, mailboxManager: MailboxManager) async -> Bool { - print("initialSaveRemotely:\(draft.localUUID)") // 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 { diff --git a/MailShareExtension/ComposeMessageWrapperView.swift b/MailShareExtension/ComposeMessageWrapperView.swift index 2c542a376..593b4ebd4 100644 --- a/MailShareExtension/ComposeMessageWrapperView.swift +++ b/MailShareExtension/ComposeMessageWrapperView.swift @@ -32,7 +32,7 @@ typealias AsyncCurriedClosure = (Input) async throws -> Output /// Execute the closure without waiting, discarding result postfix operator ~ -postfix func ~ (x: @escaping AsyncClosure) { +postfix func ~ (x: @escaping AsyncCurriedClosure) { Task { try? await x(()) } @@ -51,6 +51,8 @@ func + (_ lhs: @escaping AsyncClosure, _ rhs: @escaping AsyncClosure) -> AsyncCl } /// Represents `any` (ie. all of them not the type) curried closure, of arbitrary type. +/// +/// see `AsyncCurriedClosure` if you need support for structured concurrency or error feedback typealias CurriedClosure = (Input) -> Output /// A closure that take no argument and return nothing, but technically curried. @@ -81,9 +83,9 @@ struct ComposeMessageWrapperView: View { // Append save draft action if possible @InjectService var accountManager: AccountManager if let mailboxManager = accountManager.currentMailboxManager { - let saveDraft: AsyncClosure = { _ in - let detached = draft.detached() + let saveDraft: AsyncClosure = { @MainActor _ in @InjectService var draftManager: DraftManager + let detached = draft.detached() _ = await draftManager.initialSaveRemotely(draft: detached, mailboxManager: mailboxManager) } self.dismissHandler = saveDraft + dismissHandler From a8df84ea6452006989f50074b1972e38ac99853d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 5 Jul 2023 17:21:49 +0200 Subject: [PATCH 19/61] fix(draft): draft is not empty if has attachments fix(attachments): cap max number of attachments to keep API happy --- .../Attachments/AttachmentsManager.swift | 12 +++++++-- .../New Message/ComposeMessageBodyView.swift | 6 ++--- .../New Message/ComposeMessageView.swift | 2 +- MailCore/Cache/DraftManager.swift | 25 ++++++++++++++++--- MailCore/Models/Draft.swift | 10 ++++++++ 5 files changed, 45 insertions(+), 10 deletions(-) diff --git a/Mail/Views/New Message/Attachments/AttachmentsManager.swift b/Mail/Views/New Message/Attachments/AttachmentsManager.swift index ca89aa2ee..f47683bc4 100644 --- a/Mail/Views/New Message/Attachments/AttachmentsManager.swift +++ b/Mail/Views/New Message/Attachments/AttachmentsManager.swift @@ -22,7 +22,7 @@ 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? @@ -157,7 +157,15 @@ class AttachmentsManager: ObservableObject { return newAttachment } - func importAttachments(attachments: [Attachable], disposition: AttachmentDisposition = .attachment) { + 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 attachments = attachments[0 ... draft.availableAttachmentsSlots] + + // TODO: use ParallelTaskMapper for performance here. for attachment in attachments { Task { let cid = await importAttachment(attachment: attachment, disposition: disposition) diff --git a/Mail/Views/New Message/ComposeMessageBodyView.swift b/Mail/Views/New Message/ComposeMessageBodyView.swift index 281265938..e405b6f84 100644 --- a/Mail/Views/New Message/ComposeMessageBodyView.swift +++ b/Mail/Views/New Message/ComposeMessageBodyView.swift @@ -91,19 +91,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.swift b/Mail/Views/New Message/ComposeMessageView.swift index 4ac9aa3bd..bfb57debd 100644 --- a/Mail/Views/New Message/ComposeMessageView.swift +++ b/Mail/Views/New Message/ComposeMessageView.swift @@ -117,7 +117,7 @@ struct ComposeMessageView: View { composeMessage } .onAppear { - attachmentsManager.importAttachments(attachments: initialAttachments) + attachmentsManager.importAttachments(attachments: initialAttachments, draft: draft) initialAttachments = [] } .interactiveDismissDisabled() diff --git a/MailCore/Cache/DraftManager.swift b/MailCore/Cache/DraftManager.swift index dcee5d934..397c37383 100644 --- a/MailCore/Cache/DraftManager.swift +++ b/MailCore/Cache/DraftManager.swift @@ -154,9 +154,7 @@ public final class DraftManager { /// First save of a draft with the remote if needed @discardableResult public func initialSaveRemotely(draft: Draft, mailboxManager: MailboxManager) async -> Bool { - // 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 { + guard !isDraftEmpty(draft: draft) else { deleteEmptyDraft(draft: draft, for: mailboxManager) return false } @@ -170,8 +168,27 @@ public final class DraftManager { 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 { + private func isDraftBodyEmptyOfChanges(_ body: String) throws -> Bool { guard !body.isEmpty else { return true } diff --git a/MailCore/Models/Draft.swift b/MailCore/Models/Draft.swift index 2dcab7922..8d90876c9 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 + } +} From d995a4fa57c74b0009d398ce40ebf10def6107cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 6 Jul 2023 12:47:08 +0200 Subject: [PATCH 20/61] chore: SonarCloud feedback --- Mail/Views/Bottom sheets/ContactActionsView.swift | 2 +- Mail/Views/LockedAppView.swift | 8 ++++++-- MailShareExtension/ComposeMessageWrapperView.swift | 4 ++-- MailShareExtension/Proxy/CacheManager.swift | 4 +++- MailShareExtension/Proxy/OrientationManager.swift | 4 +++- .../Proxy/RemoteNotificationRegistrer.swift | 4 +++- MailShareExtension/Proxy/RootViewManager.swift | 4 +++- MailShareExtension/Proxy/URLNavigator.swift | 9 +++++++-- 8 files changed, 28 insertions(+), 11 deletions(-) diff --git a/Mail/Views/Bottom sheets/ContactActionsView.swift b/Mail/Views/Bottom sheets/ContactActionsView.swift index 2c2855afe..24946452f 100644 --- a/Mail/Views/Bottom sheets/ContactActionsView.swift +++ b/Mail/Views/Bottom sheets/ContactActionsView.swift @@ -28,7 +28,6 @@ struct ContactActionsView: View { @Environment(\.dismiss) var dismiss @LazyInjectService private var matomo: MatomoUtils - @LazyInjectService private var accountManager: AccountManager @LazyInjectService private var snackbarPresenter: SnackBarPresentable @State private var writtenToRecipient: Recipient? @@ -132,6 +131,7 @@ struct ContactActionsView: View { private func addToContacts() { Task { await tryOrDisplayError { + @InjectService var accountManager: AccountManager try await accountManager.currentContactManager?.addContact(recipient: recipient) snackbarPresenter.show(message: MailResourcesStrings.Localizable.snackbarContactSaved) } diff --git a/Mail/Views/LockedAppView.swift b/Mail/Views/LockedAppView.swift index 9e471c78d..f19138c43 100644 --- a/Mail/Views/LockedAppView.swift +++ b/Mail/Views/LockedAppView.swift @@ -60,9 +60,13 @@ struct LockedAppView: View { private func unlockApp() { Task { - if (try? await appLockHelper.evaluatePolicy(reason: MailResourcesStrings.Localizable.lockAppTitle)) == true { - await (window?.windowScene?.delegate as? SceneDelegate)?.showMainView() + guard let policyEvaluation = try? await appLockHelper + .evaluatePolicy(reason: MailResourcesStrings.Localizable.lockAppTitle), + policyEvaluation else { + return } + + await (window?.windowScene?.delegate as? SceneDelegate)?.showMainView() } } } diff --git a/MailShareExtension/ComposeMessageWrapperView.swift b/MailShareExtension/ComposeMessageWrapperView.swift index 593b4ebd4..2ecf5c59d 100644 --- a/MailShareExtension/ComposeMessageWrapperView.swift +++ b/MailShareExtension/ComposeMessageWrapperView.swift @@ -81,8 +81,8 @@ struct ComposeMessageWrapperView: View { _draft = State(initialValue: draft) // Append save draft action if possible - @InjectService var accountManager: AccountManager - if let mailboxManager = accountManager.currentMailboxManager { + @InjectService var manager: AccountManager + if let mailboxManager = manager.currentMailboxManager { let saveDraft: AsyncClosure = { @MainActor _ in @InjectService var draftManager: DraftManager let detached = draft.detached() diff --git a/MailShareExtension/Proxy/CacheManager.swift b/MailShareExtension/Proxy/CacheManager.swift index cfdcf9aa0..49f24d499 100644 --- a/MailShareExtension/Proxy/CacheManager.swift +++ b/MailShareExtension/Proxy/CacheManager.swift @@ -21,5 +21,7 @@ import UIKit /// A cache manager that works in Extension mode public final class CacheManager: CacheManageable { - public func refreshCacheData() {} + public func refreshCacheData() { + // NOOP in shareExtension + } } diff --git a/MailShareExtension/Proxy/OrientationManager.swift b/MailShareExtension/Proxy/OrientationManager.swift index 0e243cc4f..0c315b132 100644 --- a/MailShareExtension/Proxy/OrientationManager.swift +++ b/MailShareExtension/Proxy/OrientationManager.swift @@ -24,7 +24,9 @@ import UIKit public final class OrientationManager: OrientationManageable { public var orientationLock = UIInterfaceOrientationMask.all - public func setOrientationLock(_ orientation: UIInterfaceOrientationMask) {} + 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 index 5213e2788..c3ecc243e 100644 --- a/MailShareExtension/Proxy/RemoteNotificationRegistrer.swift +++ b/MailShareExtension/Proxy/RemoteNotificationRegistrer.swift @@ -21,5 +21,7 @@ import UIKit /// A RemoteNotificationRegistrer that works in Extension mode public final class RemoteNotificationRegistrer: RemoteNotificationRegistrable { - public func register() {} + public func register() { + // NOOP in share extension + } } diff --git a/MailShareExtension/Proxy/RootViewManager.swift b/MailShareExtension/Proxy/RootViewManager.swift index 88e2656c8..ccf975aff 100644 --- a/MailShareExtension/Proxy/RootViewManager.swift +++ b/MailShareExtension/Proxy/RootViewManager.swift @@ -30,5 +30,7 @@ public struct RootViewManager: RootViewManageable { nil } - public func updateAllWindowUI() {} + public func updateAllWindowUI() { + // NOOP in share extension + } } diff --git a/MailShareExtension/Proxy/URLNavigator.swift b/MailShareExtension/Proxy/URLNavigator.swift index 1c15618a1..edfd7f3c8 100644 --- a/MailShareExtension/Proxy/URLNavigator.swift +++ b/MailShareExtension/Proxy/URLNavigator.swift @@ -21,6 +21,11 @@ import UIKit /// An URLNavigator that works in Extension mode public struct URLNavigator: URLNavigable { - public func openUrl(_ url: URL) {} - public func openUrlIfPossible(_ url: URL) {} + public func openUrl(_ url: URL) { + // NOOP in share extension + } + + public func openUrlIfPossible(_ url: URL) { + // NOOP in share extension + } } From 2f67324ce76216362746ff56f1c2c3791fb9bc00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 7 Jul 2023 11:02:37 +0200 Subject: [PATCH 21/61] fix(attachments): performance and scallability issues --- .../Attachments/AttachmentsManager.swift | 193 ++++++++++++++---- MailCore/Cache/BackgroundRealm.swift | 4 +- 2 files changed, 154 insertions(+), 43 deletions(-) diff --git a/Mail/Views/New Message/Attachments/AttachmentsManager.swift b/Mail/Views/New Message/Attachments/AttachmentsManager.swift index df31b74db..c10a5cfd2 100644 --- a/Mail/Views/New Message/Attachments/AttachmentsManager.swift +++ b/Mail/Views/New Message/Attachments/AttachmentsManager.swift @@ -17,7 +17,9 @@ */ import CocoaLumberjackSwift +import Combine import Foundation +import InfomaniakCore import MailCore import PhotosUI import SwiftUI @@ -27,9 +29,64 @@ public extension Array { subscript(safe range: Range) -> ArraySlice { return self[Swift.min(range.startIndex, endIndex) ..< Swift.min(range.endIndex, endIndex)] } +} + +/// A thread safe Dictionary wrapper that does not require `await`. Conforms to Sendable. +/// +/// Useful when dealing with UI. +public final class SendableDictionary: @unchecked Sendable { + let lock = DispatchQueue(label: "com.infomaniak.core.SendableDictionary.lock") + private(set) var content = [T: U]() - subscript(safe range: ClosedRange) -> ArraySlice { - return self[Swift.min(range.lowerBound, endIndex) ..< Swift.min(range.upperBound, endIndex)] + public init() { + // META: keep SonarCloud happy + } + + public var values: Dictionary.Values { + var buffer: Dictionary.Values! + lock.sync { + buffer = content.values + } + return buffer + } + + public func value(for key: T) -> U? { + var buffer: U? + lock.sync { + buffer = content[key] + } + return buffer + } + + public func setValue(_ value: U?, for key: T) { + lock.sync { + content[key] = value + } + } + + public func removeValue(forKey key: T) { + lock.sync { + content.removeValue(forKey: key) + } + } + + public subscript(_ key: T) -> U? { + get { + value(for: key) + } + set { + setValue(newValue, for: key) + } + } +} + +public extension ParallelTaskMapper { + // TODO: move to core and change implem to work with ArraySlice by default + func map( + slice: ArraySlice, + toOperation operation: @escaping @Sendable (_ item: Input) async throws -> Output? + ) async throws -> [Output?] { + try await map(collection: Array(slice), toOperation: operation) } } @@ -48,11 +105,19 @@ final class AttachmentUploadTask: 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) } @@ -68,6 +133,14 @@ final 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() { @@ -75,26 +148,39 @@ final 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 @@ -130,26 +216,50 @@ final 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?) { @@ -160,10 +270,9 @@ final 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, @@ -172,7 +281,7 @@ final class AttachmentsManager: ObservableObject { size: 0, name: name, disposition: disposition) - let savedAttachment = addLocalAttachment(attachment: attachment) + let savedAttachment = await addLocalAttachment(attachment: attachment) return savedAttachment } @@ -191,7 +300,7 @@ final class AttachmentsManager: ObservableObject { name: updatedName, disposition: attachment.disposition) - updateAttachment(oldAttachment: attachment, newAttachment: newAttachment) + await updateAttachment(oldAttachment: attachment, newAttachment: newAttachment) return newAttachment } @@ -201,21 +310,23 @@ final class AttachmentsManager: ObservableObject { } // Cap max number of attachments, API errors out at 100 - let attachments = attachments[safe: 0 ... draft.availableAttachmentsSlots] + let attachmentsSlice = attachments[safe: 0 ..< draft.availableAttachmentsSlots] - // TODO: use ParallelTaskMapper for performance here. - for attachment in attachments { - Task { - let cid = await importAttachment(attachment: attachment, disposition: disposition) + Task.detached { + try? await self.parallelTaskMapper.map(slice: 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() @@ -273,7 +384,7 @@ final 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/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") } From bff2a6b7bb782a45655de85f7d99746bc2e7577a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 12 Jul 2023 18:00:22 +0200 Subject: [PATCH 22/61] chore: bump Core lib --- .package.resolved | 2 +- Project.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.package.resolved b/.package.resolved index f279061b6..47ccb531d 100644 --- a/.package.resolved +++ b/.package.resolved @@ -41,7 +41,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Infomaniak/ios-core", "state" : { - "revision" : "ef2811a288a3a4b94dd5d04c5f47ddd771e4176c" + "revision" : "75907fd7c13b0478969d7400f07749f0ce8cf1a8" } }, { diff --git a/Project.swift b/Project.swift index 4538efd8a..4450f62f8 100644 --- a/Project.swift +++ b/Project.swift @@ -24,7 +24,7 @@ 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("ef2811a288a3a4b94dd5d04c5f47ddd771e4176c")), + .package(url: "https://github.com/Infomaniak/ios-core", .revision("75907fd7c13b0478969d7400f07749f0ce8cf1a8")), .package(url: "https://github.com/Infomaniak/ios-core-ui", .upToNextMajor(from: "2.3.0")), .package(url: "https://github.com/Infomaniak/ios-notifications", .upToNextMajor(from: "2.1.0")), .package(url: "https://github.com/Infomaniak/ios-create-account", .upToNextMajor(from: "1.1.0")), From bf0d4df9fba04bc75176b6c5d2208cbf9ac8e427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 13 Jul 2023 07:38:42 +0200 Subject: [PATCH 23/61] feat(share_ext): hooking into Core lib components --- .../Attachments/AttachmentsManager.swift | 70 +------------------ .../ComposeMessageWrapperView.swift | 65 +++-------------- 2 files changed, 12 insertions(+), 123 deletions(-) diff --git a/Mail/Views/New Message/Attachments/AttachmentsManager.swift b/Mail/Views/New Message/Attachments/AttachmentsManager.swift index c10a5cfd2..28204bcb4 100644 --- a/Mail/Views/New Message/Attachments/AttachmentsManager.swift +++ b/Mail/Views/New Message/Attachments/AttachmentsManager.swift @@ -24,74 +24,6 @@ import MailCore import PhotosUI import SwiftUI -// + Tests -public extension Array { - subscript(safe range: Range) -> ArraySlice { - return self[Swift.min(range.startIndex, endIndex) ..< Swift.min(range.endIndex, endIndex)] - } -} - -/// A thread safe Dictionary wrapper that does not require `await`. Conforms to Sendable. -/// -/// Useful when dealing with UI. -public final class SendableDictionary: @unchecked Sendable { - let lock = DispatchQueue(label: "com.infomaniak.core.SendableDictionary.lock") - private(set) var content = [T: U]() - - public init() { - // META: keep SonarCloud happy - } - - public var values: Dictionary.Values { - var buffer: Dictionary.Values! - lock.sync { - buffer = content.values - } - return buffer - } - - public func value(for key: T) -> U? { - var buffer: U? - lock.sync { - buffer = content[key] - } - return buffer - } - - public func setValue(_ value: U?, for key: T) { - lock.sync { - content[key] = value - } - } - - public func removeValue(forKey key: T) { - lock.sync { - content.removeValue(forKey: key) - } - } - - public subscript(_ key: T) -> U? { - get { - value(for: key) - } - set { - setValue(newValue, for: key) - } - } -} - -public extension ParallelTaskMapper { - // TODO: move to core and change implem to work with ArraySlice by default - func map( - slice: ArraySlice, - toOperation operation: @escaping @Sendable (_ item: Input) async throws -> Output? - ) async throws -> [Output?] { - try await map(collection: Array(slice), toOperation: operation) - } -} - -// - final class AttachmentUploadTask: ObservableObject { @Published var progress: Double = 0 var task: Task? @@ -313,7 +245,7 @@ final class AttachmentsManager: ObservableObject { let attachmentsSlice = attachments[safe: 0 ..< draft.availableAttachmentsSlots] Task.detached { - try? await self.parallelTaskMapper.map(slice: attachmentsSlice) { attachment in + try? await self.parallelTaskMapper.map(collection: attachmentsSlice) { attachment in _ = await self.importAttachment(attachment: attachment, disposition: disposition) // TODO: - Manage inline attachment } diff --git a/MailShareExtension/ComposeMessageWrapperView.swift b/MailShareExtension/ComposeMessageWrapperView.swift index 2ecf5c59d..d0df5fb31 100644 --- a/MailShareExtension/ComposeMessageWrapperView.swift +++ b/MailShareExtension/ComposeMessageWrapperView.swift @@ -16,6 +16,7 @@ along with this program. If not, see . */ +import InfomaniakCore import InfomaniakCoreUI import InfomaniakDI import MailCore @@ -23,70 +24,26 @@ import Social import SwiftUI import UIKit -// most definitely - -/// Represents `any` (ie. all of them not the type) curried closure, of arbitrary type. -/// -/// Supports concurrency and error -typealias AsyncCurriedClosure = (Input) async throws -> Output - -/// Execute the closure without waiting, discarding result -postfix operator ~ -postfix func ~ (x: @escaping AsyncCurriedClosure) { - Task { - try? await x(()) - } -} - -/// A closure that take no argument and return nothing, but technically curried. -typealias AsyncClosure = AsyncCurriedClosure - -/// Append an AsyncClosure to another one -func + (_ lhs: @escaping AsyncClosure, _ rhs: @escaping AsyncClosure) -> AsyncClosure { - let closure: AsyncClosure = { _ in - try await lhs(()) - try await rhs(()) - } - return closure -} - -/// Represents `any` (ie. all of them not the type) curried closure, of arbitrary type. -/// -/// see `AsyncCurriedClosure` if you need support for structured concurrency or error feedback -typealias CurriedClosure = (Input) -> Output - -/// A closure that take no argument and return nothing, but technically curried. -typealias SimpleClosure = CurriedClosure - -/// Append a SimpleClosure to another one -func + (_ lhs: @escaping SimpleClosure, _ rhs: @escaping SimpleClosure) -> SimpleClosure { - let closure: SimpleClosure = { _ in - lhs(()) - rhs(()) - } - return closure -} - -// - struct ComposeMessageWrapperView: View { private var itemProviders: [NSItemProvider] - private var dismissHandler: AsyncClosure + private var dismissHandler: SimpleClosure @State private var draft: Draft @LazyInjectService private var accountManager: AccountManager - init(dismissHandler: @escaping AsyncClosure, itemProviders: [NSItemProvider], draft: Draft = Draft()) { + init(dismissHandler: @escaping SimpleClosure, itemProviders: [NSItemProvider], draft: Draft = Draft()) { _draft = State(initialValue: draft) // Append save draft action if possible @InjectService var manager: AccountManager if let mailboxManager = manager.currentMailboxManager { - let saveDraft: AsyncClosure = { @MainActor _ in - @InjectService var draftManager: DraftManager + let saveDraft: SimpleClosure = { _ in let detached = draft.detached() - _ = await draftManager.initialSaveRemotely(draft: detached, mailboxManager: mailboxManager) + Task { + @InjectService var draftManager: DraftManager + _ = await draftManager.initialSaveRemotely(draft: detached, mailboxManager: mailboxManager) + } } self.dismissHandler = saveDraft + dismissHandler } else { @@ -101,7 +58,7 @@ struct ComposeMessageWrapperView: View { ComposeMessageView.newMessage(draft, mailboxManager: mailboxManager, itemProviders: itemProviders) .environmentObject(mailboxManager) .environment(\.dismissModal) { - dismissHandler~ + dismissHandler(()) } } else { PleaseLoginView(tapHandler: dismissHandler) @@ -112,7 +69,7 @@ struct ComposeMessageWrapperView: View { struct PleaseLoginView: View { @State var slide = Slide.onBoardingSlides.first! - var tapHandler: AsyncClosure + var tapHandler: SimpleClosure var body: some View { VStack { @@ -128,7 +85,7 @@ struct PleaseLoginView: View { LottieView(configuration: slide.lottieConfiguration!) Spacer() }.onTapGesture { - tapHandler~ + tapHandler(()) } } } From a7aa19d7c5fc1b6d74b38a7ddf0f597023a92ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 13 Jul 2023 14:55:18 +0200 Subject: [PATCH 24/61] chore: Post merge cleanup --- .../New Message/ComposeMessageView.swift | 5 ++-- Mail/Views/Onboarding/OnboardingView.swift | 5 +--- Mail/Views/Switch User/AccountView.swift | 8 +----- .../{ => SnackBar}/IKSnackBar+Extension.swift | 14 +++++----- .../{ => SnackBar}/SnackBarPresentable.swift | 26 +------------------ 5 files changed, 14 insertions(+), 44 deletions(-) rename MailCore/Utils/{ => SnackBar}/IKSnackBar+Extension.swift (91%) rename MailCore/Utils/{ => SnackBar}/SnackBarPresentable.swift (77%) diff --git a/Mail/Views/New Message/ComposeMessageView.swift b/Mail/Views/New Message/ComposeMessageView.swift index bf8570e17..41929cb01 100644 --- a/Mail/Views/New Message/ComposeMessageView.swift +++ b/Mail/Views/New Message/ComposeMessageView.swift @@ -63,7 +63,8 @@ struct ComposeMessageView: View { @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? @@ -130,7 +131,7 @@ 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() } } diff --git a/Mail/Views/Onboarding/OnboardingView.swift b/Mail/Views/Onboarding/OnboardingView.swift index 34b99cac1..14b6eaafe 100644 --- a/Mail/Views/Onboarding/OnboardingView.swift +++ b/Mail/Views/Onboarding/OnboardingView.swift @@ -130,10 +130,7 @@ final class LoginHandler: InfomaniakLoginDelegate, ObservableObject { Task { do { _ = try await accountManager.createAndSetCurrentAccount(code: code, codeVerifier: verifier) - - // TODO fix UIApplication -// UIApplication.shared.registerForRemoteNotifications() - + remoteNotificationRegistrer.register() } catch let error as MailError where error == MailError.noMailbox { shouldShowEmptyMailboxesView = true } catch { diff --git a/Mail/Views/Switch User/AccountView.swift b/Mail/Views/Switch User/AccountView.swift index 177258164..1eb83b551 100644 --- a/Mail/Views/Switch User/AccountView.swift +++ b/Mail/Views/Switch User/AccountView.swift @@ -35,7 +35,7 @@ final class AccountViewDelegate: DeleteAccountDelegate { accountManager.removeTokenAndAccount(token: account.token) if let nextAccount = accountManager.accounts.first { accountManager.switchAccount(newAccount: nextAccount) - IKSnackBar.showSnackBar(message: "Account deleted") + snackbarPresenter.show(message: "Account deleted") } accountManager.saveAccounts() @@ -56,12 +56,6 @@ struct AccountView: View { @LazyInjectService private var matomo: MatomoUtils @LazyInjectService private var accountManager: AccountManager - // TODO remove -// private let account: Account = { -// let accountManager = LazyInjectService().wrappedValue -// return accountManager.currentAccount -// }() - @State private var isShowingLogoutAlert = false @State private var isShowingDeleteAccount = false @State private var delegate = AccountViewDelegate() diff --git a/MailCore/Utils/IKSnackBar+Extension.swift b/MailCore/Utils/SnackBar/IKSnackBar+Extension.swift similarity index 91% rename from MailCore/Utils/IKSnackBar+Extension.swift rename to MailCore/Utils/SnackBar/IKSnackBar+Extension.swift index 1ecd86376..ecd9dbce8 100644 --- a/MailCore/Utils/IKSnackBar+Extension.swift +++ b/MailCore/Utils/SnackBar/IKSnackBar+Extension.swift @@ -41,7 +41,8 @@ public extension SnackBarStyle { } } -public class SnackBarAvoider { +// TODO: delete +public final class SnackBarAvoider { public var snackBarInset: CGFloat = 0 public init() { /* Needed to init */ } @@ -60,7 +61,7 @@ public class SnackBarAvoider { public extension IKSnackBar { @discardableResult @MainActor - /// Call this method to display a snackbar + /// Call this method to display a `SnackBar` /// - Parameters: /// - message: The message to display /// - duration: The time the message should be displayed @@ -68,7 +69,7 @@ public extension IKSnackBar { /// - 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 showSnackBar( + static func showMailSnackBar( message: String, duration: SnackBar.Duration = .lengthLong, action: IKSnackBar.Action? = nil, @@ -115,10 +116,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 @@ -127,11 +129,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/SnackBarPresentable.swift b/MailCore/Utils/SnackBar/SnackBarPresentable.swift similarity index 77% rename from MailCore/Utils/SnackBarPresentable.swift rename to MailCore/Utils/SnackBar/SnackBarPresentable.swift index c32b1f82b..fe4fb64a8 100644 --- a/MailCore/Utils/SnackBarPresentable.swift +++ b/MailCore/Utils/SnackBar/SnackBarPresentable.swift @@ -21,30 +21,6 @@ import InfomaniakCoreUI import SnackBar import UIKit -// TODO: move to core UI -// TODO: Use our type not the lib -public extension IKSnackBar { - enum Duration: Equatable { - case lengthLong - case lengthShort - case infinite - case custom(CGFloat) - - var value: CGFloat { - switch self { - case .lengthLong: - return 3.5 - case .lengthShort: - return 2 - case .infinite: - return -1 - case .custom(let duration): - return duration - } - } - } -} - public protocol SnackBarPresentable { func show(message: String) func show(message: String, action: IKSnackBar.Action?) @@ -81,7 +57,7 @@ public final class SnackBarPresenter: SnackBarPresentable { contextView: UIView? = nil ) { Task { @MainActor in - IKSnackBar.showSnackBar( + IKSnackBar.showMailSnackBar( message: message, duration: duration, action: action, From da6778a4e42cd2a5b66eb270ebd398173c60c209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 13 Jul 2023 15:55:20 +0200 Subject: [PATCH 25/61] feat(catalyst): quickwin use iPadLandscape for mac catalyst --- Mail/Helpers/AppAssembly.swift | 3 ++ Mail/Views/SplitView.swift | 3 +- MailCore/Utils/CatalystDetectable.swift | 52 +++++++++++++++++++ .../Utils/SnackBar/SnackBarPresentable.swift | 1 + 4 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 MailCore/Utils/CatalystDetectable.swift diff --git a/Mail/Helpers/AppAssembly.swift b/Mail/Helpers/AppAssembly.swift index 2c6f237e8..078c78db4 100644 --- a/Mail/Helpers/AppAssembly.swift +++ b/Mail/Helpers/AppAssembly.swift @@ -85,6 +85,9 @@ enum ApplicationAssembly { }, Factory(type: UserActivityController.self) { _, _ in UserActivityController() + }, + Factory(type: PlatformDetectable.self) { _, _ in + PlatformDetector() } ] diff --git a/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift index a01f42d6b..d0c8d230b 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -54,6 +54,7 @@ struct SplitView: View { @LazyInjectService private var orientationManager: OrientationManageable @LazyInjectService private var snackbarPresenter: SnackBarPresentable + @LazyInjectService private var platformDetector: PlatformDetectable let mailboxManager: MailboxManager @@ -150,7 +151,7 @@ struct SplitView: View { } private func setupBehaviour(orientation: UIInterfaceOrientation) { - if orientation.isLandscape { + if orientation.isLandscape || platformDetector.isMacCatalyst { splitViewController?.preferredSplitBehavior = .displace splitViewController?.preferredDisplayMode = splitViewManager.selectedFolder == nil ? .twoDisplaceSecondary diff --git a/MailCore/Utils/CatalystDetectable.swift b/MailCore/Utils/CatalystDetectable.swift new file mode 100644 index 000000000..ad5a1efe0 --- /dev/null +++ b/MailCore/Utils/CatalystDetectable.swift @@ -0,0 +1,52 @@ +/* + 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 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 isInExtension: Bool = { + guard Bundle.main.bundlePath.hasSuffix(".appex") else { + return false + } + + return true + }() +} diff --git a/MailCore/Utils/SnackBar/SnackBarPresentable.swift b/MailCore/Utils/SnackBar/SnackBarPresentable.swift index fe4fb64a8..77010e85b 100644 --- a/MailCore/Utils/SnackBar/SnackBarPresentable.swift +++ b/MailCore/Utils/SnackBar/SnackBarPresentable.swift @@ -21,6 +21,7 @@ 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?) From 9feba22f2bbb187777e81a091915ac26075528a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 14 Jul 2023 17:00:59 +0200 Subject: [PATCH 26/61] fix(NotificationExtension): Fix missing DI in notification extension refactor(NSItemProvider): Sharing code between kDrive and ikMail (while using a Progress and a Result<> as discussed with Ph) feat(ShareExtension): Now ikMail has the same support for file sharing as kDrive chore(ShareExtension): BundleID is now com.infomaniak.mail.ShareExtension --- Mail/Helpers/AppAssembly.swift | 14 +- .../New Message/Attachments/Attachable.swift | 52 +++--- .../Attachments/AttachmentsManager.swift | 3 +- .../ItemProviderFileRepresentation.swift | 110 +++++++++++++ .../ItemProviderTextRepresentation.swift | 150 ++++++++++++++++++ .../ItemProviderWeblocRepresentation.swift | 114 +++++++++++++ .../ItemProviderZipRepresentation.swift | 128 +++++++++++++++ .../TO_CORE/ProgressResultable.swift | 41 +++++ .../Attachments/TO_CORE/URL+Extension.swift | 59 +++++++ .../ItemProviderUIImageRepresentation.swift | 132 +++++++++++++++ MailCore/Cache/MailboxManager.swift | 28 +--- .../NotificationService.swift | 3 + .../NotificationServiceAssembly.swift | 69 ++++++++ Project.swift | 3 +- 14 files changed, 863 insertions(+), 43 deletions(-) create mode 100644 Mail/Views/New Message/Attachments/TO_CORE/ItemProviderFileRepresentation.swift create mode 100644 Mail/Views/New Message/Attachments/TO_CORE/ItemProviderTextRepresentation.swift create mode 100644 Mail/Views/New Message/Attachments/TO_CORE/ItemProviderWeblocRepresentation.swift create mode 100644 Mail/Views/New Message/Attachments/TO_CORE/ItemProviderZipRepresentation.swift create mode 100644 Mail/Views/New Message/Attachments/TO_CORE/ProgressResultable.swift create mode 100644 Mail/Views/New Message/Attachments/TO_CORE/URL+Extension.swift create mode 100644 Mail/Views/New Message/Attachments/TO_CORE_UI/ItemProviderUIImageRepresentation.swift create mode 100644 MailNotificationServiceExtension/NotificationServiceAssembly.swift diff --git a/Mail/Helpers/AppAssembly.swift b/Mail/Helpers/AppAssembly.swift index 078c78db4..4f4235467 100644 --- a/Mail/Helpers/AppAssembly.swift +++ b/Mail/Helpers/AppAssembly.swift @@ -26,6 +26,9 @@ import InfomaniakNotifications import MailCore import os.log +private let realmRootPath = "mailboxes" +private let appGroupIdentifier = "group.com.infomaniak.mail" + extension Array where Element == Factory { func registerFactoriesInDI() { forEach { SimpleResolver.sharedResolver.store(factory: $0) } @@ -88,6 +91,16 @@ enum ApplicationAssembly { }, 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 } ] @@ -125,4 +138,3 @@ public struct EarlyDIHook { ApplicationAssembly.setupDI() } } - diff --git a/Mail/Views/New Message/Attachments/Attachable.swift b/Mail/Views/New Message/Attachments/Attachable.swift index 09420d564..687cd8c9e 100644 --- a/Mail/Views/New Message/Attachments/Attachable.swift +++ b/Mail/Views/New Message/Attachments/Attachable.swift @@ -16,7 +16,9 @@ along with this program. If not, see . */ +import Combine import Foundation +import InfomaniakCore import MailCore import PhotosUI import UniformTypeIdentifiers @@ -28,6 +30,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 +44,33 @@ extension NSItemProvider: Attachable { } func writeToTemporaryURL() async throws -> URL { - return try await loadFileRepresentation(typeIdentifier: preferredIdentifier) - } + switch underlyingType { + case .isURL: + let getPlist = try ItemProviderWeblocRepresentation(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() + + // TODO: remove from core +// let url = try await zippedRepresentation.get() +// return url - 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 28204bcb4..e9b63dac0 100644 --- a/Mail/Views/New Message/Attachments/AttachmentsManager.swift +++ b/Mail/Views/New Message/Attachments/AttachmentsManager.swift @@ -220,7 +220,8 @@ final class AttachmentsManager: ObservableObject { 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) diff --git a/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderFileRepresentation.swift b/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderFileRepresentation.swift new file mode 100644 index 000000000..0123d63e3 --- /dev/null +++ b/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderFileRepresentation.swift @@ -0,0 +1,110 @@ +/* + 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 Combine +import Foundation +import InfomaniakCore + +/// Something that can provide a `Progress` and an async `Result` in order to load an url from a `NSItemProvider` +final class ItemProviderFileRepresentation: NSObject, ProgressResultable { + enum ErrorDomain: Error { + case UTINotFound + case UnableToLoadFile + } + + typealias Success = URL + typealias Failure = Error + + /// Track task progress with internal Combine pipe + private let resultProcessed = PassthroughSubject() + + /// Internal observation of the Combine progress Pipe + private var resultProcessedObserver: AnyCancellable? + + /// Internal Task that wraps the combine result observation + private var computeResultTask: Task? + + public init(from itemProvider: NSItemProvider) throws { + guard let typeIdentifier = itemProvider.registeredTypeIdentifiers.first else { + throw ErrorDomain.UTINotFound + } + + // Keep compiler happy + progress = Progress(totalUnitCount: 1) + + super.init() + + // Set progress and hook completion closure to a combine pipe + progress = itemProvider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { fileProviderURL, error in + guard let fileProviderURL, error == nil else { + self.resultProcessed.send(completion: .failure(error ?? ErrorDomain.UnableToLoadFile)) + return + } + + do { + let fileName = fileProviderURL.lastPathComponent + let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName) + let temporaryFileURL = temporaryURL.appendingPathComponent(fileName) + try FileManager.default.createDirectory(at: temporaryURL, withIntermediateDirectories: true) + try FileManager.default.copyItem(atPath: fileProviderURL.path, toPath: temporaryFileURL.path) + self.resultProcessed.send(temporaryFileURL) + self.resultProcessed.send(completion: .finished) + } catch { + self.resultProcessed.send(completion: .failure(error)) + } + } + + /// Wrap the Combine pipe to a native Swift Async Task for convenience + computeResultTask = Task { + do { + let result: URL = try await withCheckedThrowingContinuation { continuation in + self.resultProcessedObserver = resultProcessed.sink { result in + switch result { + case .finished: + break + case .failure(let error): + continuation.resume(throwing: error) + } + self.resultProcessedObserver?.cancel() + } receiveValue: { value in + continuation.resume(with: .success(value)) + } + } + + return result + + } catch { + throw error + } + } + } + + // MARK: Public + + var progress: Progress + + var result: Result { + get async { + guard let computeResultTask else { + fatalError("This never should be nil") + } + + return await computeResultTask.result + } + } +} diff --git a/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderTextRepresentation.swift b/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderTextRepresentation.swift new file mode 100644 index 000000000..57a65fd94 --- /dev/null +++ b/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderTextRepresentation.swift @@ -0,0 +1,150 @@ +/* + 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 Combine +import Foundation +import InfomaniakCore +import InfomaniakDI + +/// Something that can provide a `Progress` and an async `Result` in order to make a raw text file from a `NSItemProvider` +final class ItemProviderTextRepresentation: NSObject, ProgressResultable { + enum ErrorDomain: Error { + case UTINotFound + case UTINotSupported + case unableToBuildTempURL + case unableToLoadURLForObject + case unknown + } + + typealias Success = URL + typealias Failure = Error + + private static let progressStep: Int64 = 1 + + /// Track task progress with internal Combine pipe + private let resultProcessed = PassthroughSubject() + + /// Internal observation of the Combine progress Pipe + private var resultProcessedObserver: AnyCancellable? + + /// Internal Task that wraps the combine result observation + private var computeResultTask: Task? + + public init(from itemProvider: NSItemProvider) throws { + guard let typeIdentifier = itemProvider.registeredTypeIdentifiers.first else { + throw ErrorDomain.UTINotFound + } + + progress = Progress(totalUnitCount: 1) + + super.init() + + let childProgress = Progress() + progress.addChild(childProgress, withPendingUnitCount: Self.progressStep) + + itemProvider.loadItem(forTypeIdentifier: typeIdentifier) { coding, error in + defer { + childProgress.completedUnitCount += Self.progressStep + } + + guard error == nil, coding != nil else { + self.resultProcessed.send(completion: .failure(error ?? ErrorDomain.unknown)) + return + } + + @InjectService var pathProvider: AppGroupPathProvidable + let tmpDirectoryURL = pathProvider.tmpDirectoryURL + + // Is String + if let text = coding as? String { + let targetURL = tmpDirectoryURL.appendingPathComponent("\(UUID().uuidString).txt") + + do { + try text.write(to: targetURL, atomically: true, encoding: .utf8) + self.resultProcessed.send(targetURL) + self.resultProcessed.send(completion: .finished) + } catch { + self.resultProcessed.send(completion: .failure(error)) + } + } + + // Is Data + else if let data = coding as? Data { + guard let uti = UTI(typeIdentifier) else { + self.resultProcessed.send(completion: .failure(ErrorDomain.UTINotFound)) + return + } + + let targetURL = tmpDirectoryURL + .appendingPathComponent("\(UUID().uuidString)") + .appendingPathExtension(for: uti) + + do { + try data.write(to: targetURL) + self.resultProcessed.send(targetURL) + self.resultProcessed.send(completion: .finished) + } catch { + self.resultProcessed.send(completion: .failure(error)) + } + } + + // Not supported + else { + self.resultProcessed.send(completion: .failure(ErrorDomain.UTINotSupported)) + } + } + + /// Wrap the Combine pipe to a native Swift Async Task for convenience + computeResultTask = Task { + do { + let result: URL = try await withCheckedThrowingContinuation { continuation in + self.resultProcessedObserver = resultProcessed.sink { result in + switch result { + case .finished: + break + case .failure(let error): + continuation.resume(throwing: error) + } + self.resultProcessedObserver?.cancel() + } receiveValue: { value in + continuation.resume(with: .success(value)) + } + } + + return result + + } catch { + throw error + } + } + } + + // MARK: Public + + var progress: Progress + + var result: Result { + get async { + guard let computeResultTask else { + fatalError("This never should be nil") + } + + return await computeResultTask.result + } + } +} diff --git a/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderWeblocRepresentation.swift b/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderWeblocRepresentation.swift new file mode 100644 index 000000000..56ef3ddf1 --- /dev/null +++ b/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderWeblocRepresentation.swift @@ -0,0 +1,114 @@ +/* + Infomaniak Mail - iOS App + Copyright (C) 2022 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import Combine +import Foundation +import InfomaniakCore +import InfomaniakDI + +/// Something that can provide a `Progress` and an async `Result` in order to make a webloc plist from a `NSItemProvider` +final class ItemProviderWeblocRepresentation: NSObject, ProgressResultable { + enum ErrorDomain: Error { + case UTINotFound + case unableToBuildTempURL + case unableToLoadURLForObject + } + + typealias Success = URL + typealias Failure = Error + + /// Track task progress with internal Combine pipe + private let resultProcessed = PassthroughSubject() + + /// Internal observation of the Combine progress Pipe + private var resultProcessedObserver: AnyCancellable? + + /// Internal Task that wraps the combine result observation + private var computeResultTask: Task? + + public init(from itemProvider: NSItemProvider) throws { + // Keep compiler happy + progress = Progress(totalUnitCount: 1) + + super.init() + + progress = itemProvider.loadObject(ofClass: URL.self) { path, error in + guard error == nil, let path: URL = path else { + let error: Error = error ?? ErrorDomain.unableToLoadURLForObject + self.resultProcessed.send(completion: .failure(error)) + return + } + + // Save the URL as a webloc file (plist) + let content = ["URL": path.absoluteString] + + @InjectService var pathProvider: AppGroupPathProvidable + let fileName = path.lastPathComponent + let tmpDirectoryURL = pathProvider.tmpDirectoryURL + let targetURL = tmpDirectoryURL.appendingPathComponent("\(fileName).webloc") + do { + let encoder = PropertyListEncoder() + let data = try encoder.encode(content) + try data.write(to: targetURL) + + self.resultProcessed.send(targetURL) + self.resultProcessed.send(completion: .finished) + } catch { + self.resultProcessed.send(completion: .failure(error)) + } + } + + /// Wrap the Combine pipe to a native Swift Async Task for convenience + computeResultTask = Task { + do { + let result: URL = try await withCheckedThrowingContinuation { continuation in + self.resultProcessedObserver = resultProcessed.sink { result in + switch result { + case .finished: + break + case .failure(let error): + continuation.resume(throwing: error) + } + self.resultProcessedObserver?.cancel() + } receiveValue: { value in + continuation.resume(with: .success(value)) + } + } + + return result + + } catch { + throw error + } + } + } + + // MARK: Public + + var progress: Progress + + var result: Result { + get async { + guard let computeResultTask else { + fatalError("This never should be nil") + } + + return await computeResultTask.result + } + } +} diff --git a/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderZipRepresentation.swift b/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderZipRepresentation.swift new file mode 100644 index 000000000..a7a0a5820 --- /dev/null +++ b/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderZipRepresentation.swift @@ -0,0 +1,128 @@ +/* + 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 Combine +import Foundation +import InfomaniakCore +import InfomaniakDI + +/// Something that can provide a `Progress` and an async `Result` in order to make a zip from a `NSItemProvider` +final class ItemProviderZipRepresentation: NSObject, ProgressResultable { + enum ErrorDomain: Error { + case UTINotFound + case unableToBuildTempURL + case unableToLoadURLForObject + } + + typealias Success = URL + typealias Failure = Error + + private static let progressStep: Int64 = 1 + + /// Track task progress with internal Combine pipe + private let resultProcessed = PassthroughSubject() + + /// Internal observation of the Combine progress Pipe + private var resultProcessedObserver: AnyCancellable? + + /// Internal Task that wraps the combine result observation + private var computeResultTask: Task? + + public init(from itemProvider: NSItemProvider) throws { + // Keep compiler happy + progress = Progress(totalUnitCount: 1) + + super.init() + + let fileManager = FileManager.default + let coordinator = NSFileCoordinator() + + progress = itemProvider.loadObject(ofClass: URL.self) { path, error in + guard error == nil, let path: URL = path else { + let error: Error = error ?? ErrorDomain.unableToLoadURLForObject + self.resultProcessed.send(completion: .failure(error)) + return + } + + // Get a NSProgress on file copy is hard ~> + // https://developer.apple.com/forums/thread/114001?answerId=350635022#350635022 + // > If you’d like to see such support [ie. for NSProgress] added in the future, I encourage you to file an + // enhancement request + + // Minimalist progress file processing support + let childProgress = Progress() + self.progress.addChild(childProgress, withPendingUnitCount: Self.progressStep) + + // compress content of folder and move it somewhere we can safely store it for upload + var error: NSError? + coordinator.coordinate(readingItemAt: path, options: [.forUploading], error: &error) { zipURL in + @InjectService var pathProvider: AppGroupPathProvidable + let tmpDirectoryURL = pathProvider.tmpDirectoryURL + let fileName = path.lastPathComponent + let tempURL = tmpDirectoryURL.appendingPathComponent("\(fileName).zip") + + do { + try fileManager.moveItem(at: zipURL, to: tempURL) + self.resultProcessed.send(tempURL) + self.resultProcessed.send(completion: .finished) + } catch { + self.resultProcessed.send(completion: .failure(error)) + } + childProgress.completedUnitCount += Self.progressStep + } + } + + /// Wrap the Combine pipe to a native Swift Async Task for convenience + computeResultTask = Task { + do { + let result: URL = try await withCheckedThrowingContinuation { continuation in + self.resultProcessedObserver = resultProcessed.sink { result in + switch result { + case .finished: + break + case .failure(let error): + continuation.resume(throwing: error) + } + self.resultProcessedObserver?.cancel() + } receiveValue: { value in + continuation.resume(with: .success(value)) + } + } + + return result + + } catch { + throw error + } + } + } + + // MARK: Public + + var progress: Progress + + var result: Result { + get async { + guard let computeResultTask else { + fatalError("This never should be nil") + } + + return await computeResultTask.result + } + } +} diff --git a/Mail/Views/New Message/Attachments/TO_CORE/ProgressResultable.swift b/Mail/Views/New Message/Attachments/TO_CORE/ProgressResultable.swift new file mode 100644 index 000000000..569a5e165 --- /dev/null +++ b/Mail/Views/New Message/Attachments/TO_CORE/ProgressResultable.swift @@ -0,0 +1,41 @@ +/* + 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 Combine +import Foundation +import InfomaniakCore + +/// Some wrapper type that give initial access to a progress, and also to an async result +/// +/// Helpful to manage UI tracking of complex subtasks using the `Progress` type +/// while working with the easy to use `Result` type. +protocol ProgressResultable { + + /// Success type + associatedtype Success + + /// Error type + associatedtype Failure: Error + + /// The progress associated with the current task + var progress: Progress { get } + + /// The result associated with the current task + /// Re-processed each time + var result: Result { get async } +} diff --git a/Mail/Views/New Message/Attachments/TO_CORE/URL+Extension.swift b/Mail/Views/New Message/Attachments/TO_CORE/URL+Extension.swift new file mode 100644 index 000000000..1d2dcc75a --- /dev/null +++ b/Mail/Views/New Message/Attachments/TO_CORE/URL+Extension.swift @@ -0,0 +1,59 @@ +/* + Infomaniak kDrive - iOS App + Copyright (C) 2021 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 + +/// Extending URL with UTI helpers +public extension URL { + var typeIdentifier: String? { + if hasDirectoryPath { + return UTI.folder.identifier + } + if FileManager.default.fileExists(atPath: path) { + return try? resourceValues(forKeys: [.typeIdentifierKey]).typeIdentifier + } else { + // If the file is not downloaded, we get the type identifier using its extension + return UTI(filenameExtension: pathExtension, conformingTo: .item)?.identifier + } + } + + var uti: UTI? { + if let typeIdentifier = typeIdentifier { + return UTI(typeIdentifier) + } + return nil + } + + var creationDate: Date? { + return try? resourceValues(forKeys: [.creationDateKey]).creationDate + } + + func appendingPathExtension(for contentType: UTI) -> URL { + guard let newExtension = contentType.preferredFilenameExtension, + pathExtension != newExtension else { + return self + } + + return appendingPathExtension(newExtension) + } + + mutating func appendPathExtension(for contentType: UTI) { + self = appendingPathExtension(for: contentType) + } +} diff --git a/Mail/Views/New Message/Attachments/TO_CORE_UI/ItemProviderUIImageRepresentation.swift b/Mail/Views/New Message/Attachments/TO_CORE_UI/ItemProviderUIImageRepresentation.swift new file mode 100644 index 000000000..939e34959 --- /dev/null +++ b/Mail/Views/New Message/Attachments/TO_CORE_UI/ItemProviderUIImageRepresentation.swift @@ -0,0 +1,132 @@ +/* + 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 Combine +import InfomaniakCore +import InfomaniakDI +import UIKit + +/// Something that can provide a `Progress` and an async `Result` in order to make an image file from a `NSItemProvider` wrapping +/// an UIImage +final class ItemProviderUIImageRepresentation: NSObject, ProgressResultable { + enum ErrorDomain: Error { + case UTINotFound + case UTINotSupported + case unableToBuildTempURL + case unableToLoadURLForObject + case unknown + } + + typealias Success = URL + typealias Failure = Error + + private static let progressStep: Int64 = 1 + + /// Track task progress with internal Combine pipe + private let resultProcessed = PassthroughSubject() + + /// Internal observation of the Combine progress Pipe + private var resultProcessedObserver: AnyCancellable? + + /// Internal Task that wraps the combine result observation + private var computeResultTask: Task? + + public init(from itemProvider: NSItemProvider) throws { + guard let typeIdentifier = itemProvider.registeredTypeIdentifiers.first else { + throw ErrorDomain.UTINotFound + } + + progress = Progress(totalUnitCount: 1) + + super.init() + + let childProgress = Progress() + progress.addChild(childProgress, withPendingUnitCount: Self.progressStep) + + itemProvider.loadItem(forTypeIdentifier: UTI.image.identifier) { coding, error in + defer { + childProgress.completedUnitCount += Self.progressStep + } + + guard error == nil, coding != nil else { + self.resultProcessed.send(completion: .failure(error ?? ErrorDomain.unknown)) + return + } + + @InjectService var pathProvider: AppGroupPathProvidable + let tmpDirectoryURL = pathProvider.tmpDirectoryURL + + // Is UIImage + if let image = coding as? UIImage, + let pngData = image.pngData() { + let targetURL = tmpDirectoryURL.appendingPathComponent("\(UUID().uuidString).png") + + do { + try pngData.write(to: targetURL, options: .atomic) + self.resultProcessed.send(targetURL) + self.resultProcessed.send(completion: .finished) + } catch { + self.resultProcessed.send(completion: .failure(error)) + } + } + + // Not supported + else { + self.resultProcessed.send(completion: .failure(ErrorDomain.UTINotSupported)) + } + } + + /// Wrap the Combine pipe to a native Swift Async Task for convenience + computeResultTask = Task { + do { + let result: URL = try await withCheckedThrowingContinuation { continuation in + self.resultProcessedObserver = resultProcessed.sink { result in + switch result { + case .finished: + break + case .failure(let error): + continuation.resume(throwing: error) + } + self.resultProcessedObserver?.cancel() + } receiveValue: { value in + continuation.resume(with: .success(value)) + } + } + + return result + + } catch { + throw error + } + } + } + + // MARK: Public + + var progress: Progress + + var result: Result { + get async { + guard let computeResultTask else { + fatalError("This never should be nil") + } + + return await computeResultTask.result + } + } +} diff --git a/MailCore/Cache/MailboxManager.swift b/MailCore/Cache/MailboxManager.swift index 3ea9dccc2..a9af2f1c2 100644 --- a/MailCore/Cache/MailboxManager.swift +++ b/MailCore/Cache/MailboxManager.swift @@ -20,38 +20,26 @@ 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 { + 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 ?? "")" ) diff --git a/MailNotificationServiceExtension/NotificationService.swift b/MailNotificationServiceExtension/NotificationService.swift index 86fb2fb89..f5aed08a7 100644 --- a/MailNotificationServiceExtension/NotificationService.swift +++ b/MailNotificationServiceExtension/NotificationService.swift @@ -26,6 +26,9 @@ import RealmSwift import UserNotifications 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)? diff --git a/MailNotificationServiceExtension/NotificationServiceAssembly.swift b/MailNotificationServiceExtension/NotificationServiceAssembly.swift new file mode 100644 index 000000000..94dbbd87f --- /dev/null +++ b/MailNotificationServiceExtension/NotificationServiceAssembly.swift @@ -0,0 +1,69 @@ +/* + 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 MailCore +import OSLog + +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: AccountManager.self) { _, _ in + AccountManager() + }, + Factory(type: AppGroupPathProvidable.self) { _, _ in + guard let provider = AppGroupPathProvider( + realmRootPath: realmRootPath, + appGroupIdentifier: appGroupIdentifier + ) else { + fatalError("could not safely init AppGroupPathProvider") + } + + return provider + } + ] + + factories.registerFactoriesInDI() + } +} + +/// Something that loads the DI on init +public struct EarlyDIHook { + public init() { + // setup DI ASAP + os_log("EarlyDIHook") + NotificationServiceAssembly.setupDI() + } +} diff --git a/Project.swift b/Project.swift index 4450f62f8..6625c9068 100644 --- a/Project.swift +++ b/Project.swift @@ -104,7 +104,7 @@ let project = Project(name: "Mail", name: "MailShareExtension", platform: .iOS, product: .appExtension, - bundleId: "com.infomaniak.mail.MailShareExtension", + bundleId: "com.infomaniak.mail.ShareExtension", deploymentTarget: Constants.deploymentTarget, infoPlist: .file(path: "MailShareExtension/Info.plist"), sources: ["MailShareExtension/**", @@ -126,6 +126,7 @@ let project = Project(name: "Mail", "MailResources/**/*.js" ], entitlements: "MailResources/Mail.entitlements", + scripts: [Constants.swiftlintScript], dependencies: [ .target(name: "MailCore"), .package(product: "Introspect"), From 4ccbfe6d281c1ee7f2b956a941a9fe61fafccf1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 19 Jul 2023 15:03:29 +0200 Subject: [PATCH 27/61] chore: use code in Core and CoreUI, remove local copy --- .../New Message/Attachments/Attachable.swift | 5 +- .../ItemProviderFileRepresentation.swift | 110 ------------- .../ItemProviderTextRepresentation.swift | 150 ------------------ .../ItemProviderWeblocRepresentation.swift | 114 ------------- .../ItemProviderZipRepresentation.swift | 128 --------------- .../TO_CORE/ProgressResultable.swift | 41 ----- .../Attachments/TO_CORE/URL+Extension.swift | 59 ------- .../ItemProviderUIImageRepresentation.swift | 132 --------------- 8 files changed, 1 insertion(+), 738 deletions(-) delete mode 100644 Mail/Views/New Message/Attachments/TO_CORE/ItemProviderFileRepresentation.swift delete mode 100644 Mail/Views/New Message/Attachments/TO_CORE/ItemProviderTextRepresentation.swift delete mode 100644 Mail/Views/New Message/Attachments/TO_CORE/ItemProviderWeblocRepresentation.swift delete mode 100644 Mail/Views/New Message/Attachments/TO_CORE/ItemProviderZipRepresentation.swift delete mode 100644 Mail/Views/New Message/Attachments/TO_CORE/ProgressResultable.swift delete mode 100644 Mail/Views/New Message/Attachments/TO_CORE/URL+Extension.swift delete mode 100644 Mail/Views/New Message/Attachments/TO_CORE_UI/ItemProviderUIImageRepresentation.swift diff --git a/Mail/Views/New Message/Attachments/Attachable.swift b/Mail/Views/New Message/Attachments/Attachable.swift index 687cd8c9e..6213e22e6 100644 --- a/Mail/Views/New Message/Attachments/Attachable.swift +++ b/Mail/Views/New Message/Attachments/Attachable.swift @@ -19,6 +19,7 @@ import Combine import Foundation import InfomaniakCore +import InfomaniakCoreUI import MailCore import PhotosUI import UniformTypeIdentifiers @@ -65,10 +66,6 @@ extension NSItemProvider: Attachable { let getFile = try ItemProviderZipRepresentation(from: self) return try await getFile.result.get() - // TODO: remove from core -// let url = try await zippedRepresentation.get() -// return url - case .none: throw ErrorDomain.UTINotFound } diff --git a/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderFileRepresentation.swift b/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderFileRepresentation.swift deleted file mode 100644 index 0123d63e3..000000000 --- a/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderFileRepresentation.swift +++ /dev/null @@ -1,110 +0,0 @@ -/* - Infomaniak Mail - iOS App - Copyright (C) 2022 Infomaniak Network SA - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -import Combine -import Foundation -import InfomaniakCore - -/// Something that can provide a `Progress` and an async `Result` in order to load an url from a `NSItemProvider` -final class ItemProviderFileRepresentation: NSObject, ProgressResultable { - enum ErrorDomain: Error { - case UTINotFound - case UnableToLoadFile - } - - typealias Success = URL - typealias Failure = Error - - /// Track task progress with internal Combine pipe - private let resultProcessed = PassthroughSubject() - - /// Internal observation of the Combine progress Pipe - private var resultProcessedObserver: AnyCancellable? - - /// Internal Task that wraps the combine result observation - private var computeResultTask: Task? - - public init(from itemProvider: NSItemProvider) throws { - guard let typeIdentifier = itemProvider.registeredTypeIdentifiers.first else { - throw ErrorDomain.UTINotFound - } - - // Keep compiler happy - progress = Progress(totalUnitCount: 1) - - super.init() - - // Set progress and hook completion closure to a combine pipe - progress = itemProvider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { fileProviderURL, error in - guard let fileProviderURL, error == nil else { - self.resultProcessed.send(completion: .failure(error ?? ErrorDomain.UnableToLoadFile)) - return - } - - do { - let fileName = fileProviderURL.lastPathComponent - let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName) - let temporaryFileURL = temporaryURL.appendingPathComponent(fileName) - try FileManager.default.createDirectory(at: temporaryURL, withIntermediateDirectories: true) - try FileManager.default.copyItem(atPath: fileProviderURL.path, toPath: temporaryFileURL.path) - self.resultProcessed.send(temporaryFileURL) - self.resultProcessed.send(completion: .finished) - } catch { - self.resultProcessed.send(completion: .failure(error)) - } - } - - /// Wrap the Combine pipe to a native Swift Async Task for convenience - computeResultTask = Task { - do { - let result: URL = try await withCheckedThrowingContinuation { continuation in - self.resultProcessedObserver = resultProcessed.sink { result in - switch result { - case .finished: - break - case .failure(let error): - continuation.resume(throwing: error) - } - self.resultProcessedObserver?.cancel() - } receiveValue: { value in - continuation.resume(with: .success(value)) - } - } - - return result - - } catch { - throw error - } - } - } - - // MARK: Public - - var progress: Progress - - var result: Result { - get async { - guard let computeResultTask else { - fatalError("This never should be nil") - } - - return await computeResultTask.result - } - } -} diff --git a/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderTextRepresentation.swift b/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderTextRepresentation.swift deleted file mode 100644 index 57a65fd94..000000000 --- a/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderTextRepresentation.swift +++ /dev/null @@ -1,150 +0,0 @@ -/* - Infomaniak Mail - iOS App - Copyright (C) 2022 Infomaniak Network SA - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -import Combine -import Foundation -import InfomaniakCore -import InfomaniakDI - -/// Something that can provide a `Progress` and an async `Result` in order to make a raw text file from a `NSItemProvider` -final class ItemProviderTextRepresentation: NSObject, ProgressResultable { - enum ErrorDomain: Error { - case UTINotFound - case UTINotSupported - case unableToBuildTempURL - case unableToLoadURLForObject - case unknown - } - - typealias Success = URL - typealias Failure = Error - - private static let progressStep: Int64 = 1 - - /// Track task progress with internal Combine pipe - private let resultProcessed = PassthroughSubject() - - /// Internal observation of the Combine progress Pipe - private var resultProcessedObserver: AnyCancellable? - - /// Internal Task that wraps the combine result observation - private var computeResultTask: Task? - - public init(from itemProvider: NSItemProvider) throws { - guard let typeIdentifier = itemProvider.registeredTypeIdentifiers.first else { - throw ErrorDomain.UTINotFound - } - - progress = Progress(totalUnitCount: 1) - - super.init() - - let childProgress = Progress() - progress.addChild(childProgress, withPendingUnitCount: Self.progressStep) - - itemProvider.loadItem(forTypeIdentifier: typeIdentifier) { coding, error in - defer { - childProgress.completedUnitCount += Self.progressStep - } - - guard error == nil, coding != nil else { - self.resultProcessed.send(completion: .failure(error ?? ErrorDomain.unknown)) - return - } - - @InjectService var pathProvider: AppGroupPathProvidable - let tmpDirectoryURL = pathProvider.tmpDirectoryURL - - // Is String - if let text = coding as? String { - let targetURL = tmpDirectoryURL.appendingPathComponent("\(UUID().uuidString).txt") - - do { - try text.write(to: targetURL, atomically: true, encoding: .utf8) - self.resultProcessed.send(targetURL) - self.resultProcessed.send(completion: .finished) - } catch { - self.resultProcessed.send(completion: .failure(error)) - } - } - - // Is Data - else if let data = coding as? Data { - guard let uti = UTI(typeIdentifier) else { - self.resultProcessed.send(completion: .failure(ErrorDomain.UTINotFound)) - return - } - - let targetURL = tmpDirectoryURL - .appendingPathComponent("\(UUID().uuidString)") - .appendingPathExtension(for: uti) - - do { - try data.write(to: targetURL) - self.resultProcessed.send(targetURL) - self.resultProcessed.send(completion: .finished) - } catch { - self.resultProcessed.send(completion: .failure(error)) - } - } - - // Not supported - else { - self.resultProcessed.send(completion: .failure(ErrorDomain.UTINotSupported)) - } - } - - /// Wrap the Combine pipe to a native Swift Async Task for convenience - computeResultTask = Task { - do { - let result: URL = try await withCheckedThrowingContinuation { continuation in - self.resultProcessedObserver = resultProcessed.sink { result in - switch result { - case .finished: - break - case .failure(let error): - continuation.resume(throwing: error) - } - self.resultProcessedObserver?.cancel() - } receiveValue: { value in - continuation.resume(with: .success(value)) - } - } - - return result - - } catch { - throw error - } - } - } - - // MARK: Public - - var progress: Progress - - var result: Result { - get async { - guard let computeResultTask else { - fatalError("This never should be nil") - } - - return await computeResultTask.result - } - } -} diff --git a/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderWeblocRepresentation.swift b/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderWeblocRepresentation.swift deleted file mode 100644 index 56ef3ddf1..000000000 --- a/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderWeblocRepresentation.swift +++ /dev/null @@ -1,114 +0,0 @@ -/* - Infomaniak Mail - iOS App - Copyright (C) 2022 Infomaniak Network SA - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -import Combine -import Foundation -import InfomaniakCore -import InfomaniakDI - -/// Something that can provide a `Progress` and an async `Result` in order to make a webloc plist from a `NSItemProvider` -final class ItemProviderWeblocRepresentation: NSObject, ProgressResultable { - enum ErrorDomain: Error { - case UTINotFound - case unableToBuildTempURL - case unableToLoadURLForObject - } - - typealias Success = URL - typealias Failure = Error - - /// Track task progress with internal Combine pipe - private let resultProcessed = PassthroughSubject() - - /// Internal observation of the Combine progress Pipe - private var resultProcessedObserver: AnyCancellable? - - /// Internal Task that wraps the combine result observation - private var computeResultTask: Task? - - public init(from itemProvider: NSItemProvider) throws { - // Keep compiler happy - progress = Progress(totalUnitCount: 1) - - super.init() - - progress = itemProvider.loadObject(ofClass: URL.self) { path, error in - guard error == nil, let path: URL = path else { - let error: Error = error ?? ErrorDomain.unableToLoadURLForObject - self.resultProcessed.send(completion: .failure(error)) - return - } - - // Save the URL as a webloc file (plist) - let content = ["URL": path.absoluteString] - - @InjectService var pathProvider: AppGroupPathProvidable - let fileName = path.lastPathComponent - let tmpDirectoryURL = pathProvider.tmpDirectoryURL - let targetURL = tmpDirectoryURL.appendingPathComponent("\(fileName).webloc") - do { - let encoder = PropertyListEncoder() - let data = try encoder.encode(content) - try data.write(to: targetURL) - - self.resultProcessed.send(targetURL) - self.resultProcessed.send(completion: .finished) - } catch { - self.resultProcessed.send(completion: .failure(error)) - } - } - - /// Wrap the Combine pipe to a native Swift Async Task for convenience - computeResultTask = Task { - do { - let result: URL = try await withCheckedThrowingContinuation { continuation in - self.resultProcessedObserver = resultProcessed.sink { result in - switch result { - case .finished: - break - case .failure(let error): - continuation.resume(throwing: error) - } - self.resultProcessedObserver?.cancel() - } receiveValue: { value in - continuation.resume(with: .success(value)) - } - } - - return result - - } catch { - throw error - } - } - } - - // MARK: Public - - var progress: Progress - - var result: Result { - get async { - guard let computeResultTask else { - fatalError("This never should be nil") - } - - return await computeResultTask.result - } - } -} diff --git a/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderZipRepresentation.swift b/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderZipRepresentation.swift deleted file mode 100644 index a7a0a5820..000000000 --- a/Mail/Views/New Message/Attachments/TO_CORE/ItemProviderZipRepresentation.swift +++ /dev/null @@ -1,128 +0,0 @@ -/* - Infomaniak Mail - iOS App - Copyright (C) 2022 Infomaniak Network SA - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -import Combine -import Foundation -import InfomaniakCore -import InfomaniakDI - -/// Something that can provide a `Progress` and an async `Result` in order to make a zip from a `NSItemProvider` -final class ItemProviderZipRepresentation: NSObject, ProgressResultable { - enum ErrorDomain: Error { - case UTINotFound - case unableToBuildTempURL - case unableToLoadURLForObject - } - - typealias Success = URL - typealias Failure = Error - - private static let progressStep: Int64 = 1 - - /// Track task progress with internal Combine pipe - private let resultProcessed = PassthroughSubject() - - /// Internal observation of the Combine progress Pipe - private var resultProcessedObserver: AnyCancellable? - - /// Internal Task that wraps the combine result observation - private var computeResultTask: Task? - - public init(from itemProvider: NSItemProvider) throws { - // Keep compiler happy - progress = Progress(totalUnitCount: 1) - - super.init() - - let fileManager = FileManager.default - let coordinator = NSFileCoordinator() - - progress = itemProvider.loadObject(ofClass: URL.self) { path, error in - guard error == nil, let path: URL = path else { - let error: Error = error ?? ErrorDomain.unableToLoadURLForObject - self.resultProcessed.send(completion: .failure(error)) - return - } - - // Get a NSProgress on file copy is hard ~> - // https://developer.apple.com/forums/thread/114001?answerId=350635022#350635022 - // > If you’d like to see such support [ie. for NSProgress] added in the future, I encourage you to file an - // enhancement request - - // Minimalist progress file processing support - let childProgress = Progress() - self.progress.addChild(childProgress, withPendingUnitCount: Self.progressStep) - - // compress content of folder and move it somewhere we can safely store it for upload - var error: NSError? - coordinator.coordinate(readingItemAt: path, options: [.forUploading], error: &error) { zipURL in - @InjectService var pathProvider: AppGroupPathProvidable - let tmpDirectoryURL = pathProvider.tmpDirectoryURL - let fileName = path.lastPathComponent - let tempURL = tmpDirectoryURL.appendingPathComponent("\(fileName).zip") - - do { - try fileManager.moveItem(at: zipURL, to: tempURL) - self.resultProcessed.send(tempURL) - self.resultProcessed.send(completion: .finished) - } catch { - self.resultProcessed.send(completion: .failure(error)) - } - childProgress.completedUnitCount += Self.progressStep - } - } - - /// Wrap the Combine pipe to a native Swift Async Task for convenience - computeResultTask = Task { - do { - let result: URL = try await withCheckedThrowingContinuation { continuation in - self.resultProcessedObserver = resultProcessed.sink { result in - switch result { - case .finished: - break - case .failure(let error): - continuation.resume(throwing: error) - } - self.resultProcessedObserver?.cancel() - } receiveValue: { value in - continuation.resume(with: .success(value)) - } - } - - return result - - } catch { - throw error - } - } - } - - // MARK: Public - - var progress: Progress - - var result: Result { - get async { - guard let computeResultTask else { - fatalError("This never should be nil") - } - - return await computeResultTask.result - } - } -} diff --git a/Mail/Views/New Message/Attachments/TO_CORE/ProgressResultable.swift b/Mail/Views/New Message/Attachments/TO_CORE/ProgressResultable.swift deleted file mode 100644 index 569a5e165..000000000 --- a/Mail/Views/New Message/Attachments/TO_CORE/ProgressResultable.swift +++ /dev/null @@ -1,41 +0,0 @@ -/* - Infomaniak Mail - iOS App - Copyright (C) 2022 Infomaniak Network SA - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -import Combine -import Foundation -import InfomaniakCore - -/// Some wrapper type that give initial access to a progress, and also to an async result -/// -/// Helpful to manage UI tracking of complex subtasks using the `Progress` type -/// while working with the easy to use `Result` type. -protocol ProgressResultable { - - /// Success type - associatedtype Success - - /// Error type - associatedtype Failure: Error - - /// The progress associated with the current task - var progress: Progress { get } - - /// The result associated with the current task - /// Re-processed each time - var result: Result { get async } -} diff --git a/Mail/Views/New Message/Attachments/TO_CORE/URL+Extension.swift b/Mail/Views/New Message/Attachments/TO_CORE/URL+Extension.swift deleted file mode 100644 index 1d2dcc75a..000000000 --- a/Mail/Views/New Message/Attachments/TO_CORE/URL+Extension.swift +++ /dev/null @@ -1,59 +0,0 @@ -/* - Infomaniak kDrive - iOS App - Copyright (C) 2021 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 - -/// Extending URL with UTI helpers -public extension URL { - var typeIdentifier: String? { - if hasDirectoryPath { - return UTI.folder.identifier - } - if FileManager.default.fileExists(atPath: path) { - return try? resourceValues(forKeys: [.typeIdentifierKey]).typeIdentifier - } else { - // If the file is not downloaded, we get the type identifier using its extension - return UTI(filenameExtension: pathExtension, conformingTo: .item)?.identifier - } - } - - var uti: UTI? { - if let typeIdentifier = typeIdentifier { - return UTI(typeIdentifier) - } - return nil - } - - var creationDate: Date? { - return try? resourceValues(forKeys: [.creationDateKey]).creationDate - } - - func appendingPathExtension(for contentType: UTI) -> URL { - guard let newExtension = contentType.preferredFilenameExtension, - pathExtension != newExtension else { - return self - } - - return appendingPathExtension(newExtension) - } - - mutating func appendPathExtension(for contentType: UTI) { - self = appendingPathExtension(for: contentType) - } -} diff --git a/Mail/Views/New Message/Attachments/TO_CORE_UI/ItemProviderUIImageRepresentation.swift b/Mail/Views/New Message/Attachments/TO_CORE_UI/ItemProviderUIImageRepresentation.swift deleted file mode 100644 index 939e34959..000000000 --- a/Mail/Views/New Message/Attachments/TO_CORE_UI/ItemProviderUIImageRepresentation.swift +++ /dev/null @@ -1,132 +0,0 @@ -/* - Infomaniak Mail - iOS App - Copyright (C) 2022 Infomaniak Network SA - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -import Combine -import InfomaniakCore -import InfomaniakDI -import UIKit - -/// Something that can provide a `Progress` and an async `Result` in order to make an image file from a `NSItemProvider` wrapping -/// an UIImage -final class ItemProviderUIImageRepresentation: NSObject, ProgressResultable { - enum ErrorDomain: Error { - case UTINotFound - case UTINotSupported - case unableToBuildTempURL - case unableToLoadURLForObject - case unknown - } - - typealias Success = URL - typealias Failure = Error - - private static let progressStep: Int64 = 1 - - /// Track task progress with internal Combine pipe - private let resultProcessed = PassthroughSubject() - - /// Internal observation of the Combine progress Pipe - private var resultProcessedObserver: AnyCancellable? - - /// Internal Task that wraps the combine result observation - private var computeResultTask: Task? - - public init(from itemProvider: NSItemProvider) throws { - guard let typeIdentifier = itemProvider.registeredTypeIdentifiers.first else { - throw ErrorDomain.UTINotFound - } - - progress = Progress(totalUnitCount: 1) - - super.init() - - let childProgress = Progress() - progress.addChild(childProgress, withPendingUnitCount: Self.progressStep) - - itemProvider.loadItem(forTypeIdentifier: UTI.image.identifier) { coding, error in - defer { - childProgress.completedUnitCount += Self.progressStep - } - - guard error == nil, coding != nil else { - self.resultProcessed.send(completion: .failure(error ?? ErrorDomain.unknown)) - return - } - - @InjectService var pathProvider: AppGroupPathProvidable - let tmpDirectoryURL = pathProvider.tmpDirectoryURL - - // Is UIImage - if let image = coding as? UIImage, - let pngData = image.pngData() { - let targetURL = tmpDirectoryURL.appendingPathComponent("\(UUID().uuidString).png") - - do { - try pngData.write(to: targetURL, options: .atomic) - self.resultProcessed.send(targetURL) - self.resultProcessed.send(completion: .finished) - } catch { - self.resultProcessed.send(completion: .failure(error)) - } - } - - // Not supported - else { - self.resultProcessed.send(completion: .failure(ErrorDomain.UTINotSupported)) - } - } - - /// Wrap the Combine pipe to a native Swift Async Task for convenience - computeResultTask = Task { - do { - let result: URL = try await withCheckedThrowingContinuation { continuation in - self.resultProcessedObserver = resultProcessed.sink { result in - switch result { - case .finished: - break - case .failure(let error): - continuation.resume(throwing: error) - } - self.resultProcessedObserver?.cancel() - } receiveValue: { value in - continuation.resume(with: .success(value)) - } - } - - return result - - } catch { - throw error - } - } - } - - // MARK: Public - - var progress: Progress - - var result: Result { - get async { - guard let computeResultTask else { - fatalError("This never should be nil") - } - - return await computeResultTask.result - } - } -} From 65caad47ca6fac1eee95578368b296a5081d5b9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 19 Jul 2023 18:12:58 +0200 Subject: [PATCH 28/61] chore: Bump Core / Core UI --- .package.resolved | 2 +- Project.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.package.resolved b/.package.resolved index 86c9d92cb..8a08a2006 100644 --- a/.package.resolved +++ b/.package.resolved @@ -41,7 +41,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Infomaniak/ios-core", "state" : { - "revision" : "0d43de51efcc97b212d73823506aa317ebfb8fa3" + "revision" : "60e101e3981ac5ce10cdd72ed8e60ebfdc0010cf" } }, { diff --git a/Project.swift b/Project.swift index 860728a11..c79ddf1d9 100644 --- a/Project.swift +++ b/Project.swift @@ -24,7 +24,7 @@ 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("0d43de51efcc97b212d73823506aa317ebfb8fa3")), + .package(url: "https://github.com/Infomaniak/ios-core", .revision("60e101e3981ac5ce10cdd72ed8e60ebfdc0010cf")), .package(url: "https://github.com/Infomaniak/ios-core-ui", .upToNextMajor(from: "2.5.1")), .package(url: "https://github.com/Infomaniak/ios-notifications", .upToNextMajor(from: "2.1.0")), .package(url: "https://github.com/Infomaniak/ios-create-account", .upToNextMajor(from: "1.1.0")), From 54fdd9d27241cb6dcb2750c90c2ebddb45897bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 26 Jul 2023 09:55:41 +0200 Subject: [PATCH 29/61] chore: bump Core --- Project.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.swift b/Project.swift index c79ddf1d9..7444bd74c 100644 --- a/Project.swift +++ b/Project.swift @@ -24,7 +24,7 @@ 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("60e101e3981ac5ce10cdd72ed8e60ebfdc0010cf")), + .package(url: "https://github.com/Infomaniak/ios-core", .revision("9256792d1571b77eed70b191dff180b4dae47969")), .package(url: "https://github.com/Infomaniak/ios-core-ui", .upToNextMajor(from: "2.5.1")), .package(url: "https://github.com/Infomaniak/ios-notifications", .upToNextMajor(from: "2.1.0")), .package(url: "https://github.com/Infomaniak/ios-create-account", .upToNextMajor(from: "1.1.0")), From 5e7adcf6cb76c94a8918f6ccfc6c586f7eddb21c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 26 Jul 2023 14:57:38 +0200 Subject: [PATCH 30/61] chore: PR Feedback --- Mail/AppDelegate.swift | 4 ++-- Mail/Proxy/Implementation/RootViewController.swift | 9 ++------- Mail/Proxy/Protocols/RootViewManageable.swift | 3 --- MailShareExtension/Proxy/RootViewManager.swift | 4 ---- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/Mail/AppDelegate.swift b/Mail/AppDelegate.swift index d15888ed9..c66ee4588 100644 --- a/Mail/AppDelegate.swift +++ b/Mail/AppDelegate.swift @@ -17,6 +17,7 @@ */ import CocoaLumberjackSwift +import InfomaniakCore import InfomaniakDI import InfomaniakNotifications import MailCore @@ -38,8 +39,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { Logging.initLogging() DDLogInfo("Application starting in foreground ? \(applicationState.applicationState != .background)") - // TODO: fix ApiFetcher call -// ApiFetcher.decoder.dateDecodingStrategy = .iso8601 + ApiFetcher.decoder.dateDecodingStrategy = .iso8601 UNUserNotificationCenter.current().delegate = notificationCenterDelegate Task { diff --git a/Mail/Proxy/Implementation/RootViewController.swift b/Mail/Proxy/Implementation/RootViewController.swift index 4c2846e09..3b05ce531 100644 --- a/Mail/Proxy/Implementation/RootViewController.swift +++ b/Mail/Proxy/Implementation/RootViewController.swift @@ -23,15 +23,10 @@ import UIKit @available(iOSApplicationExtension, unavailable) public struct RootViewManager: RootViewManageable { public var rootViewController: UIViewController? { - self.mainSceneKeyWindow?.rootViewController + mainSceneKeyWindow?.rootViewController } - + public var mainSceneKeyWindow: UIWindow? { UIApplication.shared.mainSceneKeyWindow } - - public func updateAllWindowUI() { - // TODO: fix UIApplication reference -// UIApplication.shared.connectedScenes.forEach { ($0.delegate as? SceneDelegate)?.updateWindowUI() } - } } diff --git a/Mail/Proxy/Protocols/RootViewManageable.swift b/Mail/Proxy/Protocols/RootViewManageable.swift index 9ae29e73a..c99444dcc 100644 --- a/Mail/Proxy/Protocols/RootViewManageable.swift +++ b/Mail/Proxy/Protocols/RootViewManageable.swift @@ -26,7 +26,4 @@ public protocol RootViewManageable { /// The current mainSceneKeyWindow var mainSceneKeyWindow: UIWindow? { get } - - /// Call updateWindowUI on all connected scenes - func updateAllWindowUI() } diff --git a/MailShareExtension/Proxy/RootViewManager.swift b/MailShareExtension/Proxy/RootViewManager.swift index ccf975aff..55c738fcb 100644 --- a/MailShareExtension/Proxy/RootViewManager.swift +++ b/MailShareExtension/Proxy/RootViewManager.swift @@ -29,8 +29,4 @@ public struct RootViewManager: RootViewManageable { public var mainSceneKeyWindow: UIWindow? { nil } - - public func updateAllWindowUI() { - // NOOP in share extension - } } From efef3f87eadd42a7186ecc0207f5b908f3b53616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 26 Jul 2023 16:33:28 +0200 Subject: [PATCH 31/61] chore: PR Feedback --- Mail/AppDelegate.swift | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/Mail/AppDelegate.swift b/Mail/AppDelegate.swift index c66ee4588..7d5bae89e 100644 --- a/Mail/AppDelegate.swift +++ b/Mail/AppDelegate.swift @@ -70,20 +70,6 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { } } - /// Validate deeplinks - func application(_ application: UIApplication, - open url: URL, - options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { - // Determine who sent the URL. - let sendingAppID = options[.sourceApplication] - DDLogInfo("source application = \(sendingAppID ?? "Unknown")") - - let absoluteString = url.absoluteString - let success = absoluteString.starts(with: "mailto") - DDLogInfo("URL handling = \(success ? "success" : "failure")") - return success - } - func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { DDLogError("Failed registering for notifications: \(error)") } From 3861d6f9521f528d1a56ecedb46754e916f80f9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 28 Jul 2023 10:57:40 +0200 Subject: [PATCH 32/61] chore: bump core --- .package.resolved | 2 +- Mail/Views/New Message/Attachments/Attachable.swift | 2 +- Mail/Views/New Message/Attachments/AttachmentsManager.swift | 2 +- Project.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.package.resolved b/.package.resolved index 8ed7b2530..f1ab23933 100644 --- a/.package.resolved +++ b/.package.resolved @@ -41,7 +41,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/Infomaniak/ios-core", "state" : { - "revision" : "f8c11f9a383c84b3c7c3db75afe87a1d077d7dd2" + "revision" : "4eaefd644f75d833d6b1009dd94a9d6d674ccb53" } }, { diff --git a/Mail/Views/New Message/Attachments/Attachable.swift b/Mail/Views/New Message/Attachments/Attachable.swift index 6213e22e6..91a3c510e 100644 --- a/Mail/Views/New Message/Attachments/Attachable.swift +++ b/Mail/Views/New Message/Attachments/Attachable.swift @@ -47,7 +47,7 @@ extension NSItemProvider: Attachable { func writeToTemporaryURL() async throws -> URL { switch underlyingType { case .isURL: - let getPlist = try ItemProviderWeblocRepresentation(from: self) + let getPlist = try ItemProviderURLRepresentation(from: self) return try await getPlist.result.get() case .isText: diff --git a/Mail/Views/New Message/Attachments/AttachmentsManager.swift b/Mail/Views/New Message/Attachments/AttachmentsManager.swift index e0f12af06..fd8547de9 100644 --- a/Mail/Views/New Message/Attachments/AttachmentsManager.swift +++ b/Mail/Views/New Message/Attachments/AttachmentsManager.swift @@ -245,7 +245,7 @@ final class AttachmentsManager: ObservableObject { // Cap max number of attachments, API errors out at 100 let attachmentsSlice = attachments[safe: 0 ..< draft.availableAttachmentsSlots] - Task.detached { + Task { try? await self.parallelTaskMapper.map(collection: attachmentsSlice) { attachment in _ = await self.importAttachment(attachment: attachment, disposition: disposition) // TODO: - Manage inline attachment diff --git a/Project.swift b/Project.swift index fd9208bf0..4fb593063 100644 --- a/Project.swift +++ b/Project.swift @@ -24,7 +24,7 @@ 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("f8c11f9a383c84b3c7c3db75afe87a1d077d7dd2")), + .package(url: "https://github.com/Infomaniak/ios-core", .revision("4eaefd644f75d833d6b1009dd94a9d6d674ccb53")), .package(url: "https://github.com/Infomaniak/ios-core-ui", .upToNextMajor(from: "2.5.1")), .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")), From 3028b87fbbeee68a51826200a86cb5a5ffe2c6e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 28 Jul 2023 11:04:52 +0200 Subject: [PATCH 33/61] chore: fix merge --- .package.resolved | 16 ++++++++-------- MailCore/Cache/MailboxManager.swift | 4 +++- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/.package.resolved b/.package.resolved index f1ab23933..97fe687f6 100644 --- a/.package.resolved +++ b/.package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/ProxymanApp/atlantis", "state" : { - "revision" : "f6f7be1330bd4f847b6d52278acd88516eaac471", - "version" : "1.21.1" + "revision" : "cfa72085bce2600b28e47fdbbbfa2d5b96f0392b", + "version" : "1.22.0" } }, { @@ -139,8 +139,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/kean/Nuke", "state" : { - "revision" : "c3864b8882bc69f5edfe5c70e18786c91d228b28", - "version" : "12.1.3" + "revision" : "989586f86b683680f7bd5765d6a5683edbea0c1b", + "version" : "12.1.4" } }, { @@ -175,8 +175,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/getsentry/sentry-cocoa", "state" : { - "revision" : "e46936ed191c0112cd3276e1c10c0bb7f865268e", - "version" : "8.9.1" + "revision" : "259d8bc75aa4028416535d35840ff19fc7661292", + "version" : "8.9.3" } }, { @@ -246,8 +246,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/siteline/SwiftUI-Introspect", "state" : { - "revision" : "730ab9e6cdbb3122ad88277b295c4cecd284a311", - "version" : "0.9.1" + "revision" : "ccb973cfff703cba53fb88197413485c060eb26b", + "version" : "0.10.0" } }, { diff --git a/MailCore/Cache/MailboxManager.swift b/MailCore/Cache/MailboxManager.swift index 16a3e6169..765defb82 100644 --- a/MailCore/Cache/MailboxManager.swift +++ b/MailCore/Cache/MailboxManager.swift @@ -27,6 +27,8 @@ import Sentry import SwiftRegex public final class MailboxManager: ObservableObject { + @LazyInjectService private var snackbarPresenter: SnackBarPresentable + public final class MailboxManagerConstants { private let fileManager = FileManager.default public let rootDocumentsURL: URL @@ -980,7 +982,7 @@ public final 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( From 4216019259192b9b3ba4c5d3f97dc2b3a3a64cef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 28 Jul 2023 17:03:19 +0200 Subject: [PATCH 34/61] chore: PR Feedback --- Mail/Helpers/AppAssembly.swift | 2 -- Mail/Helpers/WorkInProgress.swift | 2 +- MailCore/Utils/Error+Extension.swift | 2 +- .../NotificationServiceAssembly.swift | 2 -- MailShareExtension/ComposeMessageWrapperView.swift | 2 +- 5 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Mail/Helpers/AppAssembly.swift b/Mail/Helpers/AppAssembly.swift index 6b96bc5d9..6aa1f7372 100644 --- a/Mail/Helpers/AppAssembly.swift +++ b/Mail/Helpers/AppAssembly.swift @@ -24,7 +24,6 @@ import InfomaniakDI import InfomaniakLogin import InfomaniakNotifications import MailCore -import os.log private let realmRootPath = "mailboxes" private let appGroupIdentifier = "group.com.infomaniak.mail" @@ -137,7 +136,6 @@ enum ApplicationAssembly { public struct EarlyDIHook { public init() { // setup DI ASAP - os_log("EarlyDIHook") ApplicationAssembly.setupDI() } } diff --git a/Mail/Helpers/WorkInProgress.swift b/Mail/Helpers/WorkInProgress.swift index 15b13d156..246261271 100644 --- a/Mail/Helpers/WorkInProgress.swift +++ b/Mail/Helpers/WorkInProgress.swift @@ -25,6 +25,6 @@ import MailResources // To delete: alert to facilitate tests for beta version @MainActor func showWorkInProgressSnackBar() { - @LazyInjectService var snackbarPresenter: SnackBarPresentable + @InjectService var snackbarPresenter: SnackBarPresentable snackbarPresenter.show(message: MailResourcesStrings.Localizable.workInProgressTitle) } diff --git a/MailCore/Utils/Error+Extension.swift b/MailCore/Utils/Error+Extension.swift index b536b5763..9b484fafc 100644 --- a/MailCore/Utils/Error+Extension.swift +++ b/MailCore/Utils/Error+Extension.swift @@ -39,7 +39,7 @@ public func tryOrDisplayError(_ body: () async throws -> Void) async { } private func displayErrorIfNeeded(error: Error) { - @LazyInjectService var snackbarPresenter: SnackBarPresentable + @InjectService var snackbarPresenter: SnackBarPresentable if let error = error as? MailError { if error.shouldDisplay && !Bundle.main.isExtension { snackbarPresenter.show(message: error.errorDescription) diff --git a/MailNotificationServiceExtension/NotificationServiceAssembly.swift b/MailNotificationServiceExtension/NotificationServiceAssembly.swift index 94dbbd87f..cb4c31813 100644 --- a/MailNotificationServiceExtension/NotificationServiceAssembly.swift +++ b/MailNotificationServiceExtension/NotificationServiceAssembly.swift @@ -20,7 +20,6 @@ import Foundation import InfomaniakCore import InfomaniakDI import MailCore -import OSLog private let realmRootPath = "mailboxes" private let appGroupIdentifier = "group.com.infomaniak.mail" @@ -63,7 +62,6 @@ enum NotificationServiceAssembly { public struct EarlyDIHook { public init() { // setup DI ASAP - os_log("EarlyDIHook") NotificationServiceAssembly.setupDI() } } diff --git a/MailShareExtension/ComposeMessageWrapperView.swift b/MailShareExtension/ComposeMessageWrapperView.swift index d0df5fb31..4550a6caf 100644 --- a/MailShareExtension/ComposeMessageWrapperView.swift +++ b/MailShareExtension/ComposeMessageWrapperView.swift @@ -33,7 +33,7 @@ struct ComposeMessageWrapperView: View { @LazyInjectService private var accountManager: AccountManager init(dismissHandler: @escaping SimpleClosure, itemProviders: [NSItemProvider], draft: Draft = Draft()) { - _draft = State(initialValue: draft) + _draft = State(wrappedValue: draft) // Append save draft action if possible @InjectService var manager: AccountManager From 1da29b49e92098c3e9d55b5fecab70ea5b1a9218 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Fri, 28 Jul 2023 18:29:48 +0200 Subject: [PATCH 35/61] fix(save draft): now works in share ext, was broken by a refactor on master recently --- Mail/Views/New Message/ComposeMessageView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Mail/Views/New Message/ComposeMessageView.swift b/Mail/Views/New Message/ComposeMessageView.swift index b3bf5f6a5..fd7abac01 100644 --- a/Mail/Views/New Message/ComposeMessageView.swift +++ b/Mail/Views/New Message/ComposeMessageView.swift @@ -149,6 +149,10 @@ struct ComposeMessageView: View { } } .onDisappear { + // Only save draft on Disappear on the main app. Share extension manages its own Draft() + guard !Bundle.main.isExtension else { + return + } draftManager.syncDraft(mailboxManager: mailboxManager) } .interactiveDismissDisabled() From ff57e03e8c0c5718aef4486eaaed0402443fdb14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 2 Aug 2023 10:22:12 +0200 Subject: [PATCH 36/61] chore: PR Feedback --- Mail/MailApp.swift | 2 +- Mail/Proxy/Implementation/CacheManager.swift | 2 +- MailCore/Cache/AccountManager.swift | 2 +- MailShareExtension/ShareViewController.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Mail/MailApp.swift b/Mail/MailApp.swift index 4c5a7a29a..40810ee4e 100644 --- a/Mail/MailApp.swift +++ b/Mail/MailApp.swift @@ -105,7 +105,7 @@ struct MailApp: App { try await accountManager.updateUser(for: account) accountManager.enableBugTrackerIfAvailable() - try await accountManager.contactManager?.fetchContactsAndAddressBooks() + try await accountManager.currentContactManager?.fetchContactsAndAddressBooks() } catch { DDLogError("Error while updating user account: \(error)") } diff --git a/Mail/Proxy/Implementation/CacheManager.swift b/Mail/Proxy/Implementation/CacheManager.swift index 7a22a43ac..3010b7d1f 100644 --- a/Mail/Proxy/Implementation/CacheManager.swift +++ b/Mail/Proxy/Implementation/CacheManager.swift @@ -36,7 +36,7 @@ public final class CacheManager: CacheManageable { try await accountManager.updateUser(for: currentAccount) accountManager.enableBugTrackerIfAvailable() - try await accountManager.contactManager?.fetchContactsAndAddressBooks() + try await accountManager.currentContactManager?.fetchContactsAndAddressBooks() } catch { DDLogError("Error while updating user account: \(error)") } diff --git a/MailCore/Cache/AccountManager.swift b/MailCore/Cache/AccountManager.swift index c6cc1fdbb..07d8df1dc 100644 --- a/MailCore/Cache/AccountManager.swift +++ b/MailCore/Cache/AccountManager.swift @@ -109,7 +109,7 @@ public final class AccountManager: RefreshTokenDelegate, ObservableObject { } /// Shorthand for `currentMailboxManager?.contactManager` - public var contactManager: ContactManager? { + public var currentContactManager: ContactManager? { currentMailboxManager?.contactManager } diff --git a/MailShareExtension/ShareViewController.swift b/MailShareExtension/ShareViewController.swift index 62b235cea..d4e44608a 100644 --- a/MailShareExtension/ShareViewController.swift +++ b/MailShareExtension/ShareViewController.swift @@ -58,7 +58,7 @@ final class ShareNavigationViewController: UIViewController { } /// make sure we load the contact list asap. - if let currentContactManager = accountManager.contactManager { + if let currentContactManager = accountManager.currentContactManager { Task { try await currentContactManager.fetchContactsAndAddressBooks() } From e5f14965868ae5be5a07bff812564ce4f2dffdb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 2 Aug 2023 10:24:22 +0200 Subject: [PATCH 37/61] chore: PR Feedback --- Mail/Components/MailboxListView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Mail/Components/MailboxListView.swift b/Mail/Components/MailboxListView.swift index e3bcc97ee..1093d7461 100644 --- a/Mail/Components/MailboxListView.swift +++ b/Mail/Components/MailboxListView.swift @@ -20,7 +20,6 @@ import MailCore import MailResources import RealmSwift import SwiftUI -import InfomaniakDI struct MailboxListView: View { @EnvironmentObject private var mailboxManager: MailboxManager From ab41ae9682fc8c1c4ee8407fd72bea4d0a8d2b53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 2 Aug 2023 10:52:56 +0200 Subject: [PATCH 38/61] chore: i18n bump --- .../Localizable/de.lproj/Localizable.strings | 86 ++++++++++++++++++- .../Localizable/en.lproj/Localizable.strings | 86 ++++++++++++++++++- .../Localizable/es.lproj/Localizable.strings | 86 ++++++++++++++++++- .../Localizable/fr.lproj/Localizable.strings | 86 ++++++++++++++++++- .../Localizable/it.lproj/Localizable.strings | 86 ++++++++++++++++++- .../ComposeMessageWrapperView.swift | 4 +- 6 files changed, 417 insertions(+), 17 deletions(-) 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/ComposeMessageWrapperView.swift b/MailShareExtension/ComposeMessageWrapperView.swift index 4550a6caf..3fc91ecab 100644 --- a/MailShareExtension/ComposeMessageWrapperView.swift +++ b/MailShareExtension/ComposeMessageWrapperView.swift @@ -19,6 +19,7 @@ import InfomaniakCore import InfomaniakCoreUI import InfomaniakDI +import MailResources import MailCore import Social import SwiftUI @@ -78,8 +79,7 @@ struct PleaseLoginView: View { .scaledToFit() .frame(height: UIConstants.onboardingLogoHeight) .padding(.top, UIConstants.onboardingLogoPaddingTop) - // TODO: i18n - Text("Please login in ikMail first") + Text(MailResourcesStrings.Localizable.pleaseLogInFirst) .textStyle(.header2) .padding(.top, UIConstants.onboardingLogoPaddingTop) LottieView(configuration: slide.lottieConfiguration!) From 90b104ef8869d4208e21e6d91b3b0f1eeee389e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 2 Aug 2023 11:07:55 +0200 Subject: [PATCH 39/61] chore: PR Feedback --- Mail/Helpers/AppAssembly.swift | 3 -- .../Implementation/RootViewController.swift | 32 ------------------- Mail/Views/Settings/SettingsOptionView.swift | 1 - Mail/Views/Switch User/AccountView.swift | 1 - .../Proxy/RootViewManager.swift | 32 ------------------- 5 files changed, 69 deletions(-) delete mode 100644 Mail/Proxy/Implementation/RootViewController.swift delete mode 100644 MailShareExtension/Proxy/RootViewManager.swift diff --git a/Mail/Helpers/AppAssembly.swift b/Mail/Helpers/AppAssembly.swift index 6aa1f7372..ddfe81524 100644 --- a/Mail/Helpers/AppAssembly.swift +++ b/Mail/Helpers/AppAssembly.swift @@ -120,9 +120,6 @@ enum ApplicationAssembly { Factory(type: RemoteNotificationRegistrable.self) { _, _ in RemoteNotificationRegistrer() }, - Factory(type: RootViewManageable.self) { _, _ in - RootViewManager() - }, Factory(type: URLNavigable.self) { _, _ in URLNavigator() } diff --git a/Mail/Proxy/Implementation/RootViewController.swift b/Mail/Proxy/Implementation/RootViewController.swift deleted file mode 100644 index 3b05ce531..000000000 --- a/Mail/Proxy/Implementation/RootViewController.swift +++ /dev/null @@ -1,32 +0,0 @@ -/* - Infomaniak Mail - iOS App - Copyright (C) 2022 Infomaniak Network SA - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -import Foundation -import InfomaniakCoreUI -import UIKit - -@available(iOSApplicationExtension, unavailable) -public struct RootViewManager: RootViewManageable { - public var rootViewController: UIViewController? { - mainSceneKeyWindow?.rootViewController - } - - public var mainSceneKeyWindow: UIWindow? { - UIApplication.shared.mainSceneKeyWindow - } -} diff --git a/Mail/Views/Settings/SettingsOptionView.swift b/Mail/Views/Settings/SettingsOptionView.swift index 8e6d2b3c6..ebbe3f874 100644 --- a/Mail/Views/Settings/SettingsOptionView.swift +++ b/Mail/Views/Settings/SettingsOptionView.swift @@ -34,7 +34,6 @@ struct SettingsOptionView: View where OptionEnum: CaseIterable, Opti private let matomoValue: Float? private let matomoName: KeyPath? - @LazyInjectService private var rootViewManager: RootViewManageable @LazyInjectService private var matomo: MatomoUtils @State private var values: [OptionEnum] diff --git a/Mail/Views/Switch User/AccountView.swift b/Mail/Views/Switch User/AccountView.swift index 7e925da9c..820855b3d 100644 --- a/Mail/Views/Switch User/AccountView.swift +++ b/Mail/Views/Switch User/AccountView.swift @@ -26,7 +26,6 @@ import Sentry import SwiftUI final class AccountViewDelegate: DeleteAccountDelegate { - @LazyInjectService private var rootViewManager: RootViewManageable @LazyInjectService private var accountManager: AccountManager @LazyInjectService private var snackbarPresenter: SnackBarPresentable diff --git a/MailShareExtension/Proxy/RootViewManager.swift b/MailShareExtension/Proxy/RootViewManager.swift deleted file mode 100644 index 55c738fcb..000000000 --- a/MailShareExtension/Proxy/RootViewManager.swift +++ /dev/null @@ -1,32 +0,0 @@ -/* - Infomaniak Mail - iOS App - Copyright (C) 2022 Infomaniak Network SA - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -import Foundation -import InfomaniakCoreUI -import UIKit - -/// A RootViewManager that works in Extension mode -public struct RootViewManager: RootViewManageable { - public var rootViewController: UIViewController? { - nil - } - - public var mainSceneKeyWindow: UIWindow? { - nil - } -} From 5671159ebab3307d2a6b78ada0e8e26fa296a3a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 2 Aug 2023 14:27:10 +0200 Subject: [PATCH 40/61] chore: PR Feedback --- Mail/Views/Bottom sheets/Actions/ActionsViewModel.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Mail/Views/Bottom sheets/Actions/ActionsViewModel.swift b/Mail/Views/Bottom sheets/Actions/ActionsViewModel.swift index aa836c87a..6d05b9887 100644 --- a/Mail/Views/Bottom sheets/Actions/ActionsViewModel.swift +++ b/Mail/Views/Bottom sheets/Actions/ActionsViewModel.swift @@ -239,7 +239,6 @@ enum ActionsTarget: Equatable, Identifiable { @Published var listActions: [Action] = [] @LazyInjectService private var matomo: MatomoUtils - @LazyInjectService private var accountManager: AccountManager @LazyInjectService private var snackbarPresenter: SnackBarPresentable init(mailboxManager: MailboxManager, From 95747aef125f40dab16febc523516a9002bf1965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 2 Aug 2023 14:46:41 +0200 Subject: [PATCH 41/61] chore(URLNavigable): PR Feedback --- Mail/Helpers/AppAssembly.swift | 3 -- Mail/Proxy/Implementation/URLNavigator.swift | 33 ------------------- Mail/Proxy/Protocols/URLNavigable.swift | 29 ---------------- .../Items/MenuDrawerItemsListView.swift | 9 ++--- ...ettingsNotificationsInstructionsView.swift | 7 ++-- .../General/SettingsNotificationsView.swift | 5 +-- MailShareExtension/Proxy/URLNavigator.swift | 31 ----------------- 7 files changed, 11 insertions(+), 106 deletions(-) delete mode 100644 Mail/Proxy/Implementation/URLNavigator.swift delete mode 100644 Mail/Proxy/Protocols/URLNavigable.swift delete mode 100644 MailShareExtension/Proxy/URLNavigator.swift diff --git a/Mail/Helpers/AppAssembly.swift b/Mail/Helpers/AppAssembly.swift index ddfe81524..a317dda47 100644 --- a/Mail/Helpers/AppAssembly.swift +++ b/Mail/Helpers/AppAssembly.swift @@ -119,9 +119,6 @@ enum ApplicationAssembly { }, Factory(type: RemoteNotificationRegistrable.self) { _, _ in RemoteNotificationRegistrer() - }, - Factory(type: URLNavigable.self) { _, _ in - URLNavigator() } ] diff --git a/Mail/Proxy/Implementation/URLNavigator.swift b/Mail/Proxy/Implementation/URLNavigator.swift deleted file mode 100644 index 1488b6186..000000000 --- a/Mail/Proxy/Implementation/URLNavigator.swift +++ /dev/null @@ -1,33 +0,0 @@ -/* - Infomaniak Mail - iOS App - Copyright (C) 2022 Infomaniak Network SA - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -import Foundation -import UIKit - -@available(iOSApplicationExtension, unavailable) -public struct URLNavigator: URLNavigable { - public func openUrl(_ url: URL) { - UIApplication.shared.open(url) - } - - public func openUrlIfPossible(_ url: URL) { - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url) - } - } -} diff --git a/Mail/Proxy/Protocols/URLNavigable.swift b/Mail/Proxy/Protocols/URLNavigable.swift deleted file mode 100644 index 49af98131..000000000 --- a/Mail/Proxy/Protocols/URLNavigable.swift +++ /dev/null @@ -1,29 +0,0 @@ -/* - Infomaniak Mail - iOS App - Copyright (C) 2022 Infomaniak Network SA - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -import Foundation -import UIKit - -/// Something that can open URL in an abstract way -public protocol URLNavigable { - /// Try to open an URL - func openUrl(_ url: URL) - - /// Check if app can open URL first, then try to open it - func openUrlIfPossible(_ url: URL) -} diff --git a/Mail/Views/Menu Drawer/Items/MenuDrawerItemsListView.swift b/Mail/Views/Menu Drawer/Items/MenuDrawerItemsListView.swift index 70e454a84..74364db60 100644 --- a/Mail/Views/Menu Drawer/Items/MenuDrawerItemsListView.swift +++ b/Mail/Views/Menu Drawer/Items/MenuDrawerItemsListView.swift @@ -27,7 +27,7 @@ import SwiftUI struct MenuDrawerItemsAdvancedListView: View { @State private var isShowingRestoreMails = false - @LazyInjectService var urlNavigator: URLNavigable + @Environment(\.openURL) private var openURL let mailboxCanRestoreEmails: Bool @@ -37,7 +37,7 @@ struct MenuDrawerItemsAdvancedListView: View { MenuDrawerItemCell(icon: MailResourcesAsset.drawerDownload, label: MailResourcesStrings.Localizable.buttonImportEmails, matomoName: "importEmails") { - urlNavigator.openUrl(URLConstants.importMails.url) + openURL.callAsFunction(URLConstants.importMails.url) } if mailboxCanRestoreEmails { MenuDrawerItemCell( @@ -58,10 +58,11 @@ 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 - @LazyInjectService private var urlNavigator: URLNavigable @LazyInjectService private var accountManager: AccountManager var body: some View { @@ -90,7 +91,7 @@ struct MenuDrawerItemsHelpListView: View { if mailboxManager.account.user?.isStaff == true { isShowingBugTracker.toggle() } else if let userReportURL = URL(string: MailResourcesStrings.Localizable.urlUserReportiOS) { - urlNavigator.openUrl(userReportURL) + openURL.callAsFunction(userReportURL) } } } diff --git a/Mail/Views/Settings/General/SettingsNotificationsInstructionsView.swift b/Mail/Views/Settings/General/SettingsNotificationsInstructionsView.swift index e26dc8cdb..b0602db50 100644 --- a/Mail/Views/Settings/General/SettingsNotificationsInstructionsView.swift +++ b/Mail/Views/Settings/General/SettingsNotificationsInstructionsView.swift @@ -16,15 +16,14 @@ along with this program. If not, see . */ +import InfomaniakDI import MailResources import SwiftUI -import InfomaniakDI struct SettingsNotificationsInstructionsView: View { @Environment(\.dismiss) private var dismiss + @Environment(\.openURL) var openURL - @LazyInjectService private var urlNavigator: URLNavigable - var body: some View { VStack(alignment: .leading, spacing: 24) { Text(MailResourcesStrings.Localizable.alertNotificationsDisabledTitle) @@ -43,7 +42,7 @@ struct SettingsNotificationsInstructionsView: View { return } - urlNavigator.openUrlIfPossible(settingsUrl) + openURL.callAsFunction(settingsUrl) } } diff --git a/Mail/Views/Settings/General/SettingsNotificationsView.swift b/Mail/Views/Settings/General/SettingsNotificationsView.swift index adf487749..762b7d9b5 100644 --- a/Mail/Views/Settings/General/SettingsNotificationsView.swift +++ b/Mail/Views/Settings/General/SettingsNotificationsView.swift @@ -27,11 +27,12 @@ import SwiftUI struct SettingsNotificationsView: View { @LazyInjectService private var notificationService: InfomaniakNotifications @LazyInjectService private var matomo: MatomoUtils - @LazyInjectService private var urlNavigator: URLNavigable @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]? @@ -52,7 +53,7 @@ struct SettingsNotificationsView: View { return } - urlNavigator.openUrlIfPossible(settingsUrl) + openURL.callAsFunction(settingsUrl) } .mailButtonStyle(.link) } diff --git a/MailShareExtension/Proxy/URLNavigator.swift b/MailShareExtension/Proxy/URLNavigator.swift deleted file mode 100644 index edfd7f3c8..000000000 --- a/MailShareExtension/Proxy/URLNavigator.swift +++ /dev/null @@ -1,31 +0,0 @@ -/* - Infomaniak Mail - iOS App - Copyright (C) 2022 Infomaniak Network SA - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - */ - -import Foundation -import UIKit - -/// An URLNavigator that works in Extension mode -public struct URLNavigator: URLNavigable { - public func openUrl(_ url: URL) { - // NOOP in share extension - } - - public func openUrlIfPossible(_ url: URL) { - // NOOP in share extension - } -} From d759108611e435958b606dbec1d0299be0c13680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 2 Aug 2023 15:07:17 +0200 Subject: [PATCH 42/61] chore: PR Feedback --- .../New Message/AutocompletionView.swift | 3 --- .../New Message/ComposeMessageBodyView.swift | 1 - Mail/Views/Search/SearchViewModel.swift | 3 +-- Mail/Views/Switch User/AccountView.swift | 1 - .../Thread List/ThreadListModifiers.swift | 1 - MailCore/Models/MergedContact.swift | 3 --- MailCore/Models/Recipient.swift | 2 -- .../NotificationService.swift | 21 ------------------- 8 files changed, 1 insertion(+), 34 deletions(-) diff --git a/Mail/Views/New Message/AutocompletionView.swift b/Mail/Views/New Message/AutocompletionView.swift index 948c6e799..b1a41d6da 100644 --- a/Mail/Views/New Message/AutocompletionView.swift +++ b/Mail/Views/New Message/AutocompletionView.swift @@ -17,7 +17,6 @@ */ import Combine -import InfomaniakDI import MailCore import MailResources import RealmSwift @@ -33,8 +32,6 @@ struct AutocompletionView: View { @Binding var autocompletion: [Recipient] @Binding var addedRecipients: RealmSwift.List - @LazyInjectService private var accountManager: AccountManager - let addRecipient: @MainActor (Recipient) -> Void var body: some View { diff --git a/Mail/Views/New Message/ComposeMessageBodyView.swift b/Mail/Views/New Message/ComposeMessageBodyView.swift index a5b45bc83..ece4799c2 100644 --- a/Mail/Views/New Message/ComposeMessageBodyView.swift +++ b/Mail/Views/New Message/ComposeMessageBodyView.swift @@ -17,7 +17,6 @@ */ import InfomaniakCoreUI -import InfomaniakDI import MailCore import RealmSwift import SwiftUI diff --git a/Mail/Views/Search/SearchViewModel.swift b/Mail/Views/Search/SearchViewModel.swift index 3dbe95a79..7f615b526 100644 --- a/Mail/Views/Search/SearchViewModel.swift +++ b/Mail/Views/Search/SearchViewModel.swift @@ -97,8 +97,7 @@ enum SearchState { @Published var isLoading = false @LazyInjectService var matomo: MatomoUtils - @LazyInjectService private var accountManager: AccountManager - + let searchFolder: Folder var resourceNext: String? var lastSearch = "" diff --git a/Mail/Views/Switch User/AccountView.swift b/Mail/Views/Switch User/AccountView.swift index 820855b3d..325c219b8 100644 --- a/Mail/Views/Switch User/AccountView.swift +++ b/Mail/Views/Switch User/AccountView.swift @@ -50,7 +50,6 @@ final class AccountViewDelegate: DeleteAccountDelegate { struct AccountView: View { @LazyInjectService private var matomo: MatomoUtils @LazyInjectService private var tokenStore: TokenStore - @LazyInjectService private var accountManager: AccountManager @Environment(\.dismiss) private var dismiss diff --git a/Mail/Views/Thread List/ThreadListModifiers.swift b/Mail/Views/Thread List/ThreadListModifiers.swift index 8914ff11b..a6fe0130a 100644 --- a/Mail/Views/Thread List/ThreadListModifiers.swift +++ b/Mail/Views/Thread List/ThreadListModifiers.swift @@ -54,7 +54,6 @@ struct ThreadListCellAppearance: ViewModifier { struct ThreadListToolbar: ViewModifier { @LazyInjectService private var matomo: MatomoUtils - @LazyInjectService private var accountManager: AccountManager @Environment(\.isCompactWindow) private var isCompactWindow diff --git a/MailCore/Models/MergedContact.swift b/MailCore/Models/MergedContact.swift index 0e8295139..4d06a0321 100644 --- a/MailCore/Models/MergedContact.swift +++ b/MailCore/Models/MergedContact.swift @@ -19,7 +19,6 @@ import Contacts import Foundation import InfomaniakCore -import InfomaniakDI import Nuke import RealmSwift import SwiftUI @@ -37,8 +36,6 @@ extension CNContact { } public final class MergedContact { - @LazyInjectService private var accountManager: AccountManager - private static let contactFormatter = CNContactFormatter() public var email: String diff --git a/MailCore/Models/Recipient.swift b/MailCore/Models/Recipient.swift index 6aa0afda9..3b5c5cd9d 100644 --- a/MailCore/Models/Recipient.swift +++ b/MailCore/Models/Recipient.swift @@ -17,7 +17,6 @@ */ import Foundation -import InfomaniakDI import InfomaniakCore import MailResources import Nuke @@ -74,7 +73,6 @@ public final class Recipient: EmbeddedObject, Codable { return recipients } - func isCurrentUser(currentAccountEmail: String) -> Bool { return currentAccountEmail == email } diff --git a/MailNotificationServiceExtension/NotificationService.swift b/MailNotificationServiceExtension/NotificationService.swift index 5591cfea2..71e30e971 100644 --- a/MailNotificationServiceExtension/NotificationService.swift +++ b/MailNotificationServiceExtension/NotificationService.swift @@ -37,27 +37,6 @@ final class NotificationService: UNNotificationServiceExtension { 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? { From c4786a38df117b13463a21cc636dcea525b9cab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 2 Aug 2023 15:37:44 +0200 Subject: [PATCH 43/61] chore: PR Feedback. (IKSnackBarAvoider/ShareViewController) --- Mail/Helpers/AppAssembly.swift | 4 ++-- Mail/Utils/SnackBarAwareModifier.swift | 3 ++- .../Utils/SnackBar/IKSnackBar+Extension.swift | 19 +------------------ MailShareExtension/ShareViewController.swift | 10 ++++++---- 4 files changed, 11 insertions(+), 25 deletions(-) diff --git a/Mail/Helpers/AppAssembly.swift b/Mail/Helpers/AppAssembly.swift index a317dda47..f329a2631 100644 --- a/Mail/Helpers/AppAssembly.swift +++ b/Mail/Helpers/AppAssembly.swift @@ -67,8 +67,8 @@ enum ApplicationAssembly { Factory(type: MatomoUtils.self) { _, _ in MatomoUtils(siteId: Constants.matomoId, baseURL: URLConstants.matomo.url) }, - Factory(type: SnackBarAvoider.self) { _, _ in - SnackBarAvoider() + Factory(type: IKSnackBarAvoider.self) { _, _ in + IKSnackBarAvoider() }, Factory(type: DraftManager.self) { _, _ in DraftManager() 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/MailCore/Utils/SnackBar/IKSnackBar+Extension.swift b/MailCore/Utils/SnackBar/IKSnackBar+Extension.swift index 29070c099..98b915f55 100644 --- a/MailCore/Utils/SnackBar/IKSnackBar+Extension.swift +++ b/MailCore/Utils/SnackBar/IKSnackBar+Extension.swift @@ -41,23 +41,6 @@ public extension SnackBarStyle { } } -// TODO: delete -public final 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 @@ -76,7 +59,7 @@ public extension IKSnackBar { anchor: CGFloat = 0, contextView: UIView? = nil ) -> IKSnackBar? { - @LazyInjectService var avoider: SnackBarAvoider + @LazyInjectService var avoider: IKSnackBarAvoider let snackbar: IKSnackBar? if let contextView = contextView { diff --git a/MailShareExtension/ShareViewController.swift b/MailShareExtension/ShareViewController.swift index d4e44608a..8eee66e86 100644 --- a/MailShareExtension/ShareViewController.swift +++ b/MailShareExtension/ShareViewController.swift @@ -65,10 +65,12 @@ final class ShareNavigationViewController: UIViewController { } // We need to go threw wrapping to use SwiftUI in an NSExtension. - let hostingController = UIHostingController(rootView: ComposeMessageWrapperView(dismissHandler: { - self.dismiss(animated: true) - }, - itemProviders: itemProviders)) + 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) From f0883f3f645bf70bc43001be0b0e34af3efe314c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 2 Aug 2023 16:43:10 +0200 Subject: [PATCH 44/61] fix: crash in NotificationServiceAssembly --- MailCore/Cache/AccountManager.swift | 2 +- .../NotificationServiceAssembly.swift | 34 ++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/MailCore/Cache/AccountManager.swift b/MailCore/Cache/AccountManager.swift index 07d8df1dc..8a66256fe 100644 --- a/MailCore/Cache/AccountManager.swift +++ b/MailCore/Cache/AccountManager.swift @@ -218,7 +218,7 @@ public final 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/MailNotificationServiceExtension/NotificationServiceAssembly.swift b/MailNotificationServiceExtension/NotificationServiceAssembly.swift index cb4c31813..7ffd657c6 100644 --- a/MailNotificationServiceExtension/NotificationServiceAssembly.swift +++ b/MailNotificationServiceExtension/NotificationServiceAssembly.swift @@ -19,6 +19,8 @@ import Foundation import InfomaniakCore import InfomaniakDI +import InfomaniakLogin +import InfomaniakNotifications import MailCore private let realmRootPath = "mailboxes" @@ -39,9 +41,36 @@ enum NotificationServiceAssembly { 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: MessagePresentable.self) { _, _ in + MessagePresenter() + }, + Factory(type: UserActivityController.self) { _, _ in + UserActivityController() + }, + Factory(type: PlatformDetectable.self) { _, _ in + PlatformDetector() + }, Factory(type: AppGroupPathProvidable.self) { _, _ in guard let provider = AppGroupPathProvider( realmRootPath: realmRootPath, @@ -51,7 +80,10 @@ enum NotificationServiceAssembly { } return provider - } + }, + Factory(type: TokenStore.self) { _, _ in + TokenStore() + }, ] factories.registerFactoriesInDI() From 986854d40e219396bcbf293023a6142dc261065b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Wed, 2 Aug 2023 18:07:19 +0200 Subject: [PATCH 45/61] fix(sending snackbar): Now when sending / saving / discarding a draft from the app or share extension it does make sense --- MailCore/Cache/DraftManager.swift | 50 ++++++++++++++++--- MailCore/Utils/Error+Extension.swift | 10 ++-- .../ComposeMessageWrapperView.swift | 8 ++- 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/MailCore/Cache/DraftManager.swift b/MailCore/Cache/DraftManager.swift index 3ae7ec756..52384e481 100644 --- a/MailCore/Cache/DraftManager.swift +++ b/MailCore/Cache/DraftManager.swift @@ -125,7 +125,7 @@ public final class DraftManager { var sendDate: Date? switch draft.action { case .initialSave: - await self.initialSaveRemotely(draft: draft, mailboxManager: mailboxManager) + await self.initialSaveRemotelyAndNotify(draft: draft, mailboxManager: mailboxManager) case .save: await self.saveDraftRemotely(draft: draft, mailboxManager: mailboxManager) case .send: @@ -148,23 +148,57 @@ public final class DraftManager { } } - /// First save of a draft with the remote if needed + /// Process a `draft` when the ShareExtension dismisses. + /// - Parameters: + /// - draft: Expecting a .detached draft + /// - mailboxManager: the mailbox manager + public func saveAndProcessDraftFromShareExtension(draft: Draft, mailboxManager: MailboxManager) { + Task { + let saved = await self.initialSaveRemotelyIfNonEmpty(draft: draft, mailboxManager: mailboxManager) + + // No message for empty draft + guard saved else { + return + } + + // Present a matching message + @InjectService var messagePresentable: MessagePresentable + if draft.action == .send { + messagePresentable.show(message: MailResourcesStrings.Localizable.snackbarEmailSending) + } else { + messagePresentable.show(message: MailResourcesStrings.Localizable.snackbarDraftSaved) + } + } + } + + /// First save of a draft with the remote if non empty @discardableResult - public func initialSaveRemotely(draft: Draft, mailboxManager: MailboxManager) async -> Bool { + private func initialSaveRemotelyIfNonEmpty(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: MessageAction = (MailResourcesStrings.Localizable.actionDelete, { [weak self] in - self?.matomo.track(eventWithCategory: .snackbar, name: "deleteDraft") - self?.deleteDraftSnackBarAction(draft: draft, mailboxManager: mailboxManager) - }) - messagePresentable.show(message: MailResourcesStrings.Localizable.snackbarDraftSaved, action: messageAction) return true } + /// First save of a draft with the remote if non empty. + /// + /// Present a message with a `delete draft` action + @discardableResult + public func initialSaveRemotelyAndNotify(draft: Draft, mailboxManager: MailboxManager) async -> Bool { + let saved = await initialSaveRemotelyIfNonEmpty(draft: draft, mailboxManager: mailboxManager) + if saved { + let messageAction: MessageAction = (MailResourcesStrings.Localizable.actionDelete, { [weak self] in + self?.matomo.track(eventWithCategory: .snackbar, name: "deleteDraft") + self?.deleteDraftSnackBarAction(draft: draft, mailboxManager: mailboxManager) + }) + messagePresentable.show(message: MailResourcesStrings.Localizable.snackbarDraftSaved, action: messageAction) + } + return saved + } + /// Check multiple conditions to infer if a draft is empty or not private func isDraftEmpty(draft: Draft) -> Bool { guard isDraftBodyEmptyOfAttachments(draft: draft) else { diff --git a/MailCore/Utils/Error+Extension.swift b/MailCore/Utils/Error+Extension.swift index 9b484fafc..fa1782784 100644 --- a/MailCore/Utils/Error+Extension.swift +++ b/MailCore/Utils/Error+Extension.swift @@ -41,7 +41,7 @@ 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 { + if error.shouldDisplay { snackbarPresenter.show(message: error.errorDescription) } else { SentrySDK.capture(message: "Encountered error that we didn't display to the user") { scope in @@ -52,7 +52,7 @@ private func displayErrorIfNeeded(error: Error) { } } DDLogError("MailError: \(error)") - } else if error.shouldDisplay && !Bundle.main.isExtension { + } else if error.shouldDisplay { snackbarPresenter.show(message: error.localizedDescription) DDLogError("Error: \(error)") } @@ -60,13 +60,17 @@ private func displayErrorIfNeeded(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/MailShareExtension/ComposeMessageWrapperView.swift b/MailShareExtension/ComposeMessageWrapperView.swift index 3fc91ecab..a13efa144 100644 --- a/MailShareExtension/ComposeMessageWrapperView.swift +++ b/MailShareExtension/ComposeMessageWrapperView.swift @@ -40,11 +40,9 @@ struct ComposeMessageWrapperView: View { @InjectService var manager: AccountManager if let mailboxManager = manager.currentMailboxManager { let saveDraft: SimpleClosure = { _ in - let detached = draft.detached() - Task { - @InjectService var draftManager: DraftManager - _ = await draftManager.initialSaveRemotely(draft: detached, mailboxManager: mailboxManager) - } + let detachedDraft = draft.detached() + @InjectService var draftManager: DraftManager + draftManager.saveAndProcessDraftFromShareExtension(draft: detachedDraft, mailboxManager: mailboxManager) } self.dismissHandler = saveDraft + dismissHandler } else { From 02a87487a9adb35ec55721009df894139cf7dac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 3 Aug 2023 09:45:01 +0200 Subject: [PATCH 46/61] chore: swiftformat . --- Mail/Components/UnavailableMailboxListView.swift | 6 +++--- Mail/Proxy/Implementation/ApplicationState.swift | 2 +- Mail/Proxy/Implementation/OrientationLock.swift | 2 +- Mail/Proxy/Protocols/OrientationManageable.swift | 2 +- .../Views/Alerts/DetachMailboxConfirmationView.swift | 2 +- .../Menu Drawer/MailboxManagement/MailboxCell.swift | 3 +-- Mail/Views/New Message/ComposeMessageView+Init.swift | 3 ++- .../New Message/Recipients/FullRecipientsList.swift | 4 ++-- Mail/Views/Settings/SettingsOptionView.swift | 2 +- Mail/Views/Switch User/AccountCellView.swift | 2 +- Mail/Views/Switch User/AddMailboxView.swift | 2 +- Mail/Views/Thread/ThreadView.swift | 6 ++++-- MailCore/Cache/AccountManager.swift | 2 +- MailCore/Models/Thread.swift | 3 ++- MailCore/Utils/URLSchemeHandler.swift | 2 +- MailShareExtension/ComposeMessageWrapperView.swift | 2 +- MailShareExtension/Proxy/ApplicationState.swift | 2 +- Project.swift | 12 +++++++++--- Tuist/ProjectDescriptionHelpers/Constants.swift | 12 ++++++------ .../ProjectDescriptionHelpers/ExtensionTarget.swift | 2 +- 20 files changed, 41 insertions(+), 32 deletions(-) diff --git a/Mail/Components/UnavailableMailboxListView.swift b/Mail/Components/UnavailableMailboxListView.swift index 6555bdf5a..38fb3d1b7 100644 --- a/Mail/Components/UnavailableMailboxListView.swift +++ b/Mail/Components/UnavailableMailboxListView.swift @@ -16,11 +16,11 @@ along with this program. If not, see . */ +import InfomaniakDI import MailCore import MailResources import RealmSwift import SwiftUI -import InfomaniakDI struct UnavailableMailboxListView: View { @AppStorage(UserDefaults.shared.key(.accentColor)) private var accentColor = DefaultPreferences.accentColor @@ -30,7 +30,7 @@ struct UnavailableMailboxListView: View { configuration: MailboxInfosManager.instance.realmConfiguration, where: { mailbox in @InjectService var accountManager: AccountManager - return (mailbox.userId == accountManager.currentUserId && mailbox.isPasswordValid == false) + return mailbox.userId == accountManager.currentUserId && mailbox.isPasswordValid == false }, sortDescriptor: SortDescriptor(keyPath: \Mailbox.mailboxId) ) private var passwordBlockedMailboxes @@ -40,7 +40,7 @@ struct UnavailableMailboxListView: View { configuration: MailboxInfosManager.instance.realmConfiguration, where: { mailbox in @InjectService var accountManager: AccountManager - return (mailbox.userId == accountManager.currentUserId && mailbox.isLocked == true) + return mailbox.userId == accountManager.currentUserId && mailbox.isLocked == true }, sortDescriptor: SortDescriptor(keyPath: \Mailbox.mailboxId) ) private var lockedMailboxes diff --git a/Mail/Proxy/Implementation/ApplicationState.swift b/Mail/Proxy/Implementation/ApplicationState.swift index 093238df3..b15fd87f4 100644 --- a/Mail/Proxy/Implementation/ApplicationState.swift +++ b/Mail/Proxy/Implementation/ApplicationState.swift @@ -16,8 +16,8 @@ along with this program. If not, see . */ -import UIKit import MailCore +import UIKit public struct ApplicationState: ApplicationStatable { public var applicationState: UIApplication.State? { diff --git a/Mail/Proxy/Implementation/OrientationLock.swift b/Mail/Proxy/Implementation/OrientationLock.swift index e4352b0bc..61c8445da 100644 --- a/Mail/Proxy/Implementation/OrientationLock.swift +++ b/Mail/Proxy/Implementation/OrientationLock.swift @@ -28,7 +28,7 @@ public final class OrientationManager: OrientationManageable { public func setOrientationLock(_ orientation: UIInterfaceOrientationMask) { orientationLock = orientation } - + public var interfaceOrientation: UIInterfaceOrientation? { UIApplication.shared.mainSceneKeyWindow?.windowScene?.interfaceOrientation } diff --git a/Mail/Proxy/Protocols/OrientationManageable.swift b/Mail/Proxy/Protocols/OrientationManageable.swift index ef2b359b9..760162cf1 100644 --- a/Mail/Proxy/Protocols/OrientationManageable.swift +++ b/Mail/Proxy/Protocols/OrientationManageable.swift @@ -26,7 +26,7 @@ public protocol OrientationManageable { /// Set the orientation lock mask func setOrientationLock(_ orientation: UIInterfaceOrientationMask) - + /// Read the interface orientation var interfaceOrientation: UIInterfaceOrientation? { get } } diff --git a/Mail/Views/Alerts/DetachMailboxConfirmationView.swift b/Mail/Views/Alerts/DetachMailboxConfirmationView.swift index 6c6fc78ed..87ac9373c 100644 --- a/Mail/Views/Alerts/DetachMailboxConfirmationView.swift +++ b/Mail/Views/Alerts/DetachMailboxConfirmationView.swift @@ -24,7 +24,7 @@ import SwiftUI struct DetachMailboxConfirmationView: View { @LazyInjectService private var accountManager: AccountManager - + @EnvironmentObject private var navigationState: NavigationState let mailbox: Mailbox diff --git a/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift b/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift index 4bf794923..555dd84ce 100644 --- a/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift +++ b/Mail/Views/Menu Drawer/MailboxManagement/MailboxCell.swift @@ -42,11 +42,10 @@ extension View { struct MailboxCell: View { @LazyInjectService private var accountManager: AccountManager - + @Environment(\.mailboxCellStyle) private var style: Style @EnvironmentObject private var navigationDrawerState: NavigationDrawerState - @State private var isShowingLockedView = false @State private var isShowingUpdatePasswordView = false diff --git a/Mail/Views/New Message/ComposeMessageView+Init.swift b/Mail/Views/New Message/ComposeMessageView+Init.swift index 7c0b3ff42..e3a53cd65 100644 --- a/Mail/Views/New Message/ComposeMessageView+Init.swift +++ b/Mail/Views/New Message/ComposeMessageView+Init.swift @@ -23,7 +23,8 @@ import MailCore import RealmSwift extension ComposeMessageView { - static func newMessage(_ draft: Draft, mailboxManager: MailboxManager, itemProviders: [NSItemProvider] = []) -> ComposeMessageView { + static func newMessage(_ draft: Draft, mailboxManager: MailboxManager, + itemProviders: [NSItemProvider] = []) -> ComposeMessageView { return ComposeMessageView(draft: draft, mailboxManager: mailboxManager, attachments: itemProviders) } 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/Settings/SettingsOptionView.swift b/Mail/Views/Settings/SettingsOptionView.swift index ebbe3f874..56922d9a3 100644 --- a/Mail/Views/Settings/SettingsOptionView.swift +++ b/Mail/Views/Settings/SettingsOptionView.swift @@ -35,7 +35,7 @@ struct SettingsOptionView: View where OptionEnum: CaseIterable, Opti private let matomoName: KeyPath? @LazyInjectService private var matomo: MatomoUtils - + @State private var values: [OptionEnum] @State private var selectedValue: OptionEnum { didSet { diff --git a/Mail/Views/Switch User/AccountCellView.swift b/Mail/Views/Switch User/AccountCellView.swift index 8b17d7ef3..a07d12074 100644 --- a/Mail/Views/Switch User/AccountCellView.swift +++ b/Mail/Views/Switch User/AccountCellView.swift @@ -27,7 +27,7 @@ import SwiftUI struct AccountCellView: View { @LazyInjectService private var accountManager: AccountManager - + @Environment(\.dismissModal) var dismissModal let account: Account diff --git a/Mail/Views/Switch User/AddMailboxView.swift b/Mail/Views/Switch User/AddMailboxView.swift index 6e5bfbb83..b09e5a34a 100644 --- a/Mail/Views/Switch User/AddMailboxView.swift +++ b/Mail/Views/Switch User/AddMailboxView.swift @@ -27,7 +27,7 @@ struct AddMailboxView: View { @LazyInjectService private var accountManager: AccountManager @LazyInjectService private var snackbarPresenter: SnackBarPresentable - + @State private var newAddress = "" @State private var password = "" @State private var showError = false 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/MailCore/Cache/AccountManager.swift b/MailCore/Cache/AccountManager.swift index 8a66256fe..639d38fce 100644 --- a/MailCore/Cache/AccountManager.swift +++ b/MailCore/Cache/AccountManager.swift @@ -79,7 +79,7 @@ public final class AccountManager: RefreshTokenDelegate, ObservableObject { 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 { 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/URLSchemeHandler.swift b/MailCore/Utils/URLSchemeHandler.swift index 23476841c..2b7b7336c 100644 --- a/MailCore/Utils/URLSchemeHandler.swift +++ b/MailCore/Utils/URLSchemeHandler.swift @@ -34,7 +34,7 @@ public final class URLSchemeHandler: NSObject, WKURLSchemeHandler { urlSchemeTask.didFailWithError(MailError.resourceError) return } - + guard let currentAccessToken = accountManager.getCurrentAccount()?.token?.accessToken else { urlSchemeTask.didFailWithError(MailError.unknownError) return diff --git a/MailShareExtension/ComposeMessageWrapperView.swift b/MailShareExtension/ComposeMessageWrapperView.swift index a13efa144..8b225b663 100644 --- a/MailShareExtension/ComposeMessageWrapperView.swift +++ b/MailShareExtension/ComposeMessageWrapperView.swift @@ -19,8 +19,8 @@ import InfomaniakCore import InfomaniakCoreUI import InfomaniakDI -import MailResources import MailCore +import MailResources import Social import SwiftUI import UIKit diff --git a/MailShareExtension/Proxy/ApplicationState.swift b/MailShareExtension/Proxy/ApplicationState.swift index 43c701d41..926be5016 100644 --- a/MailShareExtension/Proxy/ApplicationState.swift +++ b/MailShareExtension/Proxy/ApplicationState.swift @@ -16,8 +16,8 @@ along with this program. If not, see . */ -import UIKit import MailCore +import UIKit public struct ApplicationState: ApplicationStatable { public var applicationState: UIApplication.State? { diff --git a/Project.swift b/Project.swift index d9e49b689..655d5220e 100644 --- a/Project.swift +++ b/Project.swift @@ -24,7 +24,10 @@ 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("4eaefd644f75d833d6b1009dd94a9d6d674ccb53") + ), .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")), @@ -36,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")), @@ -114,7 +120,7 @@ let project = Project(name: "Mail", "Mail/Utils/**", "Mail/Views/**", "Mail/Proxy/Protocols/**"], - resources:[ + resources: [ "MailShareExtension/Base.lproj/MainInterface.storyboard", "MailShareExtension/ShareExtension.entitlements", "Mail/**/*.storyboard", diff --git a/Tuist/ProjectDescriptionHelpers/Constants.swift b/Tuist/ProjectDescriptionHelpers/Constants.swift index ce18cbfdf..31070de2c 100644 --- a/Tuist/ProjectDescriptionHelpers/Constants.swift +++ b/Tuist/ProjectDescriptionHelpers/Constants.swift @@ -20,11 +20,11 @@ import ProjectDescription public enum Constants { public static let baseSettings = SettingsDictionary() - .currentProjectVersion("1") - .marketingVersion("1.0.3") - .automaticCodeSigning(devTeam: "864VDCS2QY") - + .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") -} \ No newline at end of file +} diff --git a/Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift b/Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift index 74c4e2b86..4d2d540a2 100644 --- a/Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift +++ b/Tuist/ProjectDescriptionHelpers/ExtensionTarget.swift @@ -19,5 +19,5 @@ import ProjectDescription public extension Target { - // TODO move contructors here if needed + // TODO: move contructors here if needed } From 871647788c51bd0d830ffc6291f5a645feff5b26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 3 Aug 2023 10:08:30 +0200 Subject: [PATCH 47/61] fix(mac): SplitViewController configuration works --- Mail/Views/SplitView.swift | 11 ++++++++++- ...alystDetectable.swift => PlatformDetectable.swift} | 7 +++++++ 2 files changed, 17 insertions(+), 1 deletion(-) rename MailCore/Utils/{CatalystDetectable.swift => PlatformDetectable.swift} (89%) diff --git a/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift index f26068e02..32060a556 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -28,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) @@ -153,7 +159,10 @@ struct SplitView: View { } private func setupBehaviour(orientation: UIInterfaceOrientation) { - if orientation.isLandscape || platformDetector.isMacCatalyst { + 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/MailCore/Utils/CatalystDetectable.swift b/MailCore/Utils/PlatformDetectable.swift similarity index 89% rename from MailCore/Utils/CatalystDetectable.swift rename to MailCore/Utils/PlatformDetectable.swift index ad5a1efe0..390150568 100644 --- a/MailCore/Utils/CatalystDetectable.swift +++ b/MailCore/Utils/PlatformDetectable.swift @@ -25,6 +25,9 @@ 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 } } @@ -42,6 +45,10 @@ public struct PlatformDetector: PlatformDetectable { #endif }() + public var isiOSAppOnMac: Bool = { + ProcessInfo().isiOSAppOnMac + }() + public var isInExtension: Bool = { guard Bundle.main.bundlePath.hasSuffix(".appex") else { return false From de28d6a405c80d1489f152eb51b402d3bc5737a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 3 Aug 2023 10:09:43 +0200 Subject: [PATCH 48/61] fix(theme): Share extension conform to theme --- MailShareExtension/ShareViewController.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MailShareExtension/ShareViewController.swift b/MailShareExtension/ShareViewController.swift index 8eee66e86..7632b4ea8 100644 --- a/MailShareExtension/ShareViewController.swift +++ b/MailShareExtension/ShareViewController.swift @@ -41,6 +41,10 @@ final class ShareNavigationViewController: UIViewController { overrideSnackBarPresenter(contextView: view) + // Set theme + overrideUserInterfaceStyle = UserDefaults.shared.theme.interfaceStyle + view.tintColor = UserDefaults.shared.accentColor.secondary.color + // Modify sheet size on iPadOS, property is ignored on iOS preferredContentSize = CGSize(width: 540, height: 620) From 8968983e4919882168c71397439b4f50e6433bdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 3 Aug 2023 11:35:24 +0200 Subject: [PATCH 49/61] chore: PR Feedback --- Mail/AppDelegate.swift | 3 --- Mail/Helpers/AppAssembly.swift | 3 +++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Mail/AppDelegate.swift b/Mail/AppDelegate.swift index 7d5bae89e..8473f70f9 100644 --- a/Mail/AppDelegate.swift +++ b/Mail/AppDelegate.swift @@ -36,10 +36,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { - Logging.initLogging() - DDLogInfo("Application starting in foreground ? \(applicationState.applicationState != .background)") - ApiFetcher.decoder.dateDecodingStrategy = .iso8601 UNUserNotificationCenter.current().delegate = notificationCenterDelegate Task { diff --git a/Mail/Helpers/AppAssembly.swift b/Mail/Helpers/AppAssembly.swift index f329a2631..2e8166bbe 100644 --- a/Mail/Helpers/AppAssembly.swift +++ b/Mail/Helpers/AppAssembly.swift @@ -129,6 +129,9 @@ enum ApplicationAssembly { /// Something that loads the DI on init public struct EarlyDIHook { public init() { + // Setup debug stack early + Logging.initLogging() + // setup DI ASAP ApplicationAssembly.setupDI() } From 93ba208a24bd42615c0370226bea0820ab2c80a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 3 Aug 2023 12:50:59 +0200 Subject: [PATCH 50/61] fix: Share ext entitlement --- Project.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.swift b/Project.swift index 655d5220e..b2860d385 100644 --- a/Project.swift +++ b/Project.swift @@ -131,7 +131,7 @@ let project = Project(name: "Mail", "MailResources/**/*.css", "MailResources/**/*.js" ], - entitlements: "MailResources/Mail.entitlements", + entitlements: "MailShareExtension/ShareExtension.entitlements", scripts: [Constants.swiftlintScript], dependencies: [ .target(name: "MailCore"), From 0c1afa8462d569075734bbe7c0d10a775541aa33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 3 Aug 2023 12:51:25 +0200 Subject: [PATCH 51/61] fix: Logger requires DI to be set up --- Mail/Helpers/AppAssembly.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Mail/Helpers/AppAssembly.swift b/Mail/Helpers/AppAssembly.swift index 2e8166bbe..1b85edf18 100644 --- a/Mail/Helpers/AppAssembly.swift +++ b/Mail/Helpers/AppAssembly.swift @@ -129,10 +129,10 @@ enum ApplicationAssembly { /// Something that loads the DI on init public struct EarlyDIHook { public init() { - // Setup debug stack early - Logging.initLogging() - // setup DI ASAP ApplicationAssembly.setupDI() + + // Setup debug stack early, requires DI to be setup to work + Logging.initLogging() } } From 08d916e61158d2c4bd91bb3d0cedcab1bd023a07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 3 Aug 2023 13:04:35 +0200 Subject: [PATCH 52/61] refactor: renamed MessagePresentable to UserAlertDisplayable --- Mail/Helpers/AppAssembly.swift | 4 ++-- MailCore/Cache/DraftManager.swift | 22 +++++++++---------- ...table.swift => UserAlertDisplayable.swift} | 20 ++++++++--------- .../NotificationServiceAssembly.swift | 4 ++-- 4 files changed, 25 insertions(+), 25 deletions(-) rename MailCore/Utils/{MessagePresentable.swift => UserAlertDisplayable.swift} (86%) diff --git a/Mail/Helpers/AppAssembly.swift b/Mail/Helpers/AppAssembly.swift index 1b85edf18..df189efdc 100644 --- a/Mail/Helpers/AppAssembly.swift +++ b/Mail/Helpers/AppAssembly.swift @@ -79,8 +79,8 @@ enum ApplicationAssembly { Factory(type: SnackBarPresentable.self) { _, _ in SnackBarPresenter() }, - Factory(type: MessagePresentable.self) { _, _ in - MessagePresenter() + Factory(type: UserAlertDisplayable.self) { _, _ in + UserAlertDisplayer() }, Factory(type: ApplicationStatable.self) { _, _ in ApplicationState() diff --git a/MailCore/Cache/DraftManager.swift b/MailCore/Cache/DraftManager.swift index 52384e481..b176c9f3a 100644 --- a/MailCore/Cache/DraftManager.swift +++ b/MailCore/Cache/DraftManager.swift @@ -70,7 +70,7 @@ public final class DraftManager { private static let saveExpirationSec = 3 @LazyInjectService private var matomo: MatomoUtils - @LazyInjectService private var messagePresentable: MessagePresentable + @LazyInjectService private var alertDisplayable: UserAlertDisplayable /// Used by DI only public init() { @@ -93,13 +93,13 @@ public final class DraftManager { try await mailboxManager.save(draft: draft) } catch { guard error.shouldDisplay else { return } - messagePresentable.show(message: error.localizedDescription) + alertDisplayable.show(message: error.localizedDescription) } await draftQueue.endBackgroundTask(uuid: draft.localUUID) } public func send(draft: Draft, mailboxManager: MailboxManager) async -> Date? { - messagePresentable.show(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) - messagePresentable.show(message: MailResourcesStrings.Localizable.snackbarEmailSent) + alertDisplayable.show(message: MailResourcesStrings.Localizable.snackbarEmailSent) sendDate = cancelableResponse.scheduledDate } catch { - messagePresentable.show(message: error.localizedDescription) + alertDisplayable.show(message: error.localizedDescription) } await draftQueue.endBackgroundTask(uuid: draft.localUUID) return sendDate @@ -162,11 +162,11 @@ public final class DraftManager { } // Present a matching message - @InjectService var messagePresentable: MessagePresentable + @InjectService var alertDisplayable: UserAlertDisplayable if draft.action == .send { - messagePresentable.show(message: MailResourcesStrings.Localizable.snackbarEmailSending) + alertDisplayable.show(message: MailResourcesStrings.Localizable.snackbarEmailSending) } else { - messagePresentable.show(message: MailResourcesStrings.Localizable.snackbarDraftSaved) + alertDisplayable.show(message: MailResourcesStrings.Localizable.snackbarDraftSaved) } } } @@ -190,11 +190,11 @@ public final class DraftManager { public func initialSaveRemotelyAndNotify(draft: Draft, mailboxManager: MailboxManager) async -> Bool { let saved = await initialSaveRemotelyIfNonEmpty(draft: draft, mailboxManager: mailboxManager) if saved { - let messageAction: MessageAction = (MailResourcesStrings.Localizable.actionDelete, { [weak self] in + let messageAction: UserAlertAction = (MailResourcesStrings.Localizable.actionDelete, { [weak self] in self?.matomo.track(eventWithCategory: .snackbar, name: "deleteDraft") self?.deleteDraftSnackBarAction(draft: draft, mailboxManager: mailboxManager) }) - messagePresentable.show(message: MailResourcesStrings.Localizable.snackbarDraftSaved, action: messageAction) + alertDisplayable.show(message: MailResourcesStrings.Localizable.snackbarDraftSaved, action: messageAction) } return saved } @@ -260,7 +260,7 @@ public final class DraftManager { await tryOrDisplayError { if let liveDraft = draft.thaw() { try await mailboxManager.delete(draft: liveDraft.freeze()) - messagePresentable.show(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/Utils/MessagePresentable.swift b/MailCore/Utils/UserAlertDisplayable.swift similarity index 86% rename from MailCore/Utils/MessagePresentable.swift rename to MailCore/Utils/UserAlertDisplayable.swift index 45e1a8142..4db668ba5 100644 --- a/MailCore/Utils/MessagePresentable.swift +++ b/MailCore/Utils/UserAlertDisplayable.swift @@ -28,7 +28,7 @@ import UserNotifications /// Something that can present a message to the user, abstracted of execution context (App / NSExtension) /// /// Will present a snackbar while main app is opened, a local notification in Extension or Background. -public protocol MessagePresentable { +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) @@ -37,12 +37,12 @@ public protocol MessagePresentable { /// - Parameters: /// - message: The message to display /// - action: Title and closure associated with the action - func show(message: String, action: MessageAction) + func show(message: String, action: UserAlertAction) } -public typealias MessageAction = (name: String, closure: () -> Void) +public typealias UserAlertAction = (name: String, closure: () -> Void) -public final class MessagePresenter: MessagePresentable { +public final class UserAlertDisplayer: UserAlertDisplayable { @LazyInjectService private var snackbarPresenter: SnackBarPresentable @LazyInjectService private var applicationState: ApplicationStatable @@ -51,19 +51,19 @@ public final class MessagePresenter: MessagePresentable { // META: keep sonarcloud happy } - // MARK: - MessagePresentable + // MARK: - UserAlertDisplayable public func show(message: String) { showInContext(message: message, action: nil) } - public func show(message: String, action: MessageAction) { + public func show(message: String, action: UserAlertAction) { showInContext(message: message, action: action) } // MARK: - private - private func showInContext(message: String, action: MessageAction?) { + private func showInContext(message: String, action: UserAlertAction?) { Task { @MainActor in // check not in extension mode guard !Bundle.main.isExtension else { @@ -84,7 +84,7 @@ public final class MessagePresenter: MessagePresentable { // MARK: Private - private func presentInSnackbar(message: String, action: MessageAction?) { + private func presentInSnackbar(message: String, action: UserAlertAction?) { guard let action = action else { snackbarPresenter.show(message: message) return @@ -94,7 +94,7 @@ public final class MessagePresenter: MessagePresentable { snackbarPresenter.show(message: message, action: snackBarAction) } - private func presentInLocalNotification(message: String, action: MessageAction?) { + private func presentInLocalNotification(message: String, action: UserAlertAction?) { if action != nil { DDLogError("Action not implemented in notifications for now") } @@ -107,7 +107,7 @@ public final class MessagePresenter: MessagePresentable { let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: trigger) let notificationCenter = UNUserNotificationCenter.current() notificationCenter.add(request) { error in - DDLogError("MessagePresenter local notification error:\(String(describing: error)) ") + DDLogError("UserAlertDisplayer local notification error:\(String(describing: error)) ") } // Self destruct this notification, as used only for user feedback diff --git a/MailNotificationServiceExtension/NotificationServiceAssembly.swift b/MailNotificationServiceExtension/NotificationServiceAssembly.swift index 7ffd657c6..fdbb22865 100644 --- a/MailNotificationServiceExtension/NotificationServiceAssembly.swift +++ b/MailNotificationServiceExtension/NotificationServiceAssembly.swift @@ -62,8 +62,8 @@ enum NotificationServiceAssembly { Factory(type: SnackBarPresentable.self) { _, _ in SnackBarPresenter() }, - Factory(type: MessagePresentable.self) { _, _ in - MessagePresenter() + Factory(type: UserAlertDisplayable.self) { _, _ in + UserAlertDisplayer() }, Factory(type: UserActivityController.self) { _, _ in UserActivityController() From 7c28709dfcd612c2f9eea8c68cf64c2550921378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 3 Aug 2023 14:11:50 +0200 Subject: [PATCH 53/61] chore: bump core --- Project.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.swift b/Project.swift index b2860d385..454d08dce 100644 --- a/Project.swift +++ b/Project.swift @@ -26,7 +26,7 @@ let project = Project(name: "Mail", .package(url: "https://github.com/Infomaniak/ios-dependency-injection", .upToNextMajor(from: "1.1.6")), .package( url: "https://github.com/Infomaniak/ios-core", - .revision("4eaefd644f75d833d6b1009dd94a9d6d674ccb53") + .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")), From a09ccc104cbf19d4cb828ab8665c4b5b9112047a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 3 Aug 2023 15:09:06 +0200 Subject: [PATCH 54/61] fix(ComposeMessageView): navigationViewStyle .stack for iPad display --- Mail/Views/New Message/ComposeMessageView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Mail/Views/New Message/ComposeMessageView.swift b/Mail/Views/New Message/ComposeMessageView.swift index fd7abac01..2845d7166 100644 --- a/Mail/Views/New Message/ComposeMessageView.swift +++ b/Mail/Views/New Message/ComposeMessageView.swift @@ -125,6 +125,7 @@ struct ComposeMessageView: View { NavigationView { composeMessage } + .navigationViewStyle(.stack) .task { do { isLoadingContent = true From 435fe139d618c5e57c0279bad48ee276947b8f40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 3 Aug 2023 15:14:00 +0200 Subject: [PATCH 55/61] refactor: directly use openURL() --- Mail/Views/Menu Drawer/Items/MenuDrawerItemsListView.swift | 4 ++-- .../General/SettingsNotificationsInstructionsView.swift | 2 +- Mail/Views/Settings/General/SettingsNotificationsView.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Mail/Views/Menu Drawer/Items/MenuDrawerItemsListView.swift b/Mail/Views/Menu Drawer/Items/MenuDrawerItemsListView.swift index 74364db60..ef5413844 100644 --- a/Mail/Views/Menu Drawer/Items/MenuDrawerItemsListView.swift +++ b/Mail/Views/Menu Drawer/Items/MenuDrawerItemsListView.swift @@ -37,7 +37,7 @@ struct MenuDrawerItemsAdvancedListView: View { MenuDrawerItemCell(icon: MailResourcesAsset.drawerDownload, label: MailResourcesStrings.Localizable.buttonImportEmails, matomoName: "importEmails") { - openURL.callAsFunction(URLConstants.importMails.url) + openURL(URLConstants.importMails.url) } if mailboxCanRestoreEmails { MenuDrawerItemCell( @@ -91,7 +91,7 @@ struct MenuDrawerItemsHelpListView: View { if mailboxManager.account.user?.isStaff == true { isShowingBugTracker.toggle() } else if let userReportURL = URL(string: MailResourcesStrings.Localizable.urlUserReportiOS) { - openURL.callAsFunction(userReportURL) + openURL(userReportURL) } } } diff --git a/Mail/Views/Settings/General/SettingsNotificationsInstructionsView.swift b/Mail/Views/Settings/General/SettingsNotificationsInstructionsView.swift index b0602db50..dc4046dcb 100644 --- a/Mail/Views/Settings/General/SettingsNotificationsInstructionsView.swift +++ b/Mail/Views/Settings/General/SettingsNotificationsInstructionsView.swift @@ -42,7 +42,7 @@ struct SettingsNotificationsInstructionsView: View { return } - openURL.callAsFunction(settingsUrl) + openURL(settingsUrl) } } diff --git a/Mail/Views/Settings/General/SettingsNotificationsView.swift b/Mail/Views/Settings/General/SettingsNotificationsView.swift index 762b7d9b5..0d6e939b6 100644 --- a/Mail/Views/Settings/General/SettingsNotificationsView.swift +++ b/Mail/Views/Settings/General/SettingsNotificationsView.swift @@ -53,7 +53,7 @@ struct SettingsNotificationsView: View { return } - openURL.callAsFunction(settingsUrl) + openURL(settingsUrl) } .mailButtonStyle(.link) } From 067450c91a2a29eb4ba254458f79cf6b56fbdf8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 3 Aug 2023 15:14:24 +0200 Subject: [PATCH 56/61] chore: bump core --- .package.resolved | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" } }, { From fb53d61f75f8ed0675b4965787b89eb562322e37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 3 Aug 2023 15:15:49 +0200 Subject: [PATCH 57/61] fix(ShareViewController.swift): use tint color primary --- MailShareExtension/ShareViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MailShareExtension/ShareViewController.swift b/MailShareExtension/ShareViewController.swift index 7632b4ea8..27bb48aa2 100644 --- a/MailShareExtension/ShareViewController.swift +++ b/MailShareExtension/ShareViewController.swift @@ -43,7 +43,7 @@ final class ShareNavigationViewController: UIViewController { // Set theme overrideUserInterfaceStyle = UserDefaults.shared.theme.interfaceStyle - view.tintColor = UserDefaults.shared.accentColor.secondary.color + view.tintColor = UserDefaults.shared.accentColor.primary.color // Modify sheet size on iPadOS, property is ignored on iOS preferredContentSize = CGSize(width: 540, height: 620) From 1b1c81dbcfde8083d1d12358aa54a57f01efa451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 3 Aug 2023 15:17:28 +0200 Subject: [PATCH 58/61] chore: PR Feedback --- Mail/Views/Menu Drawer/Items/MenuDrawerItemsListView.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Mail/Views/Menu Drawer/Items/MenuDrawerItemsListView.swift b/Mail/Views/Menu Drawer/Items/MenuDrawerItemsListView.swift index ef5413844..2eed3c6d2 100644 --- a/Mail/Views/Menu Drawer/Items/MenuDrawerItemsListView.swift +++ b/Mail/Views/Menu Drawer/Items/MenuDrawerItemsListView.swift @@ -63,8 +63,6 @@ struct MenuDrawerItemsHelpListView: View { @State private var isShowingHelp = false @State private var isShowingBugTracker = false - @LazyInjectService private var accountManager: AccountManager - var body: some View { MenuDrawerItemsListView { MenuDrawerItemCell(icon: MailResourcesAsset.feedback, From 42712e41c4bebf90715da83ca70b44f259fd7088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 3 Aug 2023 15:31:31 +0200 Subject: [PATCH 59/61] chore: remove extraneous copy statement in project configuration --- Project.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Project.swift b/Project.swift index 454d08dce..60862aa31 100644 --- a/Project.swift +++ b/Project.swift @@ -122,7 +122,6 @@ let project = Project(name: "Mail", "Mail/Proxy/Protocols/**"], resources: [ "MailShareExtension/Base.lproj/MainInterface.storyboard", - "MailShareExtension/ShareExtension.entitlements", "Mail/**/*.storyboard", "MailResources/**/*.xcassets", "MailResources/**/*.strings", From 08bedb14d9b7a323fec1529c97d5b149eb13ea5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 3 Aug 2023 15:53:12 +0200 Subject: [PATCH 60/61] fix(parsing): making sure we decode dates as iso8601 in app _and_ extension --- Mail/Helpers/AppAssembly.swift | 3 +++ Mail/MailApp.swift | 1 - .../NotificationServiceAssembly.swift | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Mail/Helpers/AppAssembly.swift b/Mail/Helpers/AppAssembly.swift index df189efdc..c98f437af 100644 --- a/Mail/Helpers/AppAssembly.swift +++ b/Mail/Helpers/AppAssembly.swift @@ -129,6 +129,9 @@ enum ApplicationAssembly { /// Something that loads the DI on init public struct EarlyDIHook { public init() { + // Setup date encoding + ApiFetcher.decoder.dateDecodingStrategy = .iso8601 + // setup DI ASAP ApplicationAssembly.setupDI() diff --git a/Mail/MailApp.swift b/Mail/MailApp.swift index 40810ee4e..efd0a763a 100644 --- a/Mail/MailApp.swift +++ b/Mail/MailApp.swift @@ -47,7 +47,6 @@ struct MailApp: App { init() { DDLogInfo("Application starting in foreground ? \(UIApplication.shared.applicationState != .background)") - ApiFetcher.decoder.dateDecodingStrategy = .iso8601 } var body: some Scene { diff --git a/MailNotificationServiceExtension/NotificationServiceAssembly.swift b/MailNotificationServiceExtension/NotificationServiceAssembly.swift index fdbb22865..13ca3688e 100644 --- a/MailNotificationServiceExtension/NotificationServiceAssembly.swift +++ b/MailNotificationServiceExtension/NotificationServiceAssembly.swift @@ -93,6 +93,8 @@ enum NotificationServiceAssembly { /// Something that loads the DI on init public struct EarlyDIHook { public init() { + ApiFetcher.decoder.dateDecodingStrategy = .iso8601 + // setup DI ASAP NotificationServiceAssembly.setupDI() } From 2e3aeeb9c39d22a8a2fd0f05ec5b2cfa634991d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adrien=20Coye=20de=20Brune=CC=81lis?= Date: Thu, 3 Aug 2023 16:10:39 +0200 Subject: [PATCH 61/61] refactor: use common way to send a mail --- .../New Message/ComposeMessageView.swift | 4 -- MailCore/Cache/DraftManager.swift | 54 +++++-------------- .../ComposeMessageWrapperView.swift | 15 +----- 3 files changed, 13 insertions(+), 60 deletions(-) diff --git a/Mail/Views/New Message/ComposeMessageView.swift b/Mail/Views/New Message/ComposeMessageView.swift index 2845d7166..da5d65852 100644 --- a/Mail/Views/New Message/ComposeMessageView.swift +++ b/Mail/Views/New Message/ComposeMessageView.swift @@ -150,10 +150,6 @@ struct ComposeMessageView: View { } } .onDisappear { - // Only save draft on Disappear on the main app. Share extension manages its own Draft() - guard !Bundle.main.isExtension else { - return - } draftManager.syncDraft(mailboxManager: mailboxManager) } .interactiveDismissDisabled() diff --git a/MailCore/Cache/DraftManager.swift b/MailCore/Cache/DraftManager.swift index b176c9f3a..17e2c1fdf 100644 --- a/MailCore/Cache/DraftManager.swift +++ b/MailCore/Cache/DraftManager.swift @@ -125,7 +125,7 @@ public final class DraftManager { var sendDate: Date? switch draft.action { case .initialSave: - await self.initialSaveRemotelyAndNotify(draft: draft, mailboxManager: mailboxManager) + await self.initialSaveRemotely(draft: draft, mailboxManager: mailboxManager) case .save: await self.saveDraftRemotely(draft: draft, mailboxManager: mailboxManager) case .send: @@ -148,55 +148,25 @@ public final class DraftManager { } } - /// Process a `draft` when the ShareExtension dismisses. - /// - Parameters: - /// - draft: Expecting a .detached draft - /// - mailboxManager: the mailbox manager - public func saveAndProcessDraftFromShareExtension(draft: Draft, mailboxManager: MailboxManager) { - Task { - let saved = await self.initialSaveRemotelyIfNonEmpty(draft: draft, mailboxManager: mailboxManager) - - // No message for empty draft - guard saved else { - return - } - - // Present a matching message - @InjectService var alertDisplayable: UserAlertDisplayable - if draft.action == .send { - alertDisplayable.show(message: MailResourcesStrings.Localizable.snackbarEmailSending) - } else { - alertDisplayable.show(message: MailResourcesStrings.Localizable.snackbarDraftSaved) - } - } - } - - /// First save of a draft with the remote if non empty + /// First save of a draft with the remote, if non empty. + /// + /// Present a message with a `delete draft` action @discardableResult - private func initialSaveRemotelyIfNonEmpty(draft: Draft, mailboxManager: MailboxManager) async -> Bool { + 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) - return true - } - /// First save of a draft with the remote if non empty. - /// - /// Present a message with a `delete draft` action - @discardableResult - public func initialSaveRemotelyAndNotify(draft: Draft, mailboxManager: MailboxManager) async -> Bool { - let saved = await initialSaveRemotelyIfNonEmpty(draft: draft, mailboxManager: mailboxManager) - if saved { - 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 saved + 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 diff --git a/MailShareExtension/ComposeMessageWrapperView.swift b/MailShareExtension/ComposeMessageWrapperView.swift index 8b225b663..581a01ef0 100644 --- a/MailShareExtension/ComposeMessageWrapperView.swift +++ b/MailShareExtension/ComposeMessageWrapperView.swift @@ -35,20 +35,7 @@ struct ComposeMessageWrapperView: View { init(dismissHandler: @escaping SimpleClosure, itemProviders: [NSItemProvider], draft: Draft = Draft()) { _draft = State(wrappedValue: draft) - - // Append save draft action if possible - @InjectService var manager: AccountManager - if let mailboxManager = manager.currentMailboxManager { - let saveDraft: SimpleClosure = { _ in - let detachedDraft = draft.detached() - @InjectService var draftManager: DraftManager - draftManager.saveAndProcessDraftFromShareExtension(draft: detachedDraft, mailboxManager: mailboxManager) - } - self.dismissHandler = saveDraft + dismissHandler - } else { - self.dismissHandler = dismissHandler - } - + self.dismissHandler = dismissHandler self.itemProviders = itemProviders }